diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..77feb3181 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,44 @@ +# +# SPDX-FileCopyrightText: 2025 NewPipe e.V. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +root = true + +[*.{kt,kts}] +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 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 686ae233a..069f003f4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -3,6 +3,19 @@ NewPipe contribution guidelines =============================== +## AI policy + +* Using generative AI to develop new features or making larger code changes is generally prohibited. Please refrain from contributions which are heavily depending on AI generated source code because they are usually lacking a fundamental understanding of the overall project structure and thus come with poor quality. However, you are allowed to use gen. AI if you + * are aware of the project structure, + * ensure that the generated code follows the project structure, + * fully understand the generated code, and + * review the generated code completely. +* Using AI to find the root cause of bugs and generating small fixes might be acceptable. However, gen. AI often does not fix the underlying problem but is trying to fix the symptoms. If you are using AI to fix bugs, ensure that the root cause is tackled. +* The use of AI to generate documentation is allowed. We ask you to thoroughly check the quality of generated documentation – wrong, misleading or uninformative documentation is useless and wastes the reader's time. Ensure that reasoning is documented. +* Using generative AI to write or fill in PR or issue templates is prohibited. Those texts are often lengthy and miss critical information. +* PRs and issues that do not follow this AI policy can be closed without further explanation. + + ## Crash reporting Report crashes through the **automated crash report system** of NewPipe. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 52897f1ac..60c94ad25 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -26,6 +26,8 @@ body: required: true - label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." required: true + - label: "I have read and understood the [AI policy](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md#ai-policy). The content of this bug report is not generated by AI." + required: true - type: input id: app-version diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 31ef92c44..97a3e38b5 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -25,6 +25,8 @@ body: required: true - label: "I have read and understood the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md)." required: true + - label: "I have read and understood the [AI policy](https://github.com/TeamNewPipe/NewPipe/blob/dev/.github/CONTRIBUTING.md#ai-policy). The content of this request is not generated by AI." + required: true - type: textarea diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 407c00a39..2af1556d4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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) @@ -32,3 +32,5 @@ The APK can be found by going to the "Checks" tab below the title. On the left p #### Due diligence - [ ] I read the [contribution guidelines](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md). +- [ ] The proposed changes follow the [AI policy](https://github.com/TeamNewPipe/NewPipe/blob/HEAD/.github/CONTRIBUTING.md#ai-policy). +- [ ] I tested the changes using an emulator or a physical device. diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 0fad8e169..b558d90dd 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -7,11 +7,11 @@ jobs: release: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: ref: 'master' - - uses: actions/setup-java@v4 + - uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: '21' @@ -32,7 +32,7 @@ jobs: mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk" - name: "Upload APK" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app path: app/build/outputs/apk/release/*.apk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6708fa83..d42c5a0b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,8 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - - uses: gradle/wrapper-validation-action@v2 + - uses: actions/checkout@v6 + - uses: gradle/actions/wrapper-validation@v4 - name: create and checkout branch # push events already checked out the branch @@ -48,7 +48,7 @@ jobs: run: git checkout -B "$BRANCH" - name: set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" @@ -58,7 +58,7 @@ jobs: run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint - name: Upload APK - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: app path: app/build/outputs/apk/debug/*.apk @@ -72,15 +72,15 @@ jobs: - api-level: 21 target: default arch: x86 - - api-level: 33 - target: google_apis # emulator API 33 only exists with Google APIs + - api-level: 35 + target: default arch: x86_64 permissions: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Enable KVM run: | @@ -89,7 +89,7 @@ jobs: sudo udevadm trigger --name-match=kvm - name: set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" @@ -104,7 +104,7 @@ jobs: script: ./gradlew connectedCheck --stacktrace - name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: failure() with: name: android-test-report-api${{ matrix.api-level }} @@ -118,19 +118,19 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Set up JDK - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: 21 distribution: "temurin" cache: 'gradle' - name: Cache SonarCloud packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar diff --git a/.github/workflows/image-minimizer.yml b/.github/workflows/image-minimizer.yml index d9241c33b..264a0ac6c 100644 --- a/.github/workflows/image-minimizer.yml +++ b/.github/workflows/image-minimizer.yml @@ -17,9 +17,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 16 @@ -27,7 +27,7 @@ jobs: run: npm i probe-image-size@7.2.3 --ignore-scripts - name: Minimize simple images - uses: actions/github-script@v7 + uses: actions/github-script@v8 timeout-minutes: 3 with: script: | diff --git a/.gitignore b/.gitignore index 1352b6917..49267a9f0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ captures/ *.iml *~ .weblate +.kotlin *.class app/debug/ app/release/ diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index d6c93c1f7..000000000 --- a/app/build.gradle +++ /dev/null @@ -1,360 +0,0 @@ -import com.android.tools.profgen.ArtProfileKt -import com.android.tools.profgen.ArtProfileSerializer -import com.android.tools.profgen.DexFile - -plugins { - id "com.android.application" - id "kotlin-android" - id "kotlin-kapt" - id "kotlin-parcelize" - id "checkstyle" - id "org.sonarqube" version "4.0.0.2929" -} - -android { - compileSdk 34 - namespace 'org.schabi.newpipe' - - defaultConfig { - applicationId "org.schabi.newpipe" - resValue "string", "app_name", "NewPipe" - minSdk 21 - targetSdk 33 - 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 - 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'] - } - } -} - -ext { - checkstyleVersion = '10.12.1' - - androidxLifecycleVersion = '2.6.2' - androidxRoomVersion = '2.6.1' - androidxWorkVersion = '2.8.1' - - stateSaverVersion = '1.4.1' - exoPlayerVersion = '2.18.7' - googleAutoServiceVersion = '1.1.1' - groupieVersion = '2.10.1' - markwonVersion = '4.6.2' - - leakCanaryVersion = '2.12' - stethoVersion = '1.6.0' -} - -configurations { - checkstyle - ktlint -} - -checkstyle { - getConfigDirectory().set(rootProject.file("checkstyle")) - ignoreFailures false - showViolations true - toolVersion = checkstyleVersion -} - -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") -} - -afterEvaluate { - if (!System.properties.containsKey('skipFormatKtlint')) { - preDebugBuild.dependsOn formatKtlint - } - preDebugBuild.dependsOn runCheckstyle, runKtlint -} - -sonar { - properties { - property "sonar.projectKey", "TeamNewPipe_NewPipe" - property "sonar.organization", "teamnewpipe" - property "sonar.host.url", "https://sonarcloud.io" - } -} - -dependencies { -/** Desugaring **/ - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' - -/** NewPipe libraries **/ - // You can use a local version by uncommenting a few lines in settings.gradle - // Or you can use a commit you pushed to GitHub by just replacing TeamNewPipe with your GitHub - // name and the commit hash with the commit hash of the (pushed) commit you want to test - // This works thanks to JitPack: https://jitpack.io/ - implementation 'com.github.TeamNewPipe:nanojson:e9d656ddb49a412a5a0a5d5ef20ca7ef09549996' - // WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with - // the corresponding commit hash, since JitPack sometimes deletes artifacts. - // If there’s already a git hash, just add more of it to the end (or remove a letter) - // to cause jitpack to regenerate the artifact. - implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.24.8' - implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' - -/** Checkstyle **/ - checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" - ktlint 'com.pinterest:ktlint:0.45.2' - -/** Kotlin **/ - implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" - -/** AndroidX **/ - implementation 'androidx.appcompat:appcompat:1.7.1' - implementation 'androidx.cardview:cardview:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.documentfile:documentfile:1.0.1' - implementation 'androidx.fragment:fragment-ktx:1.6.2' - implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}" - implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' - implementation 'androidx.media:media:1.7.0' - implementation 'androidx.preference:preference:1.2.1' - implementation 'androidx.recyclerview:recyclerview:1.3.2' - implementation "androidx.room:room-runtime:${androidxRoomVersion}" - implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" - kapt "androidx.room:room-compiler:${androidxRoomVersion}" - implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' - // Newer version specified to prevent accessibility regressions with RecyclerView, see: - // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 - implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' - implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" - implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" - implementation 'com.google.android.material:material:1.11.0' - implementation "androidx.webkit:webkit:1.9.0" - -/** Third-party libraries **/ - // Instance state boilerplate elimination - implementation 'com.github.livefront:bridge:v2.0.2' - implementation "com.evernote:android-state:$stateSaverVersion" - kapt "com.evernote:android-state-processor:$stateSaverVersion" - - // HTML parser - implementation "org.jsoup:jsoup:1.17.2" - - // HTTP client - implementation "com.squareup.okhttp3:okhttp:4.12.0" - - // Media player - implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-dash:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-database:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-datasource:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-hls:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-smoothstreaming:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:exoplayer-ui:${exoPlayerVersion}" - implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}" - - // Metadata generator for service descriptors - compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}" - kapt "com.google.auto.service:auto-service:${googleAutoServiceVersion}" - - // Manager for complex RecyclerView layouts - implementation "com.github.lisawray.groupie:groupie:${groupieVersion}" - implementation "com.github.lisawray.groupie:groupie-viewbinding:${groupieVersion}" - - // Image loading - //noinspection GradleDependency --> 2.8 is the last version, not 2.71828! - implementation "com.squareup.picasso:picasso:2.8" - - // Markdown library for Android - implementation "io.noties.markwon:core:${markwonVersion}" - implementation "io.noties.markwon:linkify:${markwonVersion}" - - // Crash reporting - implementation "ch.acra:acra-core:5.11.3" - - // Properly restarting - implementation 'com.jakewharton:process-phoenix:2.1.2' - - // Reactive extensions for Java VM - implementation "io.reactivex.rxjava3:rxjava:3.1.8" - implementation "io.reactivex.rxjava3:rxandroid:3.0.2" - // RxJava binding APIs for Android UI widgets - implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" - - // Date and time formatting - implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" - -/** Debugging **/ - // Memory leak detection - debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" - debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}" - debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}" - // Debug bridge for Android - debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" - debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" - -/** Testing **/ - testImplementation 'junit:junit:4.13.2' - testImplementation 'org.mockito:mockito-core:5.6.0' - - androidTestImplementation "androidx.test.ext:junit:1.1.5" - androidTestImplementation "androidx.test:runner:1.5.2" - androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" - androidTestImplementation "org.assertj:assertj-core:3.24.2" -} - -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, "") - } - } - } - } -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 000000000..1aa5297c5 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,306 @@ +/* + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) + alias(libs.plugins.jetbrains.kotlin.kapt) + alias(libs.plugins.google.ksp) + alias(libs.plugins.jetbrains.kotlin.parcelize) + alias(libs.plugins.sonarqube) + checkstyle +} + +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() ?: 1006 + + versionName = "0.28.1" + 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 + 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("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("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("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") { + 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") + } +} + +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) + + /** AndroidX **/ + implementation(libs.androidx.appcompat) + implementation(libs.androidx.cardview) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.core) + implementation(libs.androidx.documentfile) + implementation(libs.androidx.fragment) + implementation(libs.androidx.lifecycle.livedata) + implementation(libs.androidx.lifecycle.viewmodel) + implementation(libs.androidx.localbroadcastmanager) + 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.viewpager2) + implementation(libs.androidx.work.runtime) + implementation(libs.androidx.work.rxjava3) + implementation(libs.google.android.material) + implementation(libs.androidx.webkit) + + /** Third-party libraries **/ + 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) + + // Manager for complex RecyclerView layouts + implementation(libs.lisawray.groupie.core) + implementation(libs.lisawray.groupie.viewbinding) + + // Image loading + implementation(libs.squareup.picasso) + + // Markdown library for Android + implementation(libs.noties.markwon.core) + implementation(libs.noties.markwon.linkify) + + // Crash reporting + implementation(libs.acra.core) + compileOnly(libs.google.autoservice.annotations) + ksp(libs.zacsweers.autoservice.compiler) + + // 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) + + /** 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) +} diff --git a/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json b/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json index aced06c0a..b9a618638 100644 --- a/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json +++ b/app/schemas/org.schabi.newpipe.database.AppDatabase/9.json @@ -458,7 +458,7 @@ "notNull": true }, { - "fieldPath": "name", + "fieldPath": "orderingName", "columnName": "name", "affinity": "TEXT", "notNull": false diff --git a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt index a34cfece6..4327271f4 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/database/DatabaseMigrationTest.kt @@ -129,7 +129,7 @@ class DatabaseMigrationTest { ) val migratedDatabaseV3 = getMigratedDatabase() - val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() + val listFromDB = migratedDatabaseV3.streamDAO().getAll().blockingFirst() // Only expect 2, the one with the null url will be ignored assertEquals(2, listFromDB.size) @@ -217,7 +217,7 @@ class DatabaseMigrationTest { ) val migratedDatabaseV8 = getMigratedDatabase() - val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst() + val listFromDB = migratedDatabaseV8.searchHistoryDAO().getAll().blockingFirst() assertEquals(2, listFromDB.size) assertEquals("abc", listFromDB[0].search) @@ -283,8 +283,8 @@ class DatabaseMigrationTest { ) val migratedDatabaseV9 = getMigratedDatabase() - var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst() - var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst() + var localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst() + var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst() assertEquals(1, localListFromDB.size) assertEquals(localUid2, localListFromDB[0].uid) @@ -294,17 +294,27 @@ class DatabaseMigrationTest { assertEquals(-1, remoteListFromDB[0].displayIndex) val localUid3 = migratedDatabaseV9.playlistDAO().insert( - PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1) + PlaylistEntity( + name = "${DEFAULT_NAME}3", + isThumbnailPermanent = false, + thumbnailStreamId = -1, + displayIndex = -1 + ) ) val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert( PlaylistRemoteEntity( - DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL, - DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10 + serviceId = DEFAULT_THIRD_SERVICE_ID, + orderingName = DEFAULT_NAME, + url = DEFAULT_THIRD_URL, + thumbnailUrl = DEFAULT_THUMBNAIL, + uploader = DEFAULT_UPLOADER_NAME, + displayIndex = -1, + streamCount = 10 ) ) - localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst() - remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst() + localListFromDB = migratedDatabaseV9.playlistDAO().getAll().blockingFirst() + remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().blockingFirst() assertEquals(2, localListFromDB.size) assertEquals(localUid3, localListFromDB[1].uid) assertEquals(-1, localListFromDB[1].displayIndex) diff --git a/app/src/androidTest/java/org/schabi/newpipe/error/ErrorInfoTest.java b/app/src/androidTest/java/org/schabi/newpipe/error/ErrorInfoTest.java index 891824a55..892d1df0f 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/error/ErrorInfoTest.java +++ b/app/src/androidTest/java/org/schabi/newpipe/error/ErrorInfoTest.java @@ -12,6 +12,7 @@ import org.schabi.newpipe.extractor.ServiceList; import org.schabi.newpipe.extractor.exceptions.ParsingException; import java.util.Arrays; +import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -23,8 +24,23 @@ import static org.junit.Assert.assertTrue; @LargeTest public class ErrorInfoTest { + /** + * @param errorInfo the error info to access + * @return the private field errorInfo.message.stringRes using reflection + */ + private int getMessageFromErrorInfo(final ErrorInfo errorInfo) + throws NoSuchFieldException, IllegalAccessException { + final var message = ErrorInfo.class.getDeclaredField("message"); + message.setAccessible(true); + final var messageValue = (ErrorInfo.Companion.ErrorMessage) message.get(errorInfo); + + final var stringRes = ErrorInfo.Companion.ErrorMessage.class.getDeclaredField("stringRes"); + stringRes.setAccessible(true); + return (int) Objects.requireNonNull(stringRes.get(messageValue)); + } + @Test - public void errorInfoTestParcelable() { + public void errorInfoTestParcelable() throws NoSuchFieldException, IllegalAccessException { final ErrorInfo info = new ErrorInfo(new ParsingException("Hello"), UserAction.USER_REPORT, "request", ServiceList.YouTube.getServiceId()); // Obtain a Parcel object and write the parcelable object to it: @@ -39,7 +55,7 @@ public class ErrorInfoTest { assertEquals(ServiceList.YouTube.getServiceInfo().getName(), infoFromParcel.getServiceName()); assertEquals("request", infoFromParcel.getRequest()); - assertEquals(R.string.parsing_error, infoFromParcel.getMessageStringId()); + assertEquals(R.string.parsing_error, getMessageFromErrorInfo(infoFromParcel)); parcel.recycle(); } diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt b/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt index 24be0f868..0de9dd268 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/local/history/HistoryRecordManagerTest.kt @@ -41,7 +41,7 @@ class HistoryRecordManagerTest { // For some reason the Flowable returned by getAll() never completes, so we can't assert // that the number of Lists it returns is exactly 1, we can only check if the first List is // correct. Why on earth has a Flowable been used instead of a Single for getAll()?!? - val entities = database.searchHistoryDAO().all.blockingFirst() + val entities = database.searchHistoryDAO().getAll().blockingFirst() assertThat(entities).hasSize(1) assertThat(entities[0].id).isEqualTo(1) assertThat(entities[0].serviceId).isEqualTo(0) @@ -51,50 +51,50 @@ class HistoryRecordManagerTest { @Test fun deleteSearchHistory() { val entries = listOf( - SearchHistoryEntry(time.minusSeconds(1), 0, "A"), - SearchHistoryEntry(time.minusSeconds(2), 2, "A"), - SearchHistoryEntry(time.minusSeconds(3), 1, "B"), - SearchHistoryEntry(time.minusSeconds(4), 0, "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 0, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 1, search = "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 0, search = "B"), ) // make sure all 4 were inserted database.searchHistoryDAO().insertAll(entries) - assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries) + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries) // try to delete only "A" entries, "B" entries should be untouched manager.deleteSearchHistory("A").test().await().assertValue(2) - val entities = database.searchHistoryDAO().all.blockingFirst() + val entities = database.searchHistoryDAO().getAll().blockingFirst() assertThat(entities).hasSize(2) assertThat(entities).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 } .containsExactly(*entries.subList(2, 4).toTypedArray()) // assert that nothing happens if we delete a search query that does exist in the db manager.deleteSearchHistory("A").test().await().assertValue(0) - val entities2 = database.searchHistoryDAO().all.blockingFirst() + val entities2 = database.searchHistoryDAO().getAll().blockingFirst() assertThat(entities2).hasSize(2) assertThat(entities2).usingElementComparator { o1, o2 -> if (o1.hasEqualValues(o2)) 0 else 1 } .containsExactly(*entries.subList(2, 4).toTypedArray()) // delete all remaining entries manager.deleteSearchHistory("B").test().await().assertValue(2) - assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty() + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty() } @Test fun deleteCompleteSearchHistory() { val entries = listOf( - SearchHistoryEntry(time.minusSeconds(1), 1, "A"), - SearchHistoryEntry(time.minusSeconds(2), 2, "B"), - SearchHistoryEntry(time.minusSeconds(3), 0, "C"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 2, search = "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 0, search = "C"), ) // make sure all 3 were inserted database.searchHistoryDAO().insertAll(entries) - assertThat(database.searchHistoryDAO().all.blockingFirst()).hasSameSizeAs(entries) + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).hasSameSizeAs(entries) // should remove everything manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size) - assertThat(database.searchHistoryDAO().all.blockingFirst()).isEmpty() + assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty() } private fun insertShuffledRelatedSearches(relatedSearches: Collection) { @@ -107,7 +107,7 @@ class HistoryRecordManagerTest { // make sure all entries were inserted assertEquals( relatedSearches.size, - database.searchHistoryDAO().all.blockingFirst().size + database.searchHistoryDAO().getAll().blockingFirst().size ) } @@ -127,19 +127,18 @@ class HistoryRecordManagerTest { @Test fun getRelatedSearches_emptyQuery_manyDuplicates() { - insertShuffledRelatedSearches( - listOf( - SearchHistoryEntry(time.minusSeconds(9), 3, "A"), - SearchHistoryEntry(time.minusSeconds(8), 3, "AB"), - SearchHistoryEntry(time.minusSeconds(7), 3, "A"), - SearchHistoryEntry(time.minusSeconds(6), 3, "A"), - SearchHistoryEntry(time.minusSeconds(5), 3, "BA"), - SearchHistoryEntry(time.minusSeconds(4), 3, "A"), - SearchHistoryEntry(time.minusSeconds(3), 3, "A"), - SearchHistoryEntry(time.minusSeconds(2), 0, "A"), - SearchHistoryEntry(time.minusSeconds(1), 2, "AA"), - ) + val relatedSearches = listOf( + SearchHistoryEntry(creationDate = time.minusSeconds(9), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(8), serviceId = 3, search = "AB"), + SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 3, search = "BA"), + SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 2, search = "AA"), ) + insertShuffledRelatedSearches(relatedSearches) val searches = manager.getRelatedSearches("", 9, 3).blockingFirst() assertThat(searches).containsExactly("AA", "A", "BA") @@ -166,13 +165,13 @@ class HistoryRecordManagerTest { private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC) private val RELATED_SEARCHES_ENTRIES = listOf( - SearchHistoryEntry(time.minusSeconds(7), 2, "AC"), - SearchHistoryEntry(time.minusSeconds(6), 0, "ABC"), - SearchHistoryEntry(time.minusSeconds(5), 1, "BA"), - SearchHistoryEntry(time.minusSeconds(4), 3, "A"), - SearchHistoryEntry(time.minusSeconds(2), 0, "B"), - SearchHistoryEntry(time.minusSeconds(3), 2, "AA"), - SearchHistoryEntry(time.minusSeconds(1), 1, "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(7), serviceId = 2, search = "AC"), + SearchHistoryEntry(creationDate = time.minusSeconds(6), serviceId = 0, search = "ABC"), + SearchHistoryEntry(creationDate = time.minusSeconds(5), serviceId = 1, search = "BA"), + SearchHistoryEntry(creationDate = time.minusSeconds(4), serviceId = 3, search = "A"), + SearchHistoryEntry(creationDate = time.minusSeconds(2), serviceId = 0, search = "B"), + SearchHistoryEntry(creationDate = time.minusSeconds(3), serviceId = 2, search = "AA"), + SearchHistoryEntry(creationDate = time.minusSeconds(1), serviceId = 1, search = "A"), ) } } diff --git a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt index c392d8d3d..ce3aeb84a 100644 --- a/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt +++ b/app/src/androidTest/java/org/schabi/newpipe/local/playlist/LocalPlaylistManagerTest.kt @@ -72,6 +72,6 @@ class LocalPlaylistManagerTest { val result = manager.createPlaylist("name", listOf(stream, upserted)) result.test().await().assertComplete() - database.streamDAO().all.test().awaitCount(1).assertValue(listOf(stream, upserted)) + database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted)) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e0abd977b..20e9a6ca9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,8 @@ + + @@ -94,9 +96,22 @@ android:exported="false" android:label="@string/title_activity_about" /> - - - + + + + + + + - + + @@ -368,6 +385,7 @@ + @@ -421,6 +439,7 @@ diff --git a/app/src/main/java/org/schabi/newpipe/App.java b/app/src/main/java/org/schabi/newpipe/App.java index a8827c33e..cf41aad46 100644 --- a/app/src/main/java/org/schabi/newpipe/App.java +++ b/app/src/main/java/org/schabi/newpipe/App.java @@ -65,6 +65,8 @@ public class App extends Application { private static final String TAG = App.class.toString(); private boolean isFirstRun = false; + private boolean notificationsRequested = false; + private static App app; @NonNull @@ -72,6 +74,14 @@ public class App extends Application { return app; } + public boolean getNotificationsRequested() { + return notificationsRequested; + } + + public void setNotificationsRequested() { + notificationsRequested = true; + } + @Override protected void attachBaseContext(final Context base) { super.attachBaseContext(base); diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java index 4fbd562b4..8dac39682 100644 --- a/app/src/main/java/org/schabi/newpipe/MainActivity.java +++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java @@ -48,6 +48,7 @@ import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; @@ -896,7 +897,8 @@ public class MainActivity extends AppCompatActivity { }; final IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(VideoDetailFragment.ACTION_PLAYER_STARTED); - registerReceiver(broadcastReceiver, intentFilter); + ContextCompat.registerReceiver(this, broadcastReceiver, intentFilter, + ContextCompat.RECEIVER_EXPORTED); // If the PlayerHolder is not bound yet, but the service is running, try to bind to it. // Once the connection is established, the ACTION_PLAYER_STARTED will be sent. diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java deleted file mode 100644 index 21c5354f4..000000000 --- a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.schabi.newpipe; - -import static org.schabi.newpipe.database.AppDatabase.DATABASE_NAME; -import static org.schabi.newpipe.database.Migrations.MIGRATION_1_2; -import static org.schabi.newpipe.database.Migrations.MIGRATION_2_3; -import static org.schabi.newpipe.database.Migrations.MIGRATION_3_4; -import static org.schabi.newpipe.database.Migrations.MIGRATION_4_5; -import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; -import static org.schabi.newpipe.database.Migrations.MIGRATION_6_7; -import static org.schabi.newpipe.database.Migrations.MIGRATION_7_8; -import static org.schabi.newpipe.database.Migrations.MIGRATION_8_9; - -import android.content.Context; -import android.database.Cursor; - -import androidx.annotation.NonNull; -import androidx.room.Room; - -import org.schabi.newpipe.database.AppDatabase; - -public final class NewPipeDatabase { - private static volatile AppDatabase databaseInstance; - - private NewPipeDatabase() { - //no instance - } - - private static AppDatabase getDatabase(final Context context) { - return Room - .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) - .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, - MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9) - .build(); - } - - @NonNull - public static AppDatabase getInstance(@NonNull final Context context) { - AppDatabase result = databaseInstance; - if (result == null) { - synchronized (NewPipeDatabase.class) { - result = databaseInstance; - if (result == null) { - databaseInstance = getDatabase(context); - result = databaseInstance; - } - } - } - - return result; - } - - public static void checkpoint() { - if (databaseInstance == null) { - throw new IllegalStateException("database is not initialized"); - } - final Cursor c = databaseInstance.query("pragma wal_checkpoint(full)", null); - if (c.moveToFirst() && c.getInt(0) == 1) { - throw new RuntimeException("Checkpoint was blocked from completing"); - } - } - - public static void close() { - if (databaseInstance != null) { - synchronized (NewPipeDatabase.class) { - if (databaseInstance != null) { - databaseInstance.close(); - databaseInstance = null; - } - } - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt new file mode 100644 index 000000000..c3ce51524 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe + +import android.content.Context +import androidx.room.Room.databaseBuilder +import org.schabi.newpipe.database.AppDatabase +import org.schabi.newpipe.database.Migrations.MIGRATION_1_2 +import org.schabi.newpipe.database.Migrations.MIGRATION_2_3 +import org.schabi.newpipe.database.Migrations.MIGRATION_3_4 +import org.schabi.newpipe.database.Migrations.MIGRATION_4_5 +import org.schabi.newpipe.database.Migrations.MIGRATION_5_6 +import org.schabi.newpipe.database.Migrations.MIGRATION_6_7 +import org.schabi.newpipe.database.Migrations.MIGRATION_7_8 +import org.schabi.newpipe.database.Migrations.MIGRATION_8_9 +import kotlin.concurrent.Volatile + +object NewPipeDatabase { + + @Volatile + private var databaseInstance: AppDatabase? = null + + private fun getDatabase(context: Context): AppDatabase { + return databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + AppDatabase.Companion.DATABASE_NAME + ).addMigrations( + MIGRATION_1_2, + MIGRATION_2_3, + MIGRATION_3_4, + MIGRATION_4_5, + MIGRATION_5_6, + MIGRATION_6_7, + MIGRATION_7_8, + MIGRATION_8_9 + ).build() + } + + @JvmStatic + fun getInstance(context: Context): AppDatabase { + var result = databaseInstance + if (result == null) { + synchronized(NewPipeDatabase::class.java) { + result = databaseInstance + if (result == null) { + databaseInstance = getDatabase(context) + result = databaseInstance + } + } + } + + return result!! + } + + @JvmStatic + fun checkpoint() { + checkNotNull(databaseInstance) { "database is not initialized" } + val c = databaseInstance!!.query("pragma wal_checkpoint(full)", null) + if (c.moveToFirst() && c.getInt(0) == 1) { + throw RuntimeException("Checkpoint was blocked from completing") + } + } + + @JvmStatic + fun close() { + if (databaseInstance != null) { + synchronized(NewPipeDatabase::class.java) { + if (databaseInstance != null) { + databaseInstance!!.close() + databaseInstance = null + } + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java index 3294cae0b..d85fdf7de 100644 --- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java +++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java @@ -58,20 +58,10 @@ import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.StreamingService.LinkType; import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException; -import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; -import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; -import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException; -import org.schabi.newpipe.extractor.exceptions.PaidContentException; -import org.schabi.newpipe.extractor.exceptions.PrivateContentException; -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; -import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; -import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler; import org.schabi.newpipe.extractor.playlist.PlaylistInfo; import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.helper.PlayerHelper; @@ -260,7 +250,8 @@ public class RouterActivity extends AppCompatActivity { showUnsupportedUrlDialog(url); } }, throwable -> handleError(this, new ErrorInfo(throwable, - UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url)))); + UserAction.SHARE_TO_NEWPIPE, "Getting service from url: " + url, + null, url)))); } /** @@ -269,40 +260,19 @@ public class RouterActivity extends AppCompatActivity { * @param errorInfo the error information */ private static void handleError(final Context context, final ErrorInfo errorInfo) { - if (errorInfo.getThrowable() != null) { - errorInfo.getThrowable().printStackTrace(); - } - - if (errorInfo.getThrowable() instanceof ReCaptchaException) { + if (errorInfo.getRecaptchaUrl() != null) { Toast.makeText(context, R.string.recaptcha_request_toast, Toast.LENGTH_LONG).show(); // Starting ReCaptcha Challenge Activity final Intent intent = new Intent(context, ReCaptchaActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.getRecaptchaUrl()); context.startActivity(intent); - } else if (errorInfo.getThrowable() != null - && ExceptionUtils.isNetworkRelated(errorInfo.getThrowable())) { - Toast.makeText(context, R.string.network_error, Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof AgeRestrictedContentException) { - Toast.makeText(context, R.string.restricted_video_no_stream, - Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof GeographicRestrictionException) { - Toast.makeText(context, R.string.georestricted_content, Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof PaidContentException) { - Toast.makeText(context, R.string.paid_content, Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof PrivateContentException) { - Toast.makeText(context, R.string.private_content, Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof SoundCloudGoPlusContentException) { - Toast.makeText(context, R.string.soundcloud_go_plus_content, - Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof YoutubeMusicPremiumContentException) { - Toast.makeText(context, R.string.youtube_music_premium_content, - Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof ContentNotAvailableException) { - Toast.makeText(context, R.string.content_not_available, Toast.LENGTH_LONG).show(); - } else if (errorInfo.getThrowable() instanceof ContentNotSupportedException) { - Toast.makeText(context, R.string.content_not_supported, Toast.LENGTH_LONG).show(); - } else { + } else if (errorInfo.isReportable()) { ErrorUtil.createNotification(context, errorInfo); + } else { + // this exception does not usually indicate a problem that should be reported, + // so just show a toast instead of the notification + Toast.makeText(context, errorInfo.getMessage(context), Toast.LENGTH_LONG).show(); } if (context instanceof RouterActivity) { @@ -346,7 +316,8 @@ public class RouterActivity extends AppCompatActivity { if (choiceChecker.isAvailableAndSelected( R.string.video_player_key, R.string.background_player_key, - R.string.popup_player_key)) { + R.string.popup_player_key, + R.string.enqueue_key)) { final String selectedChoice = choiceChecker.getSelectedChoiceKey(); @@ -359,6 +330,8 @@ public class RouterActivity extends AppCompatActivity { || selectedChoice.equals(getString(R.string.popup_player_key)); final boolean isAudioPlayerSelected = selectedChoice.equals(getString(R.string.background_player_key)); + final boolean isEnqueueSelected = + selectedChoice.equals(getString(R.string.enqueue_key)); if (currentLinkType != LinkType.STREAM && ((isExtAudioEnabled && isAudioPlayerSelected) @@ -375,7 +348,9 @@ public class RouterActivity extends AppCompatActivity { // Check if the service supports the choice if ((isVideoPlayerSelected && capabilities.contains(VIDEO)) - || (isAudioPlayerSelected && capabilities.contains(AUDIO))) { + || (isAudioPlayerSelected && capabilities.contains(AUDIO)) + || (isEnqueueSelected && (capabilities.contains(VIDEO) + || capabilities.contains(AUDIO)))) { handleChoice(selectedChoice); } else { handleChoice(getString(R.string.show_info_key)); @@ -556,7 +531,7 @@ public class RouterActivity extends AppCompatActivity { final List capabilities = service.getServiceInfo().getMediaCapabilities(); - if (linkType == LinkType.STREAM) { + if (linkType == LinkType.STREAM || linkType == LinkType.PLAYLIST) { if (capabilities.contains(VIDEO)) { returnedItems.add(videoPlayer); returnedItems.add(popupPlayer); @@ -564,17 +539,28 @@ public class RouterActivity extends AppCompatActivity { if (capabilities.contains(AUDIO)) { returnedItems.add(backgroundPlayer); } - // download is redundant for linkType CHANNEL AND PLAYLIST (till playlist downloading is - // not supported ) - returnedItems.add(new AdapterChoiceItem(getString(R.string.download_key), - getString(R.string.download), - R.drawable.ic_file_download)); - // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can - // not be added to a playlist - returnedItems.add(new AdapterChoiceItem(getString(R.string.add_to_playlist_key), - getString(R.string.add_to_playlist), - R.drawable.ic_add)); + // Enqueue is only shown if the current queue is not empty. + // However, if the playqueue or the player is cleared after this item was chosen and + // while the item is extracted, it will automatically fall back to background player. + if (PlayerHolder.getInstance().getQueueSize() > 0) { + returnedItems.add(new AdapterChoiceItem(getString(R.string.enqueue_key), + getString(R.string.enqueue_stream), R.drawable.ic_add)); + } + + if (linkType == LinkType.STREAM) { + // download is redundant for linkType CHANNEL AND PLAYLIST + // (till playlist downloading is not supported ) + returnedItems.add(new AdapterChoiceItem(getString(R.string.download_key), + getString(R.string.download), + R.drawable.ic_file_download)); + + // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType + // since those can not be added to a playlist + returnedItems.add(new AdapterChoiceItem(getString(R.string.add_to_playlist_key), + getString(R.string.add_to_playlist), + R.drawable.ic_playlist_add)); + } } else { // LinkType.NONE is never present because it's filtered out before // channels and playlist can be played as they contain a list of videos @@ -665,7 +651,8 @@ public class RouterActivity extends AppCompatActivity { startActivity(intent); finish(); }, throwable -> handleError(this, new ErrorInfo(throwable, - UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl))) + UserAction.SHARE_TO_NEWPIPE, "Starting info activity: " + currentUrl, + null, currentUrl))) ); return; } @@ -852,10 +839,10 @@ public class RouterActivity extends AppCompatActivity { }) )), throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo( - throwable, - UserAction.REQUESTED_STREAM, + throwable, UserAction.REQUESTED_STREAM, "Tried to add " + currentUrl + " to a playlist", - ((RouterActivity) ctx).currentService.getServiceId()) + ((RouterActivity) ctx).currentService.getServiceId(), + currentUrl) )) ) ); @@ -995,7 +982,7 @@ public class RouterActivity extends AppCompatActivity { } }, throwable -> handleError(this, new ErrorInfo(throwable, finalUserAction, choice.url + " opened with " + choice.playerChoice, - choice.serviceId))); + choice.serviceId, choice.url))); } } @@ -1045,6 +1032,8 @@ public class RouterActivity extends AppCompatActivity { NavigationHelper.playOnBackgroundPlayer(this, playQueue, true); } else if (choice.playerChoice.equals(popupPlayerKey)) { NavigationHelper.playOnPopupPlayer(this, playQueue, true); + } else if (choice.playerChoice.equals(getString(R.string.enqueue_key))) { + NavigationHelper.enqueueOnPlayer(this, playQueue); } }; } diff --git a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt index 189fa148b..240e2f42b 100644 --- a/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.ViewGroup import android.webkit.WebView import androidx.appcompat.app.AlertDialog +import androidx.core.os.BundleCompat import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers @@ -33,7 +34,9 @@ class LicenseFragment : Fragment() { super.onCreate(savedInstanceState) softwareComponents = arguments?.parcelableArrayList(ARG_COMPONENTS)!! .sortedBy { it.name } // Sort components by name - activeSoftwareComponent = savedInstanceState?.getSerializable(SOFTWARE_COMPONENT_KEY) as? SoftwareComponent + activeSoftwareComponent = savedInstanceState?.let { + BundleCompat.getSerializable(it, SOFTWARE_COMPONENT_KEY, SoftwareComponent::class.java) + } } override fun onDestroy() { diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java deleted file mode 100644 index 04d93a238..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.schabi.newpipe.database; - -import static org.schabi.newpipe.database.Migrations.DB_VER_9; - -import androidx.room.Database; -import androidx.room.RoomDatabase; -import androidx.room.TypeConverters; - -import org.schabi.newpipe.database.feed.dao.FeedDAO; -import org.schabi.newpipe.database.feed.dao.FeedGroupDAO; -import org.schabi.newpipe.database.feed.model.FeedEntity; -import org.schabi.newpipe.database.feed.model.FeedGroupEntity; -import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity; -import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity; -import org.schabi.newpipe.database.history.dao.SearchHistoryDAO; -import org.schabi.newpipe.database.history.dao.StreamHistoryDAO; -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; -import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO; -import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; -import org.schabi.newpipe.database.stream.dao.StreamDAO; -import org.schabi.newpipe.database.stream.dao.StreamStateDAO; -import org.schabi.newpipe.database.stream.model.StreamEntity; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; -import org.schabi.newpipe.database.subscription.SubscriptionDAO; -import org.schabi.newpipe.database.subscription.SubscriptionEntity; - -@TypeConverters({Converters.class}) -@Database( - entities = { - SubscriptionEntity.class, SearchHistoryEntry.class, - StreamEntity.class, StreamHistoryEntity.class, StreamStateEntity.class, - PlaylistEntity.class, PlaylistStreamEntity.class, PlaylistRemoteEntity.class, - FeedEntity.class, FeedGroupEntity.class, FeedGroupSubscriptionEntity.class, - FeedLastUpdatedEntity.class - }, - version = DB_VER_9 -) -public abstract class AppDatabase extends RoomDatabase { - public static final String DATABASE_NAME = "newpipe.db"; - - public abstract SearchHistoryDAO searchHistoryDAO(); - - public abstract StreamDAO streamDAO(); - - public abstract StreamHistoryDAO streamHistoryDAO(); - - public abstract StreamStateDAO streamStateDAO(); - - public abstract PlaylistDAO playlistDAO(); - - public abstract PlaylistStreamDAO playlistStreamDAO(); - - public abstract PlaylistRemoteDAO playlistRemoteDAO(); - - public abstract FeedDAO feedDAO(); - - public abstract FeedGroupDAO feedGroupDAO(); - - public abstract SubscriptionDAO subscriptionDAO(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt new file mode 100644 index 000000000..286eddf7b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import org.schabi.newpipe.database.feed.dao.FeedDAO +import org.schabi.newpipe.database.feed.dao.FeedGroupDAO +import org.schabi.newpipe.database.feed.model.FeedEntity +import org.schabi.newpipe.database.feed.model.FeedGroupEntity +import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity +import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity +import org.schabi.newpipe.database.history.dao.SearchHistoryDAO +import org.schabi.newpipe.database.history.dao.StreamHistoryDAO +import org.schabi.newpipe.database.history.model.SearchHistoryEntry +import org.schabi.newpipe.database.history.model.StreamHistoryEntity +import org.schabi.newpipe.database.playlist.dao.PlaylistDAO +import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO +import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO +import org.schabi.newpipe.database.playlist.model.PlaylistEntity +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity +import org.schabi.newpipe.database.stream.dao.StreamDAO +import org.schabi.newpipe.database.stream.dao.StreamStateDAO +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamStateEntity +import org.schabi.newpipe.database.subscription.SubscriptionDAO +import org.schabi.newpipe.database.subscription.SubscriptionEntity + +@TypeConverters(Converters::class) +@Database( + version = Migrations.DB_VER_9, + entities = [ + SubscriptionEntity::class, + SearchHistoryEntry::class, + StreamEntity::class, + StreamHistoryEntity::class, + StreamStateEntity::class, + PlaylistEntity::class, + PlaylistStreamEntity::class, + PlaylistRemoteEntity::class, + FeedEntity::class, + FeedGroupEntity::class, + FeedGroupSubscriptionEntity::class, + FeedLastUpdatedEntity::class + ] +) +abstract class AppDatabase : RoomDatabase() { + abstract fun feedDAO(): FeedDAO + abstract fun feedGroupDAO(): FeedGroupDAO + abstract fun playlistDAO(): PlaylistDAO + abstract fun playlistRemoteDAO(): PlaylistRemoteDAO + abstract fun playlistStreamDAO(): PlaylistStreamDAO + abstract fun searchHistoryDAO(): SearchHistoryDAO + abstract fun streamDAO(): StreamDAO + abstract fun streamHistoryDAO(): StreamHistoryDAO + abstract fun streamStateDAO(): StreamStateDAO + abstract fun subscriptionDAO(): SubscriptionDAO + + companion object { + const val DATABASE_NAME: String = "newpipe.db" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java deleted file mode 100644 index 255f5ba8d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.java +++ /dev/null @@ -1,39 +0,0 @@ -package org.schabi.newpipe.database; - -import androidx.room.Dao; -import androidx.room.Delete; -import androidx.room.Insert; -import androidx.room.Update; - -import java.util.Collection; -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -@Dao -public interface BasicDAO { - /* Inserts */ - @Insert - long insert(Entity entity); - - @Insert - List insertAll(Collection entities); - - /* Searches */ - Flowable> getAll(); - - Flowable> listByService(int serviceId); - - /* Deletes */ - @Delete - void delete(Entity entity); - - int deleteAll(); - - /* Updates */ - @Update - int update(Entity entity); - - @Update - void update(Collection entities); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt new file mode 100644 index 000000000..74c7cc87c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2017-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Update +import io.reactivex.rxjava3.core.Flowable + +@Dao +interface BasicDAO { + + /* Inserts */ + @Insert + fun insert(entity: Entity): Long + + @Insert + fun insertAll(entities: Collection): List + + /* Searches */ + fun getAll(): Flowable> + + fun listByService(serviceId: Int): Flowable> + + /* Deletes */ + @Delete + fun delete(entity: Entity) + + fun deleteAll(): Int + + /* Updates */ + @Update + fun update(entity: Entity): Int + + @Update + fun update(entities: Collection) +} diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java b/app/src/main/java/org/schabi/newpipe/database/LocalItem.java deleted file mode 100644 index 54b856b06..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/LocalItem.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.schabi.newpipe.database; - -public interface LocalItem { - LocalItemType getLocalItemType(); - - enum LocalItemType { - PLAYLIST_LOCAL_ITEM, - PLAYLIST_REMOTE_ITEM, - - PLAYLIST_STREAM_ITEM, - STATISTIC_STREAM_ITEM, - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt new file mode 100644 index 000000000..50529610b --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/LocalItem.kt @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2018-2020 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +interface LocalItem { + val localItemType: LocalItemType + + enum class LocalItemType { + PLAYLIST_LOCAL_ITEM, + PLAYLIST_REMOTE_ITEM, + + PLAYLIST_STREAM_ITEM, + STATISTIC_STREAM_ITEM, + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.java b/app/src/main/java/org/schabi/newpipe/database/Migrations.java deleted file mode 100644 index c9f630869..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/Migrations.java +++ /dev/null @@ -1,307 +0,0 @@ -package org.schabi.newpipe.database; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.room.migration.Migration; -import androidx.sqlite.db.SupportSQLiteDatabase; - -import org.schabi.newpipe.MainActivity; - -public final class Migrations { - - ///////////////////////////////////////////////////////////////////////////// - // Test new migrations manually by importing a database from daily usage // - // and checking if the migration works (Use the Database Inspector // - // https://developer.android.com/studio/inspect/database). // - // If you add a migration point it out in the pull request, so that // - // others remember to test it themselves. // - ///////////////////////////////////////////////////////////////////////////// - - public static final int DB_VER_1 = 1; - public static final int DB_VER_2 = 2; - public static final int DB_VER_3 = 3; - public static final int DB_VER_4 = 4; - public static final int DB_VER_5 = 5; - public static final int DB_VER_6 = 6; - public static final int DB_VER_7 = 7; - public static final int DB_VER_8 = 8; - public static final int DB_VER_9 = 9; - - private static final String TAG = Migrations.class.getName(); - public static final boolean DEBUG = MainActivity.DEBUG; - - public static final Migration MIGRATION_1_2 = new Migration(DB_VER_1, DB_VER_2) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - if (DEBUG) { - Log.d(TAG, "Start migrating database"); - } - /* - * Unfortunately these queries must be hardcoded due to the possibility of - * schema and names changing at a later date, thus invalidating the older migration - * scripts if they are not hardcoded. - * */ - - // Not much we can do about this, since room doesn't create tables before migration. - // It's either this or blasting the entire database anew. - database.execSQL("CREATE INDEX `index_search_history_search` " - + "ON `search_history` (`search`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `streams` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " - + "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " - + "`thumbnail_url` TEXT)"); - database.execSQL("CREATE UNIQUE INDEX `index_streams_service_id_url` " - + "ON `streams` (`service_id`, `url`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_history` " - + "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, " - + "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), " - + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " - + "ON UPDATE CASCADE ON DELETE CASCADE )"); - database.execSQL("CREATE INDEX `index_stream_history_stream_id` " - + "ON `stream_history` (`stream_id`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `stream_state` " - + "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, " - + "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) " - + "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlists` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`name` TEXT, `thumbnail_url` TEXT)"); - database.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `playlist_stream_join` " - + "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, " - + "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), " - + "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " - + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); - database.execSQL("CREATE UNIQUE INDEX " - + "`index_playlist_stream_join_playlist_id_join_index` " - + "ON `playlist_stream_join` (`playlist_id`, `join_index`)"); - database.execSQL("CREATE INDEX `index_playlist_stream_join_stream_id` " - + "ON `playlist_stream_join` (`stream_id`)"); - database.execSQL("CREATE TABLE IF NOT EXISTS `remote_playlists` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " - + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)"); - database.execSQL("CREATE INDEX `index_remote_playlists_name` " - + "ON `remote_playlists` (`name`)"); - database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " - + "ON `remote_playlists` (`service_id`, `url`)"); - - // Populate streams table with existing entries in watch history - // Latest data first, thus ignoring older entries with the same indices - database.execSQL("INSERT OR IGNORE INTO streams (service_id, url, title, " - + "stream_type, duration, uploader, thumbnail_url) " - - + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " - + "uploader, thumbnail_url " - - + "FROM watch_history " - + "ORDER BY creation_date DESC"); - - // Once the streams have PKs, join them with the normalized history table - // and populate it with the remaining data from watch history - database.execSQL("INSERT INTO stream_history (stream_id, access_date, repeat_count)" - + "SELECT uid, creation_date, 1 " - + "FROM watch_history INNER JOIN streams " - + "ON watch_history.service_id == streams.service_id " - + "AND watch_history.url == streams.url " - + "ORDER BY creation_date DESC"); - - database.execSQL("DROP TABLE IF EXISTS watch_history"); - - if (DEBUG) { - Log.d(TAG, "Stop migrating database"); - } - } - }; - - public static final Migration MIGRATION_2_3 = new Migration(DB_VER_2, DB_VER_3) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - // Add NOT NULLs and new fields - database.execSQL("CREATE TABLE IF NOT EXISTS streams_new " - + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, " - + "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, " - + "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, " - + "textual_upload_date TEXT, upload_date INTEGER, " - + "is_upload_date_approximation INTEGER)"); - - database.execSQL("INSERT INTO streams_new (uid, service_id, url, title, stream_type, " - + "duration, uploader, thumbnail_url, view_count, textual_upload_date, " - + "upload_date, is_upload_date_approximation) " - - + "SELECT uid, service_id, url, ifnull(title, ''), " - + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " - + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " - - + "FROM streams WHERE url IS NOT NULL"); - - database.execSQL("DROP TABLE streams"); - database.execSQL("ALTER TABLE streams_new RENAME TO streams"); - database.execSQL("CREATE UNIQUE INDEX index_streams_service_id_url " - + "ON streams (service_id, url)"); - - // Tables for feed feature - database.execSQL("CREATE TABLE IF NOT EXISTS feed " - + "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " - + "PRIMARY KEY(stream_id, subscription_id), " - + "FOREIGN KEY(stream_id) REFERENCES streams(uid) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " - + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); - database.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)"); - database.execSQL("CREATE TABLE IF NOT EXISTS feed_group " - + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " - + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)"); - database.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)"); - database.execSQL("CREATE TABLE IF NOT EXISTS feed_group_subscription_join " - + "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " - + "PRIMARY KEY(group_id, subscription_id), " - + "FOREIGN KEY(group_id) REFERENCES feed_group(uid) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " - + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); - database.execSQL("CREATE INDEX index_feed_group_subscription_join_subscription_id " - + "ON feed_group_subscription_join (subscription_id)"); - database.execSQL("CREATE TABLE IF NOT EXISTS feed_last_updated " - + "(subscription_id INTEGER NOT NULL, last_updated INTEGER, " - + "PRIMARY KEY(subscription_id), " - + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " - + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)"); - } - }; - - public static final Migration MIGRATION_3_4 = new Migration(DB_VER_3, DB_VER_4) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL( - "ALTER TABLE streams ADD COLUMN uploader_url TEXT" - ); - } - }; - - public static final Migration MIGRATION_4_5 = new Migration(DB_VER_4, DB_VER_5) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " - + "INTEGER NOT NULL DEFAULT 0"); - } - }; - - public static final Migration MIGRATION_5_6 = new Migration(DB_VER_5, DB_VER_6) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " - + "INTEGER NOT NULL DEFAULT 0"); - } - }; - - public static final Migration MIGRATION_6_7 = new Migration(DB_VER_6, DB_VER_7) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - // Create a new column thumbnail_stream_id - database.execSQL("ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " - + "INTEGER NOT NULL DEFAULT -1"); - - // Migrate the thumbnail_url to the thumbnail_stream_id - database.execSQL("UPDATE playlists SET thumbnail_stream_id = (" - + " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" - + " FROM (" - + " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" - + " FROM playlists p" - + " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" - + " LEFT JOIN streams s ON s.uid = ps.stream_id" - + " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" - + " WHERE playlist_uid = playlists.uid)"); - - // Remove the thumbnail_url field in the playlist table - database.execSQL("CREATE TABLE IF NOT EXISTS `playlists_new`" - + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "name TEXT, " - + "is_thumbnail_permanent INTEGER NOT NULL, " - + "thumbnail_stream_id INTEGER NOT NULL)"); - - database.execSQL("INSERT INTO playlists_new" - + " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " - + " FROM playlists"); - - - database.execSQL("DROP TABLE playlists"); - database.execSQL("ALTER TABLE playlists_new RENAME TO playlists"); - database.execSQL("CREATE INDEX IF NOT EXISTS " - + "`index_playlists_name` ON `playlists` (`name`)"); - } - }; - - public static final Migration MIGRATION_7_8 = new Migration(DB_VER_7, DB_VER_8) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - database.execSQL("DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " - + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)"); - database.execSQL("UPDATE search_history SET search = trim(search)"); - } - }; - - public static final Migration MIGRATION_8_9 = new Migration(DB_VER_8, DB_VER_9) { - @Override - public void migrate(@NonNull final SupportSQLiteDatabase database) { - try { - database.beginTransaction(); - - // Update playlists. - // Create a temp table to initialize display_index. - database.execSQL("CREATE TABLE `playlists_tmp` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " - + "`thumbnail_stream_id` INTEGER NOT NULL, " - + "`display_index` INTEGER NOT NULL)"); - database.execSQL("INSERT INTO `playlists_tmp` " - + "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " - + "`display_index`) " - + "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " - + "-1 " - + "FROM `playlists`"); - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - database.execSQL("DROP TABLE `playlists`"); - database.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`"); - - - // Update remote_playlists. - // Create a temp table to initialize display_index. - database.execSQL("CREATE TABLE `remote_playlists_tmp` " - + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " - + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " - + "`thumbnail_url` TEXT, `uploader` TEXT, " - + "`display_index` INTEGER NOT NULL," - + "`stream_count` INTEGER)"); - database.execSQL("INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " - + "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " - + "`stream_count`)" - + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " - + "-1, `stream_count` FROM `remote_playlists`"); - - // Replace the old table, note that this also removes the index on the name which - // we don't need anymore. - database.execSQL("DROP TABLE `remote_playlists`"); - database.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`"); - - // Create index on the new table. - database.execSQL("CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " - + "ON `remote_playlists` (`service_id`, `url`)"); - - database.setTransactionSuccessful(); - } finally { - database.endTransaction(); - } - } - }; - - private Migrations() { - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/Migrations.kt b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt new file mode 100644 index 000000000..8988708e6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/Migrations.kt @@ -0,0 +1,368 @@ +/* + * SPDX-FileCopyrightText: 2018-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database + +import android.util.Log +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import org.schabi.newpipe.MainActivity + +object Migrations { + + // /////////////////////////////////////////////////////////////////////// // + // Test new migrations manually by importing a database from daily usage // + // and checking if the migration works (Use the Database Inspector // + // https://developer.android.com/studio/inspect/database). // + // If you add a migration point it out in the pull request, so that // + // others remember to test it themselves. // + // /////////////////////////////////////////////////////////////////////// // + + const val DB_VER_1 = 1 + const val DB_VER_2 = 2 + const val DB_VER_3 = 3 + const val DB_VER_4 = 4 + const val DB_VER_5 = 5 + const val DB_VER_6 = 6 + const val DB_VER_7 = 7 + const val DB_VER_8 = 8 + const val DB_VER_9 = 9 + + private val TAG = Migrations::class.java.getName() + private val isDebug = MainActivity.DEBUG + + val MIGRATION_1_2 = object : Migration(DB_VER_1, DB_VER_2) { + override fun migrate(db: SupportSQLiteDatabase) { + if (isDebug) { + Log.d(TAG, "Start migrating database") + } + + /* + * Unfortunately these queries must be hardcoded due to the possibility of + * schema and names changing at a later date, thus invalidating the older migration + * scripts if they are not hardcoded. + * */ + + // Not much we can do about this, since room doesn't create tables before migration. + // It's either this or blasting the entire database anew. + db.execSQL( + "CREATE INDEX `index_search_history_search` " + + "ON `search_history` (`search`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `streams` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `url` TEXT, `title` TEXT, " + + "`stream_type` TEXT, `duration` INTEGER, `uploader` TEXT, " + + "`thumbnail_url` TEXT)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_streams_service_id_url` " + + "ON `streams` (`service_id`, `url`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_history` " + + "(`stream_id` INTEGER NOT NULL, `access_date` INTEGER NOT NULL, " + + "`repeat_count` INTEGER NOT NULL, PRIMARY KEY(`stream_id`, `access_date`), " + + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE )" + ) + db.execSQL( + "CREATE INDEX `index_stream_history_stream_id` " + + "ON `stream_history` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `stream_state` " + + "(`stream_id` INTEGER NOT NULL, `progress_time` INTEGER NOT NULL, " + + "PRIMARY KEY(`stream_id`), FOREIGN KEY(`stream_id`) " + + "REFERENCES `streams`(`uid`) ON UPDATE CASCADE ON DELETE CASCADE )" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `thumbnail_url` TEXT)" + ) + db.execSQL("CREATE INDEX `index_playlists_name` ON `playlists` (`name`)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlist_stream_join` " + + "(`playlist_id` INTEGER NOT NULL, `stream_id` INTEGER NOT NULL, " + + "`join_index` INTEGER NOT NULL, PRIMARY KEY(`playlist_id`, `join_index`), " + + "FOREIGN KEY(`playlist_id`) REFERENCES `playlists`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(`stream_id`) REFERENCES `streams`(`uid`) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + db.execSQL( + "CREATE UNIQUE INDEX " + + "`index_playlist_stream_join_playlist_id_join_index` " + + "ON `playlist_stream_join` (`playlist_id`, `join_index`)" + ) + db.execSQL( + "CREATE INDEX `index_playlist_stream_join_stream_id` " + + "ON `playlist_stream_join` (`stream_id`)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS `remote_playlists` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)" + ) + db.execSQL( + "CREATE INDEX `index_remote_playlists_name` " + + "ON `remote_playlists` (`name`)" + ) + db.execSQL( + "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)" + ) + + // Populate streams table with existing entries in watch history + // Latest data first, thus ignoring older entries with the same indices + db.execSQL( + "INSERT OR IGNORE INTO streams (service_id, url, title, " + + "stream_type, duration, uploader, thumbnail_url) " + + + "SELECT service_id, url, title, 'VIDEO_STREAM', duration, " + + "uploader, thumbnail_url " + + + "FROM watch_history " + + "ORDER BY creation_date DESC" + ) + + // Once the streams have PKs, join them with the normalized history table + // and populate it with the remaining data from watch history + db.execSQL( + "INSERT INTO stream_history (stream_id, access_date, repeat_count)" + + "SELECT uid, creation_date, 1 " + + "FROM watch_history INNER JOIN streams " + + "ON watch_history.service_id == streams.service_id " + + "AND watch_history.url == streams.url " + + "ORDER BY creation_date DESC" + ) + + db.execSQL("DROP TABLE IF EXISTS watch_history") + + if (isDebug) { + Log.d(TAG, "Stop migrating database") + } + } + } + + val MIGRATION_2_3 = object : Migration(DB_VER_2, DB_VER_3) { + override fun migrate(db: SupportSQLiteDatabase) { + // Add NOT NULLs and new fields + db.execSQL( + "CREATE TABLE IF NOT EXISTS streams_new " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "service_id INTEGER NOT NULL, url TEXT NOT NULL, title TEXT NOT NULL, " + + "stream_type TEXT NOT NULL, duration INTEGER NOT NULL, " + + "uploader TEXT NOT NULL, thumbnail_url TEXT, view_count INTEGER, " + + "textual_upload_date TEXT, upload_date INTEGER, " + + "is_upload_date_approximation INTEGER)" + ) + + db.execSQL( + "INSERT INTO streams_new (uid, service_id, url, title, stream_type, " + + "duration, uploader, thumbnail_url, view_count, textual_upload_date, " + + "upload_date, is_upload_date_approximation) " + + + "SELECT uid, service_id, url, ifnull(title, ''), " + + "ifnull(stream_type, 'VIDEO_STREAM'), ifnull(duration, 0), " + + "ifnull(uploader, ''), ifnull(thumbnail_url, ''), NULL, NULL, NULL, NULL " + + + "FROM streams WHERE url IS NOT NULL" + ) + + db.execSQL("DROP TABLE streams") + db.execSQL("ALTER TABLE streams_new RENAME TO streams") + db.execSQL( + "CREATE UNIQUE INDEX index_streams_service_id_url " + + "ON streams (service_id, url)" + ) + + // Tables for feed feature + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed " + + "(stream_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + + "PRIMARY KEY(stream_id, subscription_id), " + + "FOREIGN KEY(stream_id) REFERENCES streams(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + db.execSQL("CREATE INDEX index_feed_subscription_id ON feed (subscription_id)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group " + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, " + + "icon_id INTEGER NOT NULL, sort_order INTEGER NOT NULL)" + ) + db.execSQL("CREATE INDEX index_feed_group_sort_order ON feed_group (sort_order)") + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_group_subscription_join " + + "(group_id INTEGER NOT NULL, subscription_id INTEGER NOT NULL, " + + "PRIMARY KEY(group_id, subscription_id), " + + "FOREIGN KEY(group_id) REFERENCES feed_group(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + db.execSQL( + "CREATE INDEX index_feed_group_subscription_join_subscription_id " + + "ON feed_group_subscription_join (subscription_id)" + ) + db.execSQL( + "CREATE TABLE IF NOT EXISTS feed_last_updated " + + "(subscription_id INTEGER NOT NULL, last_updated INTEGER, " + + "PRIMARY KEY(subscription_id), " + + "FOREIGN KEY(subscription_id) REFERENCES subscriptions(uid) " + + "ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)" + ) + } + } + + val MIGRATION_3_4 = object : Migration(DB_VER_3, DB_VER_4) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT") + } + } + + val MIGRATION_4_5 = object : Migration(DB_VER_4, DB_VER_5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + } + + val MIGRATION_5_6 = object : Migration(DB_VER_5, DB_VER_6) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " + + "INTEGER NOT NULL DEFAULT 0" + ) + } + } + + val MIGRATION_6_7 = object : Migration(DB_VER_6, DB_VER_7) { + override fun migrate(db: SupportSQLiteDatabase) { + // Create a new column thumbnail_stream_id + db.execSQL( + "ALTER TABLE `playlists` ADD COLUMN `thumbnail_stream_id` " + + "INTEGER NOT NULL DEFAULT -1" + ) + + // Migrate the thumbnail_url to the thumbnail_stream_id + db.execSQL( + "UPDATE playlists SET thumbnail_stream_id = (" + + " SELECT CASE WHEN COUNT(*) != 0 then stream_uid ELSE -1 END" + + " FROM (" + + " SELECT p.uid AS playlist_uid, s.uid AS stream_uid" + + " FROM playlists p" + + " LEFT JOIN playlist_stream_join ps ON p.uid = ps.playlist_id" + + " LEFT JOIN streams s ON s.uid = ps.stream_id" + + " WHERE s.thumbnail_url = p.thumbnail_url) AS temporary_table" + + " WHERE playlist_uid = playlists.uid)" + ) + + // Remove the thumbnail_url field in the playlist table + db.execSQL( + "CREATE TABLE IF NOT EXISTS `playlists_new`" + + "(uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "name TEXT, " + + "is_thumbnail_permanent INTEGER NOT NULL, " + + "thumbnail_stream_id INTEGER NOT NULL)" + ) + + db.execSQL( + "INSERT INTO playlists_new" + + " SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id " + + " FROM playlists" + ) + + db.execSQL("DROP TABLE playlists") + db.execSQL("ALTER TABLE playlists_new RENAME TO playlists") + db.execSQL( + "CREATE INDEX IF NOT EXISTS " + + "`index_playlists_name` ON `playlists` (`name`)" + ) + } + } + + val MIGRATION_7_8 = object : Migration(DB_VER_7, DB_VER_8) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "DELETE FROM search_history WHERE id NOT IN (SELECT id FROM (SELECT " + + "MIN(id) as id FROM search_history GROUP BY trim(search), service_id ) tmp)" + ) + db.execSQL("UPDATE search_history SET search = trim(search)") + } + } + + val MIGRATION_8_9 = object : Migration(DB_VER_8, DB_VER_9) { + override fun migrate(db: SupportSQLiteDatabase) { + try { + db.beginTransaction() + + // Update playlists. + // Create a temp table to initialize display_index. + db.execSQL( + "CREATE TABLE `playlists_tmp` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, " + + "`thumbnail_stream_id` INTEGER NOT NULL, " + + "`display_index` INTEGER NOT NULL)" + ) + db.execSQL( + "INSERT INTO `playlists_tmp` " + + "(`uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "`display_index`) " + + "SELECT `uid`, `name`, `is_thumbnail_permanent`, `thumbnail_stream_id`, " + + "-1 " + + "FROM `playlists`" + ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `playlists`") + db.execSQL("ALTER TABLE `playlists_tmp` RENAME TO `playlists`") + + // Update remote_playlists. + // Create a temp table to initialize display_index. + db.execSQL( + "CREATE TABLE `remote_playlists_tmp` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, " + + "`thumbnail_url` TEXT, `uploader` TEXT, " + + "`display_index` INTEGER NOT NULL," + + "`stream_count` INTEGER)" + ) + db.execSQL( + "INSERT INTO `remote_playlists_tmp` (`uid`, `service_id`, " + + "`name`, `url`, `thumbnail_url`, `uploader`, `display_index`, " + + "`stream_count`)" + + "SELECT `uid`, `service_id`, `name`, `url`, `thumbnail_url`, `uploader`, " + + "-1, `stream_count` FROM `remote_playlists`" + ) + + // Replace the old table, note that this also removes the index on the name which + // we don't need anymore. + db.execSQL("DROP TABLE `remote_playlists`") + db.execSQL("ALTER TABLE `remote_playlists_tmp` RENAME TO `remote_playlists`") + + // Create index on the new table. + db.execSQL( + "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` " + + "ON `remote_playlists` (`service_id`, `url`)" + ) + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt index e7ed93497..d756df8b1 100644 --- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt @@ -168,10 +168,10 @@ abstract class FeedDAO { ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId """ ) - abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable> + abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable> @Query("SELECT MIN(last_updated) FROM feed_last_updated") - abstract fun oldestSubscriptionUpdateFromAll(): Flowable> + abstract fun oldestSubscriptionUpdateFromAll(): Flowable> @Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL") abstract fun notLoadedCount(): Flowable diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java deleted file mode 100644 index 1ade08122..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/HistoryDAO.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import org.schabi.newpipe.database.BasicDAO; - -public interface HistoryDAO extends BasicDAO { - T getLatestEntry(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java deleted file mode 100644 index 8a281bdb4..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import androidx.annotation.Nullable; -import androidx.room.Dao; -import androidx.room.Query; - -import org.schabi.newpipe.database.history.model.SearchHistoryEntry; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.CREATION_DATE; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.ID; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SEARCH; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.SERVICE_ID; -import static org.schabi.newpipe.database.history.model.SearchHistoryEntry.TABLE_NAME; - -@Dao -public interface SearchHistoryDAO extends HistoryDAO { - String ORDER_BY_CREATION_DATE = " ORDER BY " + CREATION_DATE + " DESC"; - String ORDER_BY_MAX_CREATION_DATE = " ORDER BY MAX(" + CREATION_DATE + ") DESC"; - - @Query("SELECT * FROM " + TABLE_NAME - + " WHERE " + ID + " = (SELECT MAX(" + ID + ") FROM " + TABLE_NAME + ")") - @Nullable - SearchHistoryEntry getLatestEntry(); - - @Query("DELETE FROM " + TABLE_NAME) - @Override - int deleteAll(); - - @Query("DELETE FROM " + TABLE_NAME + " WHERE " + SEARCH + " = :query") - int deleteAllWhereQuery(String query); - - @Query("SELECT * FROM " + TABLE_NAME + ORDER_BY_CREATION_DATE) - @Override - Flowable> getAll(); - - @Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " GROUP BY " + SEARCH - + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit") - Flowable> getUniqueEntries(int limit); - - @Query("SELECT * FROM " + TABLE_NAME - + " WHERE " + SERVICE_ID + " = :serviceId" + ORDER_BY_CREATION_DATE) - @Override - Flowable> listByService(int serviceId); - - @Query("SELECT " + SEARCH + " FROM " + TABLE_NAME + " WHERE " + SEARCH + " LIKE :query || '%'" - + " GROUP BY " + SEARCH + ORDER_BY_MAX_CREATION_DATE + " LIMIT :limit") - Flowable> getSimilarEntries(String query, int limit); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt new file mode 100644 index 000000000..ddcb00489 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/SearchHistoryDAO.kt @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2017-2021 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.dao + +import androidx.room.Dao +import androidx.room.Query +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.history.model.SearchHistoryEntry + +@Dao +interface SearchHistoryDAO : BasicDAO { + + @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> + + @Query("SELECT search FROM search_history GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit") + fun getUniqueEntries(limit: Int): Flowable> + + @Query("SELECT * FROM search_history WHERE service_id = :serviceId ORDER BY creation_date DESC") + override fun listByService(serviceId: Int): Flowable> + + @Query( + """ + SELECT search FROM search_history WHERE search LIKE :query || + '%' GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit + """ + ) + fun getSimilarEntries(query: String, limit: Int): Flowable> +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java deleted file mode 100644 index 150d4a8e5..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.schabi.newpipe.database.history.dao; - -import androidx.annotation.Nullable; -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.RewriteQueriesToDropUnusedColumns; - -import org.schabi.newpipe.database.history.model.StreamHistoryEntity; -import org.schabi.newpipe.database.history.model.StreamHistoryEntry; -import org.schabi.newpipe.database.stream.StreamStatisticsEntry; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_REPEAT_COUNT; -import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_LATEST_DATE; -import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WATCH_COUNT; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Dao -public abstract class StreamHistoryDAO implements HistoryDAO { - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE - + " WHERE " + STREAM_ACCESS_DATE + " = " - + "(SELECT MAX(" + STREAM_ACCESS_DATE + ") FROM " + STREAM_HISTORY_TABLE + ")") - @Override - @Nullable - public abstract StreamHistoryEntity getLatestEntry(); - - @Override - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE) - public abstract Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_HISTORY_TABLE) - public abstract int deleteAll(); - - @Override - public Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + STREAM_TABLE - + " INNER JOIN " + STREAM_HISTORY_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " ORDER BY " + STREAM_ACCESS_DATE + " DESC") - public abstract Flowable> getHistory(); - - - @Query("SELECT * FROM " + STREAM_TABLE - + " INNER JOIN " + STREAM_HISTORY_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " ORDER BY " + STREAM_ID + " ASC") - public abstract Flowable> getHistorySortedById(); - - @Query("SELECT * FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID - + " = :streamId ORDER BY " + STREAM_ACCESS_DATE + " DESC LIMIT 1") - @Nullable - public abstract StreamHistoryEntity getLatestEntry(long streamId); - - @Query("DELETE FROM " + STREAM_HISTORY_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - public abstract int deleteStreamHistory(long streamId); - - @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM " + STREAM_TABLE - - // Select the latest entry and watch count for each stream id on history table - + " INNER JOIN " - + "(SELECT " + JOIN_STREAM_ID + ", " - + " MAX(" + STREAM_ACCESS_DATE + ") AS " + STREAM_LATEST_DATE + ", " - + " SUM(" + STREAM_REPEAT_COUNT + ") AS " + STREAM_WATCH_COUNT - + " FROM " + STREAM_HISTORY_TABLE + " GROUP BY " + JOIN_STREAM_ID + ")" - - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - - + " LEFT JOIN " - + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_MILLIS - + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS) - public abstract Flowable> getStatistics(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt new file mode 100644 index 000000000..916d4e5ed --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/dao/StreamHistoryDAO.kt @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.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 { + + @Query("SELECT * FROM stream_history") + abstract override fun getAll(): Flowable> + + @Query("DELETE FROM stream_history") + abstract override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY access_date DESC") + abstract val history: Flowable> + + @get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY uid ASC") + abstract val historySortedById: Flowable> + + @Query("SELECT * FROM stream_history WHERE stream_id = :streamId ORDER BY access_date DESC LIMIT 1") + abstract fun getLatestEntry(streamId: Long): StreamHistoryEntity? + + @Query("DELETE FROM stream_history WHERE stream_id = :streamId") + abstract fun deleteStreamHistory(streamId: Long): Int + + // Select the latest entry and watch count for each stream id on history table + @RewriteQueriesToDropUnusedColumns + @Query( + """ + SELECT * FROM streams + + INNER JOIN ( + SELECT stream_id, MAX(access_date) AS latestAccess, SUM(repeat_count) AS watchCount + FROM stream_history + GROUP BY stream_id + ) + ON uid = stream_id + + LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state ) + ON uid = stream_id_alias + """ + ) + abstract fun getStatistics(): Flowable> +} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt index 8cb9a25ca..e6006a069 100644 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/SearchHistoryEntry.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.database.history.model import androidx.room.ColumnInfo @@ -11,23 +17,24 @@ import java.time.OffsetDateTime tableName = SearchHistoryEntry.TABLE_NAME, indices = [Index(value = [SearchHistoryEntry.SEARCH])] ) -data class SearchHistoryEntry( - @field:ColumnInfo(name = CREATION_DATE) var creationDate: OffsetDateTime?, - @field:ColumnInfo( - name = SERVICE_ID - ) var serviceId: Int, - @field:ColumnInfo(name = SEARCH) var search: String? -) { +data class SearchHistoryEntry @JvmOverloads constructor( + @ColumnInfo(name = CREATION_DATE) + var creationDate: OffsetDateTime?, + + @ColumnInfo(name = SERVICE_ID) + val serviceId: Int, + + @ColumnInfo(name = SEARCH) + val search: String?, + @ColumnInfo(name = ID) @PrimaryKey(autoGenerate = true) - var id: Long = 0 + val id: Long = 0, +) { @Ignore fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean { - return ( - serviceId == otherEntry.serviceId && - search == otherEntry.search - ) + return serviceId == otherEntry.serviceId && search == otherEntry.search } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java deleted file mode 100644 index a9d69afe8..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.schabi.newpipe.database.history.model; - -import androidx.annotation.NonNull; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.ForeignKey; -import androidx.room.Index; - -import org.schabi.newpipe.database.stream.model.StreamEntity; - -import java.time.OffsetDateTime; - -import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_ACCESS_DATE; -import static org.schabi.newpipe.database.history.model.StreamHistoryEntity.STREAM_HISTORY_TABLE; - -@Entity(tableName = STREAM_HISTORY_TABLE, - primaryKeys = {JOIN_STREAM_ID, STREAM_ACCESS_DATE}, - // No need to index for timestamp as they will almost always be unique - indices = {@Index(value = {JOIN_STREAM_ID})}, - foreignKeys = { - @ForeignKey(entity = StreamEntity.class, - parentColumns = StreamEntity.STREAM_ID, - childColumns = JOIN_STREAM_ID, - onDelete = CASCADE, onUpdate = CASCADE) - }) -public class StreamHistoryEntity { - public static final String STREAM_HISTORY_TABLE = "stream_history"; - public static final String JOIN_STREAM_ID = "stream_id"; - public static final String STREAM_ACCESS_DATE = "access_date"; - public static final String STREAM_REPEAT_COUNT = "repeat_count"; - - @ColumnInfo(name = JOIN_STREAM_ID) - private long streamUid; - - @NonNull - @ColumnInfo(name = STREAM_ACCESS_DATE) - private OffsetDateTime accessDate; - - @ColumnInfo(name = STREAM_REPEAT_COUNT) - private long repeatCount; - - /** - * @param streamUid the stream id this history item will refer to - * @param accessDate the last time the stream was accessed - * @param repeatCount the total number of views this stream received - */ - public StreamHistoryEntity(final long streamUid, - @NonNull final OffsetDateTime accessDate, - final long repeatCount) { - this.streamUid = streamUid; - this.accessDate = accessDate; - this.repeatCount = repeatCount; - } - - public long getStreamUid() { - return streamUid; - } - - public void setStreamUid(final long streamUid) { - this.streamUid = streamUid; - } - - @NonNull - public OffsetDateTime getAccessDate() { - return accessDate; - } - - public void setAccessDate(@NonNull final OffsetDateTime accessDate) { - this.accessDate = accessDate; - } - - public long getRepeatCount() { - return repeatCount; - } - - public void setRepeatCount(final long repeatCount) { - this.repeatCount = repeatCount; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt new file mode 100644 index 000000000..db41e141c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/history/model/StreamHistoryEntity.kt @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.history.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import androidx.room.Index +import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.JOIN_STREAM_ID +import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_ACCESS_DATE +import org.schabi.newpipe.database.history.model.StreamHistoryEntity.Companion.STREAM_HISTORY_TABLE +import org.schabi.newpipe.database.stream.model.StreamEntity +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID +import java.time.OffsetDateTime + +/** + * @param streamUid the stream id this history item will refer to + * @param accessDate the last time the stream was accessed + * @param repeatCount the total number of views this stream received + */ +@Entity( + tableName = STREAM_HISTORY_TABLE, + primaryKeys = [JOIN_STREAM_ID, STREAM_ACCESS_DATE], + indices = [Index(value = [JOIN_STREAM_ID])], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = arrayOf(STREAM_ID), + childColumns = arrayOf(JOIN_STREAM_ID), + onDelete = CASCADE, + onUpdate = CASCADE + ) + ] +) +data class StreamHistoryEntity( + @ColumnInfo(name = JOIN_STREAM_ID) + val streamUid: Long, + + @ColumnInfo(name = STREAM_ACCESS_DATE) + var accessDate: OffsetDateTime, + + @ColumnInfo(name = STREAM_REPEAT_COUNT) + var repeatCount: Long +) { + companion object { + const val STREAM_HISTORY_TABLE: String = "stream_history" + const val STREAM_ACCESS_DATE: String = "access_date" + const val JOIN_STREAM_ID: String = "stream_id" + const val STREAM_REPEAT_COUNT: String = "repeat_count" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java deleted file mode 100644 index 3be85e6e1..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.java +++ /dev/null @@ -1,29 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.room.ColumnInfo; - -/** - * This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing - * how many times a specific stream is already contained inside a local playlist. Used to be able - * to grey out playlists which already contain the current stream in the playlist append dialog. - * @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String) - */ -public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry { - public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained"; - @ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED) - public final long timesStreamIsContained; - - @SuppressWarnings("checkstyle:ParameterNumber") - public PlaylistDuplicatesEntry(final long uid, - final String name, - final String thumbnailUrl, - final boolean isThumbnailPermanent, - final long thumbnailStreamId, - final long displayIndex, - final long streamCount, - final long timesStreamIsContained) { - super(uid, name, thumbnailUrl, isThumbnailPermanent, thumbnailStreamId, displayIndex, - streamCount); - this.timesStreamIsContained = timesStreamIsContained; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt new file mode 100644 index 000000000..84972a89e --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistDuplicatesEntry.kt @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import androidx.room.ColumnInfo +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" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java deleted file mode 100644 index 91f4622e9..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.annotation.Nullable; - -import org.schabi.newpipe.database.LocalItem; - -public interface PlaylistLocalItem extends LocalItem { - String getOrderingName(); - - long getDisplayIndex(); - - long getUid(); - - void setDisplayIndex(long displayIndex); - - @Nullable - String getThumbnailUrl(); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt new file mode 100644 index 000000000..4f2f79aa0 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistLocalItem.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import org.schabi.newpipe.database.LocalItem + +interface PlaylistLocalItem : LocalItem { + val orderingName: String? + val displayIndex: Long? + val uid: Long + val thumbnailUrl: String? +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java deleted file mode 100644 index 8fbadb020..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.schabi.newpipe.database.playlist; - -import androidx.room.ColumnInfo; - -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; - -import androidx.annotation.Nullable; - -public class PlaylistMetadataEntry implements PlaylistLocalItem { - public static final String PLAYLIST_STREAM_COUNT = "streamCount"; - - @ColumnInfo(name = PLAYLIST_ID) - private final long uid; - @ColumnInfo(name = PLAYLIST_NAME) - public final String name; - @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT) - private final boolean isThumbnailPermanent; - @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) - private final long thumbnailStreamId; - @ColumnInfo(name = PLAYLIST_THUMBNAIL_URL) - public final String thumbnailUrl; - @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - private long displayIndex; - @ColumnInfo(name = PLAYLIST_STREAM_COUNT) - public final long streamCount; - - public PlaylistMetadataEntry(final long uid, final String name, final String thumbnailUrl, - final boolean isThumbnailPermanent, final long thumbnailStreamId, - final long displayIndex, final long streamCount) { - this.uid = uid; - this.name = name; - this.thumbnailUrl = thumbnailUrl; - this.isThumbnailPermanent = isThumbnailPermanent; - this.thumbnailStreamId = thumbnailStreamId; - this.displayIndex = displayIndex; - this.streamCount = streamCount; - } - - @Override - public LocalItemType getLocalItemType() { - return LocalItemType.PLAYLIST_LOCAL_ITEM; - } - - @Override - public String getOrderingName() { - return name; - } - - public boolean isThumbnailPermanent() { - return isThumbnailPermanent; - } - - public long getThumbnailStreamId() { - return thumbnailStreamId; - } - - @Override - public long getDisplayIndex() { - return displayIndex; - } - - @Override - public long getUid() { - return uid; - } - - @Override - public void setDisplayIndex(final long displayIndex) { - this.displayIndex = displayIndex; - } - - @Nullable - @Override - public String getThumbnailUrl() { - return thumbnailUrl; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt new file mode 100644 index 000000000..9b62c1380 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistMetadataEntry.kt @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist + +import androidx.room.ColumnInfo +import org.schabi.newpipe.database.LocalItem.LocalItemType +import org.schabi.newpipe.database.playlist.model.PlaylistEntity + +open class PlaylistMetadataEntry( + @ColumnInfo(name = PlaylistEntity.PLAYLIST_ID) + override val uid: Long, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_NAME) + override val orderingName: String?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_URL) + override val thumbnailUrl: String?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_DISPLAY_INDEX) + override var displayIndex: Long?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT) + open val isThumbnailPermanent: Boolean?, + + @ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID) + open val thumbnailStreamId: Long?, + + @ColumnInfo(name = PLAYLIST_STREAM_COUNT) + open val streamCount: Long +) : PlaylistLocalItem { + + override val localItemType: LocalItemType + get() = LocalItemType.PLAYLIST_LOCAL_ITEM + + companion object { + const val PLAYLIST_STREAM_COUNT: String = "streamCount" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt index 1d74c6d31..90fdee2d3 100644 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/PlaylistStreamEntry.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2020-2023 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.database.playlist import androidx.room.ColumnInfo @@ -23,18 +29,21 @@ data class PlaylistStreamEntry( val joinIndex: Int ) : LocalItem { + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + @Throws(IllegalArgumentException::class) fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - - override fun getLocalItemType(): LocalItem.LocalItemType { - return LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + return StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } } } diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java deleted file mode 100644 index d8071e0af..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.schabi.newpipe.database.playlist.dao; - -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; - -@Dao -public interface PlaylistDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + PLAYLIST_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + PLAYLIST_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") - Flowable> getPlaylist(long playlistId); - - @Query("DELETE FROM " + PLAYLIST_TABLE + " WHERE " + PLAYLIST_ID + " = :playlistId") - int deletePlaylist(long playlistId); - - @Query("SELECT COUNT(*) FROM " + PLAYLIST_TABLE) - Flowable getCount(); - - @Transaction - default long upsertPlaylist(final PlaylistEntity playlist) { - final long playlistId = playlist.getUid(); - - if (playlistId == -1) { - // This situation is probably impossible. - return insert(playlist); - } else { - update(playlist); - return playlistId; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt new file mode 100644 index 000000000..9c2dd89a8 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistDAO.kt @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2018-2022 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.playlist.model.PlaylistEntity + +@Dao +interface PlaylistDAO : BasicDAO { + + @Query("SELECT * FROM playlists") + override fun getAll(): Flowable> + + @Query("DELETE FROM playlists") + override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @Query("SELECT * FROM playlists WHERE uid = :playlistId") + fun getPlaylist(playlistId: Long): Flowable> + + @Query("DELETE FROM playlists WHERE uid = :playlistId") + fun deletePlaylist(playlistId: Long): Int + + @get:Query("SELECT COUNT(*) FROM playlists") + val count: Flowable + + @Transaction + fun upsertPlaylist(playlist: PlaylistEntity): Long { + if (playlist.uid == -1L) { + // This situation is probably impossible. + return insert(playlist) + } else { + update(playlist) + return playlist.uid + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java deleted file mode 100644 index ef77d5ade..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.schabi.newpipe.database.playlist.dao; - -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_DISPLAY_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; - -@Dao -public interface PlaylistRemoteDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE) - int deleteAll(); - - @Override - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE - + " WHERE " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") - Flowable> listByService(int serviceId); - - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " - + REMOTE_PLAYLIST_ID + " = :playlistId") - Flowable getPlaylist(long playlistId); - - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE + " WHERE " - + REMOTE_PLAYLIST_URL + " = :url AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") - Flowable> getPlaylist(long serviceId, String url); - - @Query("SELECT * FROM " + REMOTE_PLAYLIST_TABLE - + " ORDER BY " + REMOTE_PLAYLIST_DISPLAY_INDEX) - Flowable> getPlaylists(); - - @Query("SELECT " + REMOTE_PLAYLIST_ID + " FROM " + REMOTE_PLAYLIST_TABLE - + " WHERE " + REMOTE_PLAYLIST_URL + " = :url " - + "AND " + REMOTE_PLAYLIST_SERVICE_ID + " = :serviceId") - Long getPlaylistIdInternal(long serviceId, String url); - - @Transaction - default long upsert(final PlaylistRemoteEntity playlist) { - final Long playlistId = getPlaylistIdInternal(playlist.getServiceId(), playlist.getUrl()); - - if (playlistId == null) { - return insert(playlist); - } else { - playlist.setUid(playlistId); - update(playlist); - return playlistId; - } - } - - @Query("DELETE FROM " + REMOTE_PLAYLIST_TABLE - + " WHERE " + REMOTE_PLAYLIST_ID + " = :playlistId") - int deletePlaylist(long playlistId); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt new file mode 100644 index 000000000..36a80bc91 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistRemoteDAO.kt @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity + +@Dao +interface PlaylistRemoteDAO : BasicDAO { + + @Query("SELECT * FROM remote_playlists") + override fun getAll(): Flowable> + + @Query("DELETE FROM remote_playlists") + override fun deleteAll(): Int + + @Query("SELECT * FROM remote_playlists WHERE service_id = :serviceId") + override fun listByService(serviceId: Int): Flowable> + + @Query("SELECT * FROM remote_playlists WHERE uid = :playlistId") + fun getPlaylist(playlistId: Long): Flowable + + @Query("SELECT * FROM remote_playlists WHERE url = :url AND service_id = :serviceId") + fun getPlaylist(serviceId: Long, url: String?): Flowable> + + @get:Query("SELECT * FROM remote_playlists ORDER BY display_index") + val playlists: Flowable> + + @Query("SELECT uid FROM remote_playlists WHERE url = :url AND service_id = :serviceId") + fun getPlaylistIdInternal(serviceId: Long, url: String?): Long? + + @Transaction + fun upsert(playlist: PlaylistRemoteEntity): Long { + val playlistId = getPlaylistIdInternal(playlist.serviceId.toLong(), playlist.url) + + if (playlistId == null) { + return insert(playlist) + } else { + playlist.uid = playlistId + update(playlist) + return playlistId + } + } + + @Query("DELETE FROM remote_playlists WHERE uid = :playlistId") + fun deletePlaylist(playlistId: Long): Int +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java deleted file mode 100644 index 6b77166ea..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.schabi.newpipe.database.playlist.dao; - -import androidx.room.Dao; -import androidx.room.Query; -import androidx.room.RewriteQueriesToDropUnusedColumns; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; -import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; -import org.schabi.newpipe.database.playlist.model.PlaylistEntity; -import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED; -import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_DISPLAY_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.DEFAULT_THUMBNAIL; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_THUMBNAIL_URL; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL; -import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Dao -public interface PlaylistStreamDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + PLAYLIST_STREAM_JOIN_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("DELETE FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") - void deleteBatch(long playlistId); - - @Query("SELECT COALESCE(MAX(" + JOIN_INDEX + "), -1)" - + " FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId") - Flowable getMaximumIndexOf(long playlistId); - - @Query("SELECT CASE WHEN COUNT(*) != 0 then " + STREAM_ID - + " ELSE " + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " END" - + " FROM " + STREAM_TABLE - + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId " - + " LIMIT 1" - ) - Flowable getAutomaticThumbnailStreamId(long playlistId); - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query("SELECT * FROM " + STREAM_TABLE + " INNER JOIN " - // get ids of streams of the given playlist - + "(SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX - + " FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" - - // then merge with the stream metadata - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - - + " LEFT JOIN " - + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_MILLIS - + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS - - + " ORDER BY " + JOIN_INDEX + " ASC") - Flowable> getOrderedStreamsOf(long playlistId); - - @Transaction - @Query("SELECT " + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " - + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", " - + PLAYLIST_DISPLAY_INDEX + ", " - - + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = " - + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'" - + " ELSE (SELECT " + STREAM_THUMBNAIL_URL - + " FROM " + STREAM_TABLE - + " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID - + " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", " - - + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT - + " FROM " + PLAYLIST_TABLE - + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE - + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID - + " GROUP BY " + PLAYLIST_ID - + " ORDER BY " + PLAYLIST_DISPLAY_INDEX) - Flowable> getPlaylistMetadata(); - - @RewriteQueriesToDropUnusedColumns - @Transaction - @Query("SELECT *, MIN(" + JOIN_INDEX + ")" - + " FROM " + STREAM_TABLE + " INNER JOIN" - + " (SELECT " + JOIN_STREAM_ID + "," + JOIN_INDEX - + " FROM " + PLAYLIST_STREAM_JOIN_TABLE - + " WHERE " + JOIN_PLAYLIST_ID + " = :playlistId)" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID - + " LEFT JOIN " - + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " - + STREAM_PROGRESS_MILLIS - + " FROM " + STREAM_STATE_TABLE + " )" - + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS - + " GROUP BY " + STREAM_ID - + " ORDER BY MIN(" + JOIN_INDEX + ") ASC") - Flowable> getStreamsWithoutDuplicates(long playlistId); - - @Transaction - @Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", " + PLAYLIST_NAME + ", " - + PLAYLIST_THUMBNAIL_PERMANENT + ", " + PLAYLIST_THUMBNAIL_STREAM_ID + ", " - + PLAYLIST_DISPLAY_INDEX + ", " - - + " CASE WHEN " + PLAYLIST_THUMBNAIL_STREAM_ID + " = " - + PlaylistEntity.DEFAULT_THUMBNAIL_ID + " THEN " + "'" + DEFAULT_THUMBNAIL + "'" - + " ELSE (SELECT " + STREAM_THUMBNAIL_URL - + " FROM " + STREAM_TABLE - + " WHERE " + STREAM_TABLE + "." + STREAM_ID + " = " + PLAYLIST_THUMBNAIL_STREAM_ID - + " ) END AS " + PLAYLIST_THUMBNAIL_URL + ", " - - + "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", " - + "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS " - + PLAYLIST_TIMES_STREAM_IS_CONTAINED - - + " FROM " + PLAYLIST_TABLE - + " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE - + " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID - - + " LEFT JOIN " + STREAM_TABLE - + " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID - + " AND :streamUrl = :streamUrl" - - + " GROUP BY " + JOIN_PLAYLIST_ID - + " ORDER BY " + PLAYLIST_DISPLAY_INDEX + ", " + PLAYLIST_NAME) - Flowable> getPlaylistDuplicatesMetadata(String streamUrl); -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt new file mode 100644 index 000000000..8bf26d754 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.kt @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2018-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry +import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry +import org.schabi.newpipe.database.playlist.PlaylistStreamEntry +import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity + +@Dao +interface PlaylistStreamDAO : BasicDAO { + + @Query("SELECT * FROM playlist_stream_join") + override fun getAll(): Flowable> + + @Query("DELETE FROM playlist_stream_join") + override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @Query("DELETE FROM playlist_stream_join WHERE playlist_id = :playlistId") + fun deleteBatch(playlistId: Long) + + @Query("SELECT COALESCE(MAX(join_index), -1) FROM playlist_stream_join WHERE playlist_id = :playlistId") + fun getMaximumIndexOf(playlistId: Long): Flowable + + @Query( + """ + SELECT CASE WHEN COUNT(*) != 0 then stream_id ELSE $DEFAULT_THUMBNAIL_ID END + FROM streams + + LEFT JOIN playlist_stream_join + ON uid = stream_id + + WHERE playlist_id = :playlistId LIMIT 1 + """ + ) + fun getAutomaticThumbnailStreamId(playlistId: Long): Flowable + + // get ids of streams of the given playlist then merge with the stream metadata + @RewriteQueriesToDropUnusedColumns + @Transaction + @Query( + """ + SELECT * FROM streams + + INNER JOIN (SELECT stream_id, join_index FROM playlist_stream_join WHERE playlist_id = :playlistId) + ON uid = stream_id + + LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state ) + ON uid = stream_id_alias + + ORDER BY join_index ASC + """ + ) + fun getOrderedStreamsOf(playlistId: Long): Flowable> + + @Transaction + @Query( + """ + SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index, + (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> + + @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> + + @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> +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java deleted file mode 100644 index e0c1a06b7..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.schabi.newpipe.database.playlist.model; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.PrimaryKey; - -import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_TABLE; - -import org.schabi.newpipe.R; -import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; - -@Entity(tableName = PLAYLIST_TABLE) -public class PlaylistEntity { - - public static final String DEFAULT_THUMBNAIL = "drawable://" - + R.drawable.placeholder_thumbnail_playlist; - public static final long DEFAULT_THUMBNAIL_ID = -1; - - public static final String PLAYLIST_TABLE = "playlists"; - public static final String PLAYLIST_ID = "uid"; - public static final String PLAYLIST_NAME = "name"; - public static final String PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; - public static final String PLAYLIST_DISPLAY_INDEX = "display_index"; - public static final String PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent"; - public static final String PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id"; - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = PLAYLIST_ID) - private long uid = 0; - - @ColumnInfo(name = PLAYLIST_NAME) - private String name; - - @ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT) - private boolean isThumbnailPermanent; - - @ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID) - private long thumbnailStreamId; - - @ColumnInfo(name = PLAYLIST_DISPLAY_INDEX) - private long displayIndex; - - public PlaylistEntity(final String name, final boolean isThumbnailPermanent, - final long thumbnailStreamId, final long displayIndex) { - this.name = name; - this.isThumbnailPermanent = isThumbnailPermanent; - this.thumbnailStreamId = thumbnailStreamId; - this.displayIndex = displayIndex; - } - - @Ignore - public PlaylistEntity(final PlaylistMetadataEntry item) { - this.uid = item.getUid(); - this.name = item.name; - this.isThumbnailPermanent = item.isThumbnailPermanent(); - this.thumbnailStreamId = item.getThumbnailStreamId(); - this.displayIndex = item.getDisplayIndex(); - } - - public long getUid() { - return uid; - } - - public void setUid(final long uid) { - this.uid = uid; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public long getThumbnailStreamId() { - return thumbnailStreamId; - } - - public void setThumbnailStreamId(final long thumbnailStreamId) { - this.thumbnailStreamId = thumbnailStreamId; - } - - public boolean getIsThumbnailPermanent() { - return isThumbnailPermanent; - } - - public void setIsThumbnailPermanent(final boolean isThumbnailSet) { - this.isThumbnailPermanent = isThumbnailSet; - } - - public long getDisplayIndex() { - return displayIndex; - } - - public void setDisplayIndex(final long displayIndex) { - this.displayIndex = displayIndex; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt new file mode 100644 index 000000000..4ea4eb3a7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistEntity.kt @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2018-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey +import org.schabi.newpipe.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" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java deleted file mode 100644 index 0b0e3605e..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.java +++ /dev/null @@ -1,191 +0,0 @@ -package org.schabi.newpipe.database.playlist.model; - -import android.text.TextUtils; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; -import androidx.room.PrimaryKey; - -import org.schabi.newpipe.database.playlist.PlaylistLocalItem; -import org.schabi.newpipe.extractor.playlist.PlaylistInfo; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.image.ImageStrategy; - -import static org.schabi.newpipe.database.LocalItem.LocalItemType.PLAYLIST_REMOTE_ITEM; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_NAME; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_SERVICE_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_TABLE; -import static org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.REMOTE_PLAYLIST_URL; - -@Entity(tableName = REMOTE_PLAYLIST_TABLE, - indices = { - @Index(value = {REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL}, unique = true) - }) -public class PlaylistRemoteEntity implements PlaylistLocalItem { - public static final String REMOTE_PLAYLIST_TABLE = "remote_playlists"; - public static final String REMOTE_PLAYLIST_ID = "uid"; - public static final String REMOTE_PLAYLIST_SERVICE_ID = "service_id"; - public static final String REMOTE_PLAYLIST_NAME = "name"; - public static final String REMOTE_PLAYLIST_URL = "url"; - public static final String REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url"; - public static final String REMOTE_PLAYLIST_UPLOADER_NAME = "uploader"; - public static final String REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index"; - public static final String REMOTE_PLAYLIST_STREAM_COUNT = "stream_count"; - - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = REMOTE_PLAYLIST_ID) - private long uid = 0; - - @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) - private int serviceId = Constants.NO_SERVICE_ID; - - @ColumnInfo(name = REMOTE_PLAYLIST_NAME) - private String name; - - @ColumnInfo(name = REMOTE_PLAYLIST_URL) - private String url; - - @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) - private String thumbnailUrl; - - @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) - private String uploader; - - @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) - private long displayIndex = -1; // Make sure the new item is on the top - - @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) - private Long streamCount; - - public PlaylistRemoteEntity(final int serviceId, final String name, final String url, - final String thumbnailUrl, final String uploader, - final Long streamCount) { - this.serviceId = serviceId; - this.name = name; - this.url = url; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; - this.streamCount = streamCount; - } - - @Ignore - public PlaylistRemoteEntity(final int serviceId, final String name, final String url, - final String thumbnailUrl, final String uploader, - final long displayIndex, final Long streamCount) { - this.serviceId = serviceId; - this.name = name; - this.url = url; - this.thumbnailUrl = thumbnailUrl; - this.uploader = uploader; - this.displayIndex = displayIndex; - this.streamCount = streamCount; - } - - @Ignore - public PlaylistRemoteEntity(final PlaylistInfo info) { - this(info.getServiceId(), info.getName(), info.getUrl(), - // use uploader avatar when no thumbnail is available - ImageStrategy.imageListToDbUrl(info.getThumbnails().isEmpty() - ? info.getUploaderAvatars() : info.getThumbnails()), - info.getUploaderName(), info.getStreamCount()); - } - - @Ignore - public boolean isIdenticalTo(final PlaylistInfo info) { - /* - * Returns boolean comparing the online playlist and the local copy. - * (False if info changed such as playlist name or track count) - */ - return getServiceId() == info.getServiceId() - && getStreamCount() == info.getStreamCount() - && TextUtils.equals(getName(), info.getName()) - && TextUtils.equals(getUrl(), info.getUrl()) - // we want to update the local playlist data even when either the remote thumbnail - // URL changes, or the preferred image quality setting is changed by the user - && TextUtils.equals(getThumbnailUrl(), - ImageStrategy.imageListToDbUrl(info.getThumbnails())) - && TextUtils.equals(getUploader(), info.getUploaderName()); - } - - @Override - public long getUid() { - return uid; - } - - public void setUid(final long uid) { - this.uid = uid; - } - - public int getServiceId() { - return serviceId; - } - - public void setServiceId(final int serviceId) { - this.serviceId = serviceId; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - @Nullable - @Override - public String getThumbnailUrl() { - return thumbnailUrl; - } - - public void setThumbnailUrl(final String thumbnailUrl) { - this.thumbnailUrl = thumbnailUrl; - } - - public String getUrl() { - return url; - } - - public void setUrl(final String url) { - this.url = url; - } - - public String getUploader() { - return uploader; - } - - public void setUploader(final String uploader) { - this.uploader = uploader; - } - - @Override - public long getDisplayIndex() { - return displayIndex; - } - - @Override - public void setDisplayIndex(final long displayIndex) { - this.displayIndex = displayIndex; - } - - public Long getStreamCount() { - return streamCount; - } - - public void setStreamCount(final Long streamCount) { - this.streamCount = streamCount; - } - - @Override - public LocalItemType getLocalItemType() { - return PLAYLIST_REMOTE_ITEM; - } - - @Override - public String getOrderingName() { - return name; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt new file mode 100644 index 000000000..82162e1e4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistRemoteEntity.kt @@ -0,0 +1,104 @@ +/* + * SPDX-FileCopyrightText: 2018-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import android.text.TextUtils +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import org.schabi.newpipe.database.LocalItem.LocalItemType +import org.schabi.newpipe.database.playlist.PlaylistLocalItem +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE +import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL +import org.schabi.newpipe.extractor.playlist.PlaylistInfo +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.image.ImageStrategy + +@Entity( + tableName = REMOTE_PLAYLIST_TABLE, + indices = [ + Index( + value = [REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL], + unique = true + ) + ] +) +data class PlaylistRemoteEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = REMOTE_PLAYLIST_ID) + override var uid: Long = 0, + + @ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID) + val serviceId: Int = NO_SERVICE_ID, + + @ColumnInfo(name = REMOTE_PLAYLIST_NAME) + override val orderingName: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_URL) + val url: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL) + override val thumbnailUrl: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME) + val uploader: String?, + + @ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX) + override var displayIndex: Long = -1, // Make sure the new item is on the top + + @ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT) + val streamCount: Long? +) : PlaylistLocalItem { + + constructor(playlistInfo: PlaylistInfo) : this( + serviceId = playlistInfo.serviceId, + orderingName = playlistInfo.name, + url = playlistInfo.url, + thumbnailUrl = ImageStrategy.imageListToDbUrl( + if (playlistInfo.thumbnails.isEmpty()) { + playlistInfo.uploaderAvatars + } else { + playlistInfo.thumbnails + } + ), + uploader = playlistInfo.uploaderName, + streamCount = playlistInfo.streamCount + ) + + override val localItemType: LocalItemType + get() = LocalItemType.PLAYLIST_REMOTE_ITEM + + /** + * Returns boolean comparing the online playlist and the local copy. + * (False if info changed such as playlist name or track count) + */ + @Ignore + fun isIdenticalTo(info: PlaylistInfo): Boolean { + return this.serviceId == info.serviceId && this.streamCount == info.streamCount && + TextUtils.equals(this.orderingName, info.name) && + TextUtils.equals(this.url, info.url) && + // we want to update the local playlist data even when either the remote thumbnail + // URL changes, or the preferred image quality setting is changed by the user + TextUtils.equals(thumbnailUrl, ImageStrategy.imageListToDbUrl(info.thumbnails)) && + TextUtils.equals(this.uploader, info.uploaderName) + } + + companion object { + const val REMOTE_PLAYLIST_TABLE = "remote_playlists" + const val REMOTE_PLAYLIST_ID = "uid" + const val REMOTE_PLAYLIST_SERVICE_ID = "service_id" + const val REMOTE_PLAYLIST_NAME = "name" + const val REMOTE_PLAYLIST_URL = "url" + const val REMOTE_PLAYLIST_THUMBNAIL_URL = "thumbnail_url" + const val REMOTE_PLAYLIST_UPLOADER_NAME = "uploader" + const val REMOTE_PLAYLIST_DISPLAY_INDEX = "display_index" + const val REMOTE_PLAYLIST_STREAM_COUNT = "stream_count" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java deleted file mode 100644 index f3208b6d5..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.schabi.newpipe.database.playlist.model; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.ForeignKey; -import androidx.room.Index; - -import org.schabi.newpipe.database.stream.model.StreamEntity; - -import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_INDEX; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_PLAYLIST_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PLAYLIST_STREAM_JOIN_TABLE; - -@Entity(tableName = PLAYLIST_STREAM_JOIN_TABLE, - primaryKeys = {JOIN_PLAYLIST_ID, JOIN_INDEX}, - indices = { - @Index(value = {JOIN_PLAYLIST_ID, JOIN_INDEX}, unique = true), - @Index(value = {JOIN_STREAM_ID}) - }, - foreignKeys = { - @ForeignKey(entity = PlaylistEntity.class, - parentColumns = PlaylistEntity.PLAYLIST_ID, - childColumns = JOIN_PLAYLIST_ID, - onDelete = CASCADE, onUpdate = CASCADE, deferred = true), - @ForeignKey(entity = StreamEntity.class, - parentColumns = StreamEntity.STREAM_ID, - childColumns = JOIN_STREAM_ID, - onDelete = CASCADE, onUpdate = CASCADE, deferred = true) - }) -public class PlaylistStreamEntity { - public static final String PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"; - public static final String JOIN_PLAYLIST_ID = "playlist_id"; - public static final String JOIN_STREAM_ID = "stream_id"; - public static final String JOIN_INDEX = "join_index"; - - @ColumnInfo(name = JOIN_PLAYLIST_ID) - private long playlistUid; - - @ColumnInfo(name = JOIN_STREAM_ID) - private long streamUid; - - @ColumnInfo(name = JOIN_INDEX) - private int index; - - public PlaylistStreamEntity(final long playlistUid, final long streamUid, final int index) { - this.playlistUid = playlistUid; - this.streamUid = streamUid; - this.index = index; - } - - public long getPlaylistUid() { - return playlistUid; - } - - public void setPlaylistUid(final long playlistUid) { - this.playlistUid = playlistUid; - } - - public long getStreamUid() { - return streamUid; - } - - public void setStreamUid(final long streamUid) { - this.streamUid = streamUid; - } - - public int getIndex() { - return index; - } - - public void setIndex(final int index) { - this.index = index; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt new file mode 100644 index 000000000..6ab1b6ac4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/playlist/model/PlaylistStreamEntity.kt @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2018-2020 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.playlist.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import androidx.room.Index +import org.schabi.newpipe.database.LocalItem +import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.PLAYLIST_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_INDEX +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_STREAM_ID +import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE +import org.schabi.newpipe.database.stream.model.StreamEntity + +@Entity( + tableName = PLAYLIST_STREAM_JOIN_TABLE, + primaryKeys = [JOIN_PLAYLIST_ID, JOIN_INDEX], + indices = [ + Index(value = [JOIN_PLAYLIST_ID, JOIN_INDEX], unique = true), + Index(value = [JOIN_STREAM_ID]) + ], + foreignKeys = [ + ForeignKey( + entity = PlaylistEntity::class, + parentColumns = arrayOf(PLAYLIST_ID), + childColumns = arrayOf(JOIN_PLAYLIST_ID), + onDelete = CASCADE, + onUpdate = CASCADE, + deferred = true + ), + ForeignKey( + entity = StreamEntity::class, + parentColumns = arrayOf(StreamEntity.STREAM_ID), + childColumns = arrayOf(JOIN_STREAM_ID), + onDelete = CASCADE, + onUpdate = CASCADE, + deferred = true + ) + ] +) +data class PlaylistStreamEntity( + @ColumnInfo(name = JOIN_PLAYLIST_ID) + val playlistUid: Long, + + @ColumnInfo(name = JOIN_STREAM_ID) + val streamUid: Long, + + @ColumnInfo(name = JOIN_INDEX) + val index: Int +) : LocalItem { + + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM + + companion object { + const val PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join" + const val JOIN_PLAYLIST_ID = "playlist_id" + const val JOIN_STREAM_ID = "stream_id" + const val JOIN_INDEX = "join_index" + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt index 1f3654e7a..3fa281e45 100644 --- a/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt +++ b/app/src/main/java/org/schabi/newpipe/database/stream/StreamStatisticsEntry.kt @@ -1,16 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2020-2023 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + package org.schabi.newpipe.database.stream import androidx.room.ColumnInfo import androidx.room.Embedded +import androidx.room.Ignore import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.stream.model.StreamEntity -import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_PROGRESS_MILLIS import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.util.image.ImageStrategy import java.time.OffsetDateTime -class StreamStatisticsEntry( +data class StreamStatisticsEntry( @Embedded val streamEntity: StreamEntity, @@ -26,18 +33,23 @@ class StreamStatisticsEntry( @ColumnInfo(name = STREAM_WATCH_COUNT) val watchCount: Long ) : LocalItem { + + override val localItemType: LocalItem.LocalItemType + get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM + + @Ignore fun toStreamInfoItem(): StreamInfoItem { - val item = StreamInfoItem(streamEntity.serviceId, streamEntity.url, streamEntity.title, streamEntity.streamType) - item.duration = streamEntity.duration - item.uploaderName = streamEntity.uploader - item.uploaderUrl = streamEntity.uploaderUrl - item.thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) - - return item - } - - override fun getLocalItemType(): LocalItem.LocalItemType { - return LocalItem.LocalItemType.STATISTIC_STREAM_ITEM + return StreamInfoItem( + streamEntity.serviceId, + streamEntity.url, + streamEntity.title, + streamEntity.streamType + ).apply { + duration = streamEntity.duration + uploaderName = streamEntity.uploader + uploaderUrl = streamEntity.uploaderUrl + thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl) + } } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java deleted file mode 100644 index 06371248d..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.schabi.newpipe.database.stream.dao; - -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; -import androidx.room.Transaction; - -import org.schabi.newpipe.database.BasicDAO; -import org.schabi.newpipe.database.stream.model.StreamStateEntity; - -import java.util.List; - -import io.reactivex.rxjava3.core.Flowable; - -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Dao -public interface StreamStateDAO extends BasicDAO { - @Override - @Query("SELECT * FROM " + STREAM_STATE_TABLE) - Flowable> getAll(); - - @Override - @Query("DELETE FROM " + STREAM_STATE_TABLE) - int deleteAll(); - - @Override - default Flowable> listByService(final int serviceId) { - throw new UnsupportedOperationException(); - } - - @Query("SELECT * FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - Flowable> getState(long streamId); - - @Query("DELETE FROM " + STREAM_STATE_TABLE + " WHERE " + JOIN_STREAM_ID + " = :streamId") - int deleteState(long streamId); - - @Insert(onConflict = OnConflictStrategy.IGNORE) - void silentInsertInternal(StreamStateEntity streamState); - - @Transaction - default long upsert(final StreamStateEntity stream) { - silentInsertInternal(stream); - return update(stream); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt new file mode 100644 index 000000000..f3c44f1f2 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/dao/StreamStateDAO.kt @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2018-2021 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.stream.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import io.reactivex.rxjava3.core.Flowable +import org.schabi.newpipe.database.BasicDAO +import org.schabi.newpipe.database.stream.model.StreamStateEntity + +@Dao +interface StreamStateDAO : BasicDAO { + + @Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE) + override fun getAll(): Flowable> + + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE) + override fun deleteAll(): Int + + override fun listByService(serviceId: Int): Flowable> { + throw UnsupportedOperationException() + } + + @Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun getState(streamId: Long): Flowable> + + @Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId") + fun deleteState(streamId: Long): Int + + @Insert(onConflict = OnConflictStrategy.Companion.IGNORE) + fun silentInsertInternal(streamState: StreamStateEntity) + + @Transaction + fun upsert(stream: StreamStateEntity): Long { + silentInsertInternal(stream) + return update(stream).toLong() + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java deleted file mode 100644 index 627acea45..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.schabi.newpipe.database.stream.model; - -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.ForeignKey; - -import java.util.Objects; - -import static androidx.room.ForeignKey.CASCADE; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID; -import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; - -@Entity(tableName = STREAM_STATE_TABLE, - primaryKeys = {JOIN_STREAM_ID}, - foreignKeys = { - @ForeignKey(entity = StreamEntity.class, - parentColumns = StreamEntity.STREAM_ID, - childColumns = JOIN_STREAM_ID, - onDelete = CASCADE, onUpdate = CASCADE) - }) -public class StreamStateEntity { - public static final String STREAM_STATE_TABLE = "stream_state"; - public static final String JOIN_STREAM_ID = "stream_id"; - // This additional field is required for the SQL query because 'stream_id' is used - // for some other joins already - public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias"; - public static final String STREAM_PROGRESS_MILLIS = "progress_time"; - - /** - * Playback state will not be saved, if playback time is less than this threshold (5000ms = 5s). - */ - public static final long PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000; - - /** - * Stream will be considered finished if the playback time left exceeds this threshold - * (60000ms = 60s). - * @see #isFinished(long) - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long) - */ - public static final long PLAYBACK_FINISHED_END_MILLISECONDS = 60000; - - @ColumnInfo(name = JOIN_STREAM_ID) - private long streamUid; - - @ColumnInfo(name = STREAM_PROGRESS_MILLIS) - private long progressMillis; - - public StreamStateEntity(final long streamUid, final long progressMillis) { - this.streamUid = streamUid; - this.progressMillis = progressMillis; - } - - public long getStreamUid() { - return streamUid; - } - - public void setStreamUid(final long streamUid) { - this.streamUid = streamUid; - } - - public long getProgressMillis() { - return progressMillis; - } - - public void setProgressMillis(final long progressMillis) { - this.progressMillis = progressMillis; - } - - /** - * The state will be considered valid, and thus be saved, if the progress is more than {@link - * #PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS} or at least 1/4 of the video length. - * @param durationInSeconds the duration of the stream connected with this state, in seconds - * @return whether this stream state entity should be saved or not - */ - public boolean isValid(final long durationInSeconds) { - return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS - || progressMillis > durationInSeconds * 1000 / 4; - } - - /** - * The video will be considered as finished, if the time left is less than {@link - * #PLAYBACK_FINISHED_END_MILLISECONDS} and the progress is at least 3/4 of the video length. - * The state will be saved anyway, so that it can be shown under stream info items, but the - * player will not resume if a state is considered as finished. Finished streams are also the - * ones that can be filtered out in the feed fragment. - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreams() - * @see org.schabi.newpipe.database.feed.dao.FeedDAO#getLiveOrNotPlayedStreamsForGroup(long) - * @param durationInSeconds the duration of the stream connected with this state, in seconds - * @return whether the stream is finished or not - */ - public boolean isFinished(final long durationInSeconds) { - return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS - && progressMillis >= durationInSeconds * 1000 * 3 / 4; - } - - @Override - public boolean equals(@Nullable final Object obj) { - if (obj instanceof StreamStateEntity) { - return ((StreamStateEntity) obj).streamUid == streamUid - && ((StreamStateEntity) obj).progressMillis == progressMillis; - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(streamUid, progressMillis); - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt new file mode 100644 index 000000000..759a2dcec --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/stream/model/StreamStateEntity.kt @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2018-2023 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.stream.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.Companion.CASCADE +import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.JOIN_STREAM_ID +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.PLAYBACK_FINISHED_END_MILLISECONDS +import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_STATE_TABLE + +@Entity( + tableName = STREAM_STATE_TABLE, + primaryKeys = [JOIN_STREAM_ID], + foreignKeys = [ + ForeignKey( + entity = StreamEntity::class, + parentColumns = arrayOf(STREAM_ID), + childColumns = arrayOf(JOIN_STREAM_ID), + onDelete = CASCADE, + onUpdate = CASCADE + ) + ] +) +data class StreamStateEntity( + @ColumnInfo(name = JOIN_STREAM_ID) + val streamUid: Long, + + @ColumnInfo(name = STREAM_PROGRESS_MILLIS) + val progressMillis: Long +) { + /** + * The state will be considered valid, and thus be saved, if the progress is more than + * [PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS] or at least 1/4 of the video length. + * @param durationInSeconds the duration of the stream connected with this state, in seconds + * @return whether this stream state entity should be saved or not + */ + fun isValid(durationInSeconds: Long): Boolean { + return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS || + progressMillis > durationInSeconds * 1000 / 4 + } + + /** + * The video will be considered as finished, if the time left is less than + * [PLAYBACK_FINISHED_END_MILLISECONDS] and the progress is at least 3/4 of the video length. + * The state will be saved anyway, so that it can be shown under stream info items, but the + * player will not resume if a state is considered as finished. Finished streams are also the + * ones that can be filtered out in the feed fragment. + * @param durationInSeconds the duration of the stream connected with this state, in seconds + * @return whether the stream is finished or not + */ + fun isFinished(durationInSeconds: Long): Boolean { + return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS && + progressMillis >= durationInSeconds * 1000 * 3 / 4 + } + + companion object { + const val STREAM_STATE_TABLE = "stream_state" + const val JOIN_STREAM_ID = "stream_id" + + // This additional field is required for the SQL query because 'stream_id' is used + // for some other joins already + const val JOIN_STREAM_ID_ALIAS = "stream_id_alias" + const val STREAM_PROGRESS_MILLIS = "progress_time" + + /** + * Playback state will not be saved, if playback time is less than this threshold + * (5000ms = 5s). + */ + const val PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000L + + /** + * Stream will be considered finished if the playback time left exceeds this threshold + * (60000ms = 60s). + * @see org.schabi.newpipe.database.stream.model.StreamStateEntity.isFinished + */ + const val PLAYBACK_FINISHED_END_MILLISECONDS = 60000L + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java deleted file mode 100644 index 07e0eb7d3..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.schabi.newpipe.database.subscription; - -import androidx.annotation.IntDef; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@IntDef({NotificationMode.DISABLED, NotificationMode.ENABLED}) -@Retention(RetentionPolicy.SOURCE) -public @interface NotificationMode { - - int DISABLED = 0; - int ENABLED = 1; - //other values reserved for the future -} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt new file mode 100644 index 000000000..f9bb18c0c --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/NotificationMode.kt @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.subscription + +import androidx.annotation.IntDef + +@IntDef(NotificationMode.Companion.DISABLED, NotificationMode.Companion.ENABLED) +@Retention(AnnotationRetention.SOURCE) +annotation class NotificationMode { + companion object { + const val DISABLED = 0 + const val ENABLED = 1 // other values reserved for the future + } +} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt index 47b6f4dd9..e6fdcbf70 100644 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionDAO.kt @@ -99,7 +99,7 @@ abstract class SubscriptionDAO : BasicDAO { if (uidFromInsert != -1L) { entity.uid = uidFromInsert } else { - val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url) + val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!) ?: throw IllegalStateException("Subscription cannot be null just after insertion.") entity.uid = subscriptionIdFromDb diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java deleted file mode 100644 index df5a3067a..000000000 --- a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.java +++ /dev/null @@ -1,200 +0,0 @@ -package org.schabi.newpipe.database.subscription; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.room.ColumnInfo; -import androidx.room.Entity; -import androidx.room.Ignore; -import androidx.room.Index; -import androidx.room.PrimaryKey; - -import org.schabi.newpipe.extractor.channel.ChannelInfo; -import org.schabi.newpipe.extractor.channel.ChannelInfoItem; -import org.schabi.newpipe.util.Constants; -import org.schabi.newpipe.util.image.ImageStrategy; - -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_SERVICE_ID; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_TABLE; -import static org.schabi.newpipe.database.subscription.SubscriptionEntity.SUBSCRIPTION_URL; - -@Entity(tableName = SUBSCRIPTION_TABLE, - indices = {@Index(value = {SUBSCRIPTION_SERVICE_ID, SUBSCRIPTION_URL}, unique = true)}) -public class SubscriptionEntity { - public static final String SUBSCRIPTION_UID = "uid"; - public static final String SUBSCRIPTION_TABLE = "subscriptions"; - public static final String SUBSCRIPTION_SERVICE_ID = "service_id"; - public static final String SUBSCRIPTION_URL = "url"; - public static final String SUBSCRIPTION_NAME = "name"; - public static final String SUBSCRIPTION_AVATAR_URL = "avatar_url"; - public static final String SUBSCRIPTION_SUBSCRIBER_COUNT = "subscriber_count"; - public static final String SUBSCRIPTION_DESCRIPTION = "description"; - public static final String SUBSCRIPTION_NOTIFICATION_MODE = "notification_mode"; - - @PrimaryKey(autoGenerate = true) - private long uid = 0; - - @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID) - private int serviceId = Constants.NO_SERVICE_ID; - - @ColumnInfo(name = SUBSCRIPTION_URL) - private String url; - - @ColumnInfo(name = SUBSCRIPTION_NAME) - private String name; - - @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL) - private String avatarUrl; - - @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT) - private Long subscriberCount; - - @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) - private String description; - - @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE) - private int notificationMode; - - @Ignore - public static SubscriptionEntity from(@NonNull final ChannelInfo info) { - final SubscriptionEntity result = new SubscriptionEntity(); - result.setServiceId(info.getServiceId()); - result.setUrl(info.getUrl()); - result.setData(info.getName(), ImageStrategy.imageListToDbUrl(info.getAvatars()), - info.getDescription(), info.getSubscriberCount()); - return result; - } - - public long getUid() { - return uid; - } - - public void setUid(final long uid) { - this.uid = uid; - } - - public int getServiceId() { - return serviceId; - } - - public void setServiceId(final int serviceId) { - this.serviceId = serviceId; - } - - public String getUrl() { - return url; - } - - public void setUrl(final String url) { - this.url = url; - } - - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - @Nullable - public String getAvatarUrl() { - return avatarUrl; - } - - public void setAvatarUrl(@Nullable final String avatarUrl) { - this.avatarUrl = avatarUrl; - } - - public Long getSubscriberCount() { - return subscriberCount; - } - - public void setSubscriberCount(final Long subscriberCount) { - this.subscriberCount = subscriberCount; - } - - public String getDescription() { - return description; - } - - public void setDescription(final String description) { - this.description = description; - } - - @NotificationMode - public int getNotificationMode() { - return notificationMode; - } - - public void setNotificationMode(@NotificationMode final int notificationMode) { - this.notificationMode = notificationMode; - } - - @Ignore - public void setData(final String n, final String au, final String d, final Long sc) { - this.setName(n); - this.setAvatarUrl(au); - this.setDescription(d); - this.setSubscriberCount(sc); - } - - @Ignore - public ChannelInfoItem toChannelInfoItem() { - final ChannelInfoItem item = new ChannelInfoItem(getServiceId(), getUrl(), getName()); - item.setThumbnails(ImageStrategy.dbUrlToImageList(getAvatarUrl())); - item.setSubscriberCount(getSubscriberCount()); - item.setDescription(getDescription()); - return item; - } - - - // TODO: Remove these generated methods by migrating this class to a data class from Kotlin. - @Override - @SuppressWarnings("EqualsReplaceableByObjectsCall") - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - final SubscriptionEntity that = (SubscriptionEntity) o; - - if (uid != that.uid) { - return false; - } - if (serviceId != that.serviceId) { - return false; - } - if (!url.equals(that.url)) { - return false; - } - if (name != null ? !name.equals(that.name) : that.name != null) { - return false; - } - if (avatarUrl != null ? !avatarUrl.equals(that.avatarUrl) : that.avatarUrl != null) { - return false; - } - if (subscriberCount != null - ? !subscriberCount.equals(that.subscriberCount) - : that.subscriberCount != null) { - return false; - } - return description != null - ? description.equals(that.description) - : that.description == null; - } - - @Override - public int hashCode() { - int result = (int) (uid ^ (uid >>> 32)); - result = 31 * result + serviceId; - result = 31 * result + url.hashCode(); - result = 31 * result + (name != null ? name.hashCode() : 0); - result = 31 * result + (avatarUrl != null ? avatarUrl.hashCode() : 0); - result = 31 * result + (subscriberCount != null ? subscriberCount.hashCode() : 0); - result = 31 * result + (description != null ? description.hashCode() : 0); - return result; - } -} diff --git a/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt new file mode 100644 index 000000000..7df9830e4 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/database/subscription/SubscriptionEntity.kt @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2017-2024 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.database.subscription + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.PrimaryKey +import org.schabi.newpipe.extractor.channel.ChannelInfo +import org.schabi.newpipe.extractor.channel.ChannelInfoItem +import org.schabi.newpipe.util.NO_SERVICE_ID +import org.schabi.newpipe.util.image.ImageStrategy + +@Entity( + tableName = SubscriptionEntity.Companion.SUBSCRIPTION_TABLE, + indices = [ + Index( + value = [SubscriptionEntity.Companion.SUBSCRIPTION_SERVICE_ID, SubscriptionEntity.Companion.SUBSCRIPTION_URL], + unique = true + ) + ] +) +data class SubscriptionEntity( + @PrimaryKey(autoGenerate = true) + var uid: Long = 0, + + @ColumnInfo(name = SUBSCRIPTION_SERVICE_ID) + var serviceId: Int = NO_SERVICE_ID, + + @ColumnInfo(name = SUBSCRIPTION_URL) + var url: String? = null, + + @ColumnInfo(name = SUBSCRIPTION_NAME) + var name: String? = null, + + @ColumnInfo(name = SUBSCRIPTION_AVATAR_URL) + var avatarUrl: String? = null, + + @ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT) + var subscriberCount: Long? = null, + + @ColumnInfo(name = SUBSCRIPTION_DESCRIPTION) + var description: String? = null, + + @get:NotificationMode + @ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE) + var notificationMode: Int = 0 +) { + @Ignore + fun toChannelInfoItem(): ChannelInfoItem { + return ChannelInfoItem(this.serviceId, this.url, this.name).apply { + thumbnails = ImageStrategy.dbUrlToImageList(this@SubscriptionEntity.avatarUrl) + subscriberCount = this@SubscriptionEntity.subscriberCount ?: -1 + description = this@SubscriptionEntity.description + } + } + + companion object { + const val SUBSCRIPTION_UID: String = "uid" + const val SUBSCRIPTION_TABLE: String = "subscriptions" + const val SUBSCRIPTION_SERVICE_ID: String = "service_id" + const val SUBSCRIPTION_URL: String = "url" + const val SUBSCRIPTION_NAME: String = "name" + const val SUBSCRIPTION_AVATAR_URL: String = "avatar_url" + const val SUBSCRIPTION_SUBSCRIBER_COUNT: String = "subscriber_count" + const val SUBSCRIPTION_DESCRIPTION: String = "description" + const val SUBSCRIPTION_NOTIFICATION_MODE: String = "notification_mode" + + @JvmStatic + @Ignore + fun from(info: ChannelInfo): SubscriptionEntity { + return SubscriptionEntity( + serviceId = info.serviceId, + url = info.url, + name = info.name, + avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars), + description = info.description, + subscriberCount = info.subscriberCount + ) + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 003aa5893..741bda246 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -389,8 +389,7 @@ public class DownloadDialog extends DialogFragment } }, throwable -> ErrorUtil.showSnackbar(context, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, - "Downloading video stream size", - currentInfo.getServiceId())))); + "Downloading video stream size", currentInfo)))); disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(getWrappedAudioStreams()) .subscribe(result -> { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() @@ -399,8 +398,7 @@ public class DownloadDialog extends DialogFragment } }, throwable -> ErrorUtil.showSnackbar(context, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, - "Downloading audio stream size", - currentInfo.getServiceId())))); + "Downloading audio stream size", currentInfo)))); disposables.add(StreamInfoWrapper.fetchMoreInfoForWrapper(wrappedSubtitleStreams) .subscribe(result -> { if (dialogBinding.videoAudioGroup.getCheckedRadioButtonId() @@ -409,8 +407,7 @@ public class DownloadDialog extends DialogFragment } }, throwable -> ErrorUtil.showSnackbar(context, new ErrorInfo(throwable, UserAction.DOWNLOAD_OPEN_DIALOG, - "Downloading subtitle stream size", - currentInfo.getServiceId())))); + "Downloading subtitle stream size", currentInfo)))); } private void setupAudioTrackSpinner() { @@ -1136,7 +1133,7 @@ public class DownloadDialog extends DialogFragment } DownloadManagerService.startMission(context, urls, storage, kind, threads, - currentInfo.getUrl(), psName, psArgs, nearLength, new ArrayList<>(recoveryInfo)); + currentInfo, psName, psArgs, nearLength, new ArrayList<>(recoveryInfo)); Toast.makeText(context, getString(R.string.download_has_started), Toast.LENGTH_SHORT).show(); diff --git a/app/src/main/java/org/schabi/newpipe/error/AcraReportSender.java b/app/src/main/java/org/schabi/newpipe/error/AcraReportSender.java index 4d9966364..90d8f4797 100644 --- a/app/src/main/java/org/schabi/newpipe/error/AcraReportSender.java +++ b/app/src/main/java/org/schabi/newpipe/error/AcraReportSender.java @@ -36,8 +36,8 @@ public class AcraReportSender implements ReportSender { ErrorUtil.openActivity(context, new ErrorInfo( new String[]{report.getString(ReportField.STACK_TRACE)}, UserAction.UI_ERROR, - ErrorInfo.SERVICE_NONE, "ACRA report", + null, R.string.app_ui_crash)); } } diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java index a07b9b0b5..160dcca4d 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java @@ -115,7 +115,7 @@ public class ErrorActivity extends AppCompatActivity { // normal bugreport buildInfo(errorInfo); - activityErrorBinding.errorMessageView.setText(errorInfo.getMessageStringId()); + activityErrorBinding.errorMessageView.setText(errorInfo.getMessage(this)); activityErrorBinding.errorView.setText(formErrorText(errorInfo.getStackTraces())); // print stack trace once again for debugging: diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt index 6d8c1bd63..609fbb336 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt @@ -1,115 +1,304 @@ package org.schabi.newpipe.error +import android.content.Context import android.os.Parcelable import androidx.annotation.StringRes +import androidx.core.content.ContextCompat import com.google.android.exoplayer2.ExoPlaybackException -import kotlinx.parcelize.IgnoredOnParcel +import com.google.android.exoplayer2.upstream.HttpDataSource +import com.google.android.exoplayer2.upstream.Loader import kotlinx.parcelize.Parcelize import org.schabi.newpipe.R import org.schabi.newpipe.extractor.Info +import org.schabi.newpipe.extractor.ServiceList +import org.schabi.newpipe.extractor.ServiceList.YouTube import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException +import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException import org.schabi.newpipe.extractor.exceptions.ExtractionException +import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException +import org.schabi.newpipe.extractor.exceptions.PaidContentException +import org.schabi.newpipe.extractor.exceptions.PrivateContentException +import org.schabi.newpipe.extractor.exceptions.ReCaptchaException +import org.schabi.newpipe.extractor.exceptions.SignInConfirmNotBotException +import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException +import org.schabi.newpipe.extractor.exceptions.UnsupportedContentInCountryException +import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException import org.schabi.newpipe.ktx.isNetworkRelated -import org.schabi.newpipe.util.ServiceHelper +import org.schabi.newpipe.player.mediasource.FailedMediaSource +import org.schabi.newpipe.player.resolver.PlaybackResolver +import java.net.UnknownHostException +/** + * An error has occurred in the app. This class contains plain old parcelable data that can be used + * to report the error and to show it to the user along with correct action buttons. + */ @Parcelize -class ErrorInfo( +class ErrorInfo private constructor( val stackTraces: Array, val userAction: UserAction, - val serviceName: String, val request: String, - val messageStringId: Int + val serviceId: Int?, + private val message: ErrorMessage, + /** + * If `true`, a report button will be shown for this error. Otherwise the error is not something + * that can really be reported (e.g. a network issue, or content not being available at all). + */ + val isReportable: Boolean, + /** + * If `true`, the process causing this error can be retried, otherwise not. + */ + val isRetryable: Boolean, + /** + * If present, indicates that the exception was a ReCaptchaException, and this is the URL + * provided by the service that can be used to solve the ReCaptcha challenge. + */ + val recaptchaUrl: String?, + /** + * If present, this resource can alternatively be opened in browser (useful if NewPipe is + * badly broken). + */ + val openInBrowserUrl: String?, ) : Parcelable { - // no need to store throwable, all data for report is in other variables - // also, the throwable might not be serializable, see TeamNewPipe/NewPipe#7302 - @IgnoredOnParcel - var throwable: Throwable? = null - - private constructor( + @JvmOverloads + constructor( throwable: Throwable, userAction: UserAction, - serviceName: String, - request: String + request: String, + serviceId: Int? = null, + openInBrowserUrl: String? = null, ) : this( throwableToStringList(throwable), userAction, - serviceName, request, - getMessageStringId(throwable, userAction) - ) { - this.throwable = throwable - } + serviceId, + getMessage(throwable, userAction, serviceId), + isReportable(throwable), + isRetryable(throwable), + (throwable as? ReCaptchaException)?.url, + openInBrowserUrl, + ) - private constructor( - throwable: List, + @JvmOverloads + constructor( + throwables: List, userAction: UserAction, - serviceName: String, - request: String + request: String, + serviceId: Int? = null, + openInBrowserUrl: String? = null, ) : this( - throwableListToStringList(throwable), + throwableListToStringList(throwables), userAction, - serviceName, request, - getMessageStringId(throwable.firstOrNull(), userAction) - ) { - this.throwable = throwable.firstOrNull() + serviceId, + getMessage(throwables.firstOrNull(), userAction, serviceId), + throwables.any(::isReportable), + throwables.isEmpty() || throwables.any(::isRetryable), + throwables.firstNotNullOfOrNull { it as? ReCaptchaException }?.url, + openInBrowserUrl, + ) + + // constructor to manually build ErrorInfo when no throwable is available + constructor( + stackTraces: Array, + userAction: UserAction, + request: String, + serviceId: Int?, + @StringRes message: Int + ) : + this( + stackTraces, userAction, request, serviceId, ErrorMessage(message), + true, false, null, null + ) + + // constructor with only one throwable to extract service id and openInBrowserUrl from an Info + constructor( + throwable: Throwable, + userAction: UserAction, + request: String, + info: Info?, + ) : + this(throwable, userAction, request, info?.serviceId, info?.url) + + // constructor with multiple throwables to extract service id and openInBrowserUrl from an Info + constructor( + throwables: List, + userAction: UserAction, + request: String, + info: Info?, + ) : + this(throwables, userAction, request, info?.serviceId, info?.url) + + fun getServiceName(): String { + return getServiceName(serviceId) } - // constructors with single throwable - constructor(throwable: Throwable, userAction: UserAction, request: String) : - this(throwable, userAction, SERVICE_NONE, request) - constructor(throwable: Throwable, userAction: UserAction, request: String, serviceId: Int) : - this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request) - constructor(throwable: Throwable, userAction: UserAction, request: String, info: Info?) : - this(throwable, userAction, getInfoServiceName(info), request) - - // constructors with list of throwables - constructor(throwable: List, userAction: UserAction, request: String) : - this(throwable, userAction, SERVICE_NONE, request) - constructor(throwable: List, userAction: UserAction, request: String, serviceId: Int) : - this(throwable, userAction, ServiceHelper.getNameOfServiceById(serviceId), request) - constructor(throwable: List, userAction: UserAction, request: String, info: Info?) : - this(throwable, userAction, getInfoServiceName(info), request) + fun getMessage(context: Context): String { + return message.getString(context) + } companion object { - const val SERVICE_NONE = "none" + @Parcelize + class ErrorMessage( + @StringRes + private val stringRes: Int, + private vararg val formatArgs: String, + ) : Parcelable { + fun getString(context: Context): String { + return if (formatArgs.isEmpty()) { + // use ContextCompat.getString() just in case context is not AppCompatActivity + ContextCompat.getString(context, stringRes) + } else { + // ContextCompat.getString() with formatArgs does not exist, so we just + // replicate its source code but with formatArgs + ContextCompat.getContextForLanguage(context).getString(stringRes, *formatArgs) + } + } + } + + const val SERVICE_NONE = "" + + private fun getServiceName(serviceId: Int?) = + // not using getNameOfServiceById since we want to accept a nullable serviceId and we + // want to default to SERVICE_NONE + ServiceList.all()?.firstOrNull { it.serviceId == serviceId }?.serviceInfo?.name + ?: SERVICE_NONE fun throwableToStringList(throwable: Throwable) = arrayOf(throwable.stackTraceToString()) fun throwableListToStringList(throwableList: List) = throwableList.map { it.stackTraceToString() }.toTypedArray() - private fun getInfoServiceName(info: Info?) = - if (info == null) SERVICE_NONE else ServiceHelper.getNameOfServiceById(info.serviceId) - - @StringRes - private fun getMessageStringId( + fun getMessage( throwable: Throwable?, - action: UserAction - ): Int { + action: UserAction?, + serviceId: Int?, + ): ErrorMessage { return when { - throwable is AccountTerminatedException -> R.string.account_terminated - throwable is ContentNotAvailableException -> R.string.content_not_available - throwable != null && throwable.isNetworkRelated -> R.string.network_error - throwable is ContentNotSupportedException -> R.string.content_not_supported - throwable is ExtractionException -> R.string.parsing_error + // player exceptions + // some may be IOException, so do these checks before isNetworkRelated! throwable is ExoPlaybackException -> { - when (throwable.type) { - ExoPlaybackException.TYPE_SOURCE -> R.string.player_stream_failure - ExoPlaybackException.TYPE_UNEXPECTED -> R.string.player_recoverable_failure - else -> R.string.player_unrecoverable_failure + val cause = throwable.cause + when { + cause is HttpDataSource.InvalidResponseCodeException -> { + if (cause.responseCode == 403) { + if (serviceId == YouTube.serviceId) { + ErrorMessage(R.string.youtube_player_http_403) + } else { + ErrorMessage(R.string.player_http_403) + } + } else { + ErrorMessage(R.string.player_http_invalid_status, cause.responseCode.toString()) + } + } + cause is Loader.UnexpectedLoaderException && cause.cause is ExtractionException -> + getMessage(throwable, action, serviceId) + throwable.type == ExoPlaybackException.TYPE_SOURCE -> + ErrorMessage(R.string.player_stream_failure) + throwable.type == ExoPlaybackException.TYPE_UNEXPECTED -> + ErrorMessage(R.string.player_recoverable_failure) + else -> + ErrorMessage(R.string.player_unrecoverable_failure) } } - action == UserAction.UI_ERROR -> R.string.app_ui_crash - action == UserAction.REQUESTED_COMMENTS -> R.string.error_unable_to_load_comments - action == UserAction.SUBSCRIPTION_CHANGE -> R.string.subscription_change_failed - action == UserAction.SUBSCRIPTION_UPDATE -> R.string.subscription_update_failed - action == UserAction.LOAD_IMAGE -> R.string.could_not_load_thumbnails - action == UserAction.DOWNLOAD_OPEN_DIALOG -> R.string.could_not_setup_download_menu - else -> R.string.general_error + throwable is FailedMediaSource.FailedMediaSourceException -> + getMessage(throwable.cause, action, serviceId) + throwable is PlaybackResolver.ResolverException -> + ErrorMessage(R.string.player_stream_failure) + + // content not available exceptions + throwable is AccountTerminatedException -> + throwable.message + ?.takeIf { reason -> !reason.isEmpty() } + ?.let { reason -> + ErrorMessage( + R.string.account_terminated_service_provides_reason, + getServiceName(serviceId), + reason + ) + } + ?: ErrorMessage(R.string.account_terminated) + throwable is AgeRestrictedContentException -> + ErrorMessage(R.string.restricted_video_no_stream) + throwable is GeographicRestrictionException -> + ErrorMessage(R.string.georestricted_content) + throwable is PaidContentException -> + ErrorMessage(R.string.paid_content) + throwable is PrivateContentException -> + ErrorMessage(R.string.private_content) + throwable is SoundCloudGoPlusContentException -> + ErrorMessage(R.string.soundcloud_go_plus_content) + throwable is UnsupportedContentInCountryException -> + ErrorMessage(R.string.unsupported_content_in_country) + throwable is YoutubeMusicPremiumContentException -> + ErrorMessage(R.string.youtube_music_premium_content) + throwable is SignInConfirmNotBotException -> + ErrorMessage(R.string.sign_in_confirm_not_bot_error, getServiceName(serviceId)) + throwable is ContentNotAvailableException -> + ErrorMessage(R.string.content_not_available) + + // other extractor exceptions + throwable is ContentNotSupportedException -> + ErrorMessage(R.string.content_not_supported) + // ReCaptchas will be handled in a special way anyway + throwable is ReCaptchaException -> + ErrorMessage(R.string.recaptcha_request_toast) + // test this at the end as many exceptions could be a subclass of IOException + throwable != null && throwable.isNetworkRelated -> + ErrorMessage(R.string.network_error) + // an extraction exception unrelated to the network + // is likely an issue with parsing the website + throwable is ExtractionException -> + ErrorMessage(R.string.parsing_error) + + // user actions (in case the exception is null or unrecognizable) + action == UserAction.UI_ERROR -> + ErrorMessage(R.string.app_ui_crash) + action == UserAction.REQUESTED_COMMENTS -> + ErrorMessage(R.string.error_unable_to_load_comments) + action == UserAction.SUBSCRIPTION_CHANGE -> + ErrorMessage(R.string.subscription_change_failed) + action == UserAction.SUBSCRIPTION_UPDATE -> + ErrorMessage(R.string.subscription_update_failed) + action == UserAction.LOAD_IMAGE -> + ErrorMessage(R.string.could_not_load_thumbnails) + action == UserAction.DOWNLOAD_OPEN_DIALOG -> + ErrorMessage(R.string.could_not_setup_download_menu) + else -> + ErrorMessage(R.string.error_snackbar_message) + } + } + + fun isReportable(throwable: Throwable?): Boolean { + return when (throwable) { + // we don't have an exception, so this is a manually built error, which likely + // indicates that it's important and is thus reportable + null -> true + // the service explicitly said that content is not available (e.g. age restrictions, + // video deleted, etc.), there is no use in letting users report it + is ContentNotAvailableException -> false + // we know the content is not supported, no need to let the user report it + is ContentNotSupportedException -> false + // happens often when there is no internet connection; we don't use + // `throwable.isNetworkRelated` since any `IOException` would make that function + // return true, but not all `IOException`s are network related + is UnknownHostException -> false + // by default, this is an unexpected exception, which the user could report + else -> true + } + } + + fun isRetryable(throwable: Throwable?): Boolean { + return when (throwable) { + // we know the content is not available, retrying won't help + is ContentNotAvailableException -> false + // we know the content is not supported, retrying won't help + is ContentNotSupportedException -> false + // by default (including if throwable is null), enable retrying (though the retry + // button will be shown only if a way to perform the retry is implemented) + else -> true } } } diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt index 14ec41148..4ec5f58c3 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt @@ -2,7 +2,6 @@ package org.schabi.newpipe.error import android.content.Context import android.content.Intent -import android.util.Log import android.view.View import android.widget.Button import android.widget.TextView @@ -14,21 +13,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import org.schabi.newpipe.MainActivity import org.schabi.newpipe.R -import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException -import org.schabi.newpipe.extractor.exceptions.AgeRestrictedContentException -import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException -import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException -import org.schabi.newpipe.extractor.exceptions.GeographicRestrictionException -import org.schabi.newpipe.extractor.exceptions.PaidContentException -import org.schabi.newpipe.extractor.exceptions.PrivateContentException -import org.schabi.newpipe.extractor.exceptions.ReCaptchaException -import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException -import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException -import org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty import org.schabi.newpipe.ktx.animate -import org.schabi.newpipe.ktx.isInterruptedCaused -import org.schabi.newpipe.ktx.isNetworkRelated -import org.schabi.newpipe.util.ServiceHelper import org.schabi.newpipe.util.external_communication.ShareUtils import java.util.concurrent.TimeUnit @@ -78,64 +63,32 @@ class ErrorPanelHelper( } fun showError(errorInfo: ErrorInfo) { - - if (errorInfo.throwable != null && errorInfo.throwable!!.isInterruptedCaused) { - if (DEBUG) { - Log.w(TAG, "onError() isInterruptedCaused! = [$errorInfo.throwable]") - } - return - } - ensureDefaultVisibility() + errorTextView.text = errorInfo.getMessage(context) - if (errorInfo.throwable is ReCaptchaException) { - errorTextView.setText(R.string.recaptcha_request_toast) - - showAndSetErrorButtonAction( - R.string.recaptcha_solve - ) { + if (errorInfo.recaptchaUrl != null) { + showAndSetErrorButtonAction(R.string.recaptcha_solve) { // Starting ReCaptcha Challenge Activity val intent = Intent(context, ReCaptchaActivity::class.java) - intent.putExtra( - ReCaptchaActivity.RECAPTCHA_URL_EXTRA, - (errorInfo.throwable as ReCaptchaException).url - ) + intent.putExtra(ReCaptchaActivity.RECAPTCHA_URL_EXTRA, errorInfo.recaptchaUrl) fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST) errorActionButton.setOnClickListener(null) } - - errorRetryButton.isVisible = retryShouldBeShown - showAndSetOpenInBrowserButtonAction(errorInfo) - } else if (errorInfo.throwable is AccountTerminatedException) { - errorTextView.setText(R.string.account_terminated) - - if (!isNullOrEmpty((errorInfo.throwable as AccountTerminatedException).message)) { - errorServiceInfoTextView.text = context.resources.getString( - R.string.service_provides_reason, - ServiceHelper.getSelectedService(context)?.serviceInfo?.name ?: "" - ) - errorServiceInfoTextView.isVisible = true - - errorServiceExplanationTextView.text = - (errorInfo.throwable as AccountTerminatedException).message - errorServiceExplanationTextView.isVisible = true - } - } else { - showAndSetErrorButtonAction( - R.string.error_snackbar_action - ) { + } else if (errorInfo.isReportable) { + showAndSetErrorButtonAction(R.string.error_snackbar_action) { ErrorUtil.openActivity(context, errorInfo) } + } - errorTextView.setText(getExceptionDescription(errorInfo.throwable)) + if (errorInfo.isRetryable) { + errorRetryButton.isVisible = retryShouldBeShown + } - if (errorInfo.throwable !is ContentNotAvailableException && - errorInfo.throwable !is ContentNotSupportedException - ) { - // show retry button only for content which is not unavailable or unsupported - errorRetryButton.isVisible = retryShouldBeShown + if (errorInfo.openInBrowserUrl != null) { + errorOpenInBrowserButton.isVisible = true + errorOpenInBrowserButton.setOnClickListener { + ShareUtils.openUrlInBrowser(context, errorInfo.openInBrowserUrl) } - showAndSetOpenInBrowserButtonAction(errorInfo) } setRootVisible() @@ -153,15 +106,6 @@ class ErrorPanelHelper( errorActionButton.setOnClickListener(listener) } - fun showAndSetOpenInBrowserButtonAction( - errorInfo: ErrorInfo - ) { - errorOpenInBrowserButton.isVisible = true - errorOpenInBrowserButton.setOnClickListener { - ShareUtils.openUrlInBrowser(context, errorInfo.request) - } - } - fun showTextError(errorString: String) { ensureDefaultVisibility() @@ -192,27 +136,5 @@ class ErrorPanelHelper( companion object { val TAG: String = ErrorPanelHelper::class.simpleName!! val DEBUG: Boolean = MainActivity.DEBUG - - @StringRes - fun getExceptionDescription(throwable: Throwable?): Int { - return when (throwable) { - is AgeRestrictedContentException -> R.string.restricted_video_no_stream - is GeographicRestrictionException -> R.string.georestricted_content - is PaidContentException -> R.string.paid_content - is PrivateContentException -> R.string.private_content - is SoundCloudGoPlusContentException -> R.string.soundcloud_go_plus_content - is YoutubeMusicPremiumContentException -> R.string.youtube_music_premium_content - is ContentNotAvailableException -> R.string.content_not_available - is ContentNotSupportedException -> R.string.content_not_supported - else -> { - // show retry button only for content which is not unavailable or unsupported - if (throwable != null && throwable.isNetworkRelated) { - R.string.network_error - } else { - R.string.error_snackbar_message - } - } - } - } } } diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt index 93dd8e522..b358a5fd2 100644 --- a/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt +++ b/app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt @@ -10,6 +10,7 @@ import android.widget.Toast import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.google.android.material.snackbar.Snackbar @@ -121,7 +122,7 @@ class ErrorUtil { ) .setSmallIcon(R.drawable.ic_bug_report) .setContentTitle(context.getString(R.string.error_report_notification_title)) - .setContentText(context.getString(errorInfo.messageStringId)) + .setContentText(errorInfo.getMessage(context)) .setAutoCancel(true) .setContentIntent( PendingIntentCompat.getActivity( @@ -136,9 +137,11 @@ class ErrorUtil { NotificationManagerCompat.from(context) .notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build()) - // since the notification is silent, also show a toast, otherwise the user is confused - Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT) - .show() + ContextCompat.getMainExecutor(context).execute { + // since the notification is silent, also show a toast, otherwise the user is confused + Toast.makeText(context, R.string.error_report_notification_toast, Toast.LENGTH_SHORT) + .show() + } } private fun getErrorActivityIntent(context: Context, errorInfo: ErrorInfo): Intent { @@ -153,10 +156,10 @@ class ErrorUtil { // fallback to showing a notification if no root view is available createNotification(context, errorInfo) } else { - Snackbar.make(rootView, R.string.error_snackbar_message, Snackbar.LENGTH_LONG) + Snackbar.make(rootView, errorInfo.getMessage(context), Snackbar.LENGTH_LONG) .setActionTextColor(Color.YELLOW) .setAction(context.getString(R.string.error_snackbar_action).uppercase()) { - openActivity(context, errorInfo) + context.startActivity(getErrorActivityIntent(context, errorInfo)) }.show() } } diff --git a/app/src/main/java/org/schabi/newpipe/error/UserAction.java b/app/src/main/java/org/schabi/newpipe/error/UserAction.java index afb880a29..d3af9d32e 100644 --- a/app/src/main/java/org/schabi/newpipe/error/UserAction.java +++ b/app/src/main/java/org/schabi/newpipe/error/UserAction.java @@ -33,7 +33,9 @@ public enum UserAction { SHARE_TO_NEWPIPE("share to newpipe"), CHECK_FOR_NEW_APP_VERSION("check for new app version"), OPEN_INFO_ITEM_DIALOG("open info item dialog"), - GETTING_MAIN_SCREEN_TAB("getting main screen tab"); + GETTING_MAIN_SCREEN_TAB("getting main screen tab"), + PLAY_ON_POPUP("play on popup"), + SUBSCRIPTIONS("loading subscriptions"); private final String message; diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 083d1fe05..9bffa149c 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -93,6 +93,7 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.playlist.LocalPlaylistFragment; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.PlayerIntentType; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.event.OnKeyDownListener; @@ -205,6 +206,8 @@ public final class VideoDetailFragment int lastStableBottomSheetState = BottomSheetBehavior.STATE_EXPANDED; @State protected boolean autoPlayEnabled = true; + @State + protected int originalOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; @Nullable private StreamInfo currentInfo = null; @@ -876,7 +879,7 @@ public final class VideoDetailFragment } } }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, - url == null ? "no url" : url, serviceId))); + url == null ? "no url" : url, serviceId, url))); } /*////////////////////////////////////////////////////////////////////////// @@ -1166,8 +1169,12 @@ public final class VideoDetailFragment final PlayQueue queue = setupPlayQueueForIntent(false); tryAddVideoPlayerView(); - final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), - PlayerService.class, queue, true, autoPlayEnabled); + final Context context = requireContext(); + final Intent playerIntent = + NavigationHelper.getPlayerIntent(context, PlayerService.class, queue, + PlayerIntentType.AllOthers) + .putExtra(Player.PLAY_WHEN_READY, autoPlayEnabled) + .putExtra(Player.RESUME_PLAYBACK, true); ContextCompat.startForegroundService(activity, playerIntent); } @@ -1225,7 +1232,13 @@ public final class VideoDetailFragment disposables.add(recordManager.onViewed(info).onErrorComplete() .subscribe( ignored -> { /* successful */ }, - error -> Log.e(TAG, "Register view failure: ", error) + error -> showSnackBarError( + new ErrorInfo( + error, + UserAction.PLAY_STREAM, + "Got an error when modifying history on viewed" + ) + ) )); } @@ -1411,10 +1424,8 @@ public final class VideoDetailFragment bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); } // Rebound to the service if it was closed via notification or mini player - if (!playerHolder.isBound()) { - playerHolder.startService( - false, VideoDetailFragment.this); - } + playerHolder.setListener(VideoDetailFragment.this); + playerHolder.tryBindIfNeeded(context); break; } } @@ -1423,7 +1434,8 @@ public final class VideoDetailFragment intentFilter.addAction(ACTION_SHOW_MAIN_PLAYER); intentFilter.addAction(ACTION_HIDE_MAIN_PLAYER); intentFilter.addAction(ACTION_PLAYER_STARTED); - activity.registerReceiver(broadcastReceiver, intentFilter); + ContextCompat.registerReceiver(activity, broadcastReceiver, intentFilter, + ContextCompat.RECEIVER_EXPORTED); } @@ -1592,8 +1604,8 @@ public final class VideoDetailFragment } if (!info.getErrors().isEmpty()) { - showSnackBarError(new ErrorInfo(info.getErrors(), - UserAction.REQUESTED_STREAM, info.getUrl(), info)); + showSnackBarError(new ErrorInfo(info.getErrors(), UserAction.REQUESTED_STREAM, + "Some info not extracted: " + info.getUrl(), info)); } } @@ -1896,22 +1908,29 @@ public final class VideoDetailFragment @Override public void onScreenRotationButtonClicked() { - // In tablet user experience will be better if screen will not be rotated - // from landscape to portrait every time. - // Just turn on fullscreen mode in landscape orientation - // or portrait & unlocked global orientation - final boolean isLandscape = DeviceUtils.isLandscape(requireContext()); - if (DeviceUtils.isTablet(activity) - && (!globalScreenOrientationLocked(activity) || isLandscape)) { - player.UIs().get(MainPlayerUi.class).ifPresent(MainPlayerUi::toggleFullscreen); + final Optional playerUi = player != null + ? player.UIs().get(MainPlayerUi.class) + : Optional.empty(); + if (playerUi.isEmpty()) { return; } - final int newOrientation = isLandscape - ? ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; + // On tablets and TVs, just toggle fullscreen UI without orientation change. + if (DeviceUtils.isTablet(activity) || DeviceUtils.isTv(activity)) { + playerUi.get().toggleFullscreen(); + return; + } - activity.setRequestedOrientation(newOrientation); + if (playerUi.get().isFullscreen()) { + // EXITING FULLSCREEN + playerUi.get().toggleFullscreen(); + activity.setRequestedOrientation(originalOrientation); + } else { + // ENTERING FULLSCREEN + originalOrientation = activity.getRequestedOrientation(); + playerUi.get().toggleFullscreen(); + activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE); + } } /* diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java index 7f594734a..848dfe6f5 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/BaseListInfoFragment.java @@ -153,7 +153,7 @@ public abstract class BaseListInfoFragment showError(new ErrorInfo(throwable, errorUserAction, - "Start loading: " + url, serviceId))); + "Start loading: " + url, serviceId, url))); } /** @@ -184,7 +184,7 @@ public abstract class BaseListInfoFragment dynamicallyShowErrorPanelOrSnackbar(new ErrorInfo(throwable, - errorUserAction, "Loading more items: " + url, serviceId))); + errorUserAction, "Loading more items: " + url, serviceId, url))); } private void forbidDownwardFocusScroll() { @@ -210,7 +210,7 @@ public abstract class BaseListInfoFragment 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)); @@ -577,7 +577,7 @@ public class ChannelFragment extends BaseStateFragment isLoading.set(false); handleResult(result); }, throwable -> showError(new ErrorInfo(throwable, UserAction.REQUESTED_CHANNEL, - url == null ? "No URL" : url, serviceId))); + url == null ? "No URL" : url, serviceId, url))); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java index cea06b942..8cb5f6497 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java @@ -54,6 +54,7 @@ import org.schabi.newpipe.extractor.MetaInfo; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.search.SearchExtractor; import org.schabi.newpipe.extractor.search.SearchInfo; import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory; @@ -934,7 +935,21 @@ public class SearchFragment extends BaseListFragment { + private static final class SuggestionItemCallback + extends DiffUtil.ItemCallback { @Override public boolean areItemsTheSame(@NonNull final SuggestionItem oldItem, @NonNull final SuggestionItem newItem) { diff --git a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java index c7ac9556f..a2bf4a1ff 100644 --- a/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java +++ b/app/src/main/java/org/schabi/newpipe/info_list/dialog/StreamDialogDefaultEntry.java @@ -13,6 +13,9 @@ import androidx.annotation.StringRes; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.download.DownloadDialog; +import org.schabi.newpipe.error.ErrorInfo; +import org.schabi.newpipe.error.ErrorUtil; +import org.schabi.newpipe.error.UserAction; import org.schabi.newpipe.local.dialog.PlaylistAppendDialog; import org.schabi.newpipe.local.dialog.PlaylistDialog; import org.schabi.newpipe.local.history.HistoryRecordManager; @@ -132,6 +135,16 @@ public enum StreamDialogDefaultEntry { MARK_AS_WATCHED(R.string.mark_as_watched, (fragment, item) -> new HistoryRecordManager(fragment.getContext()) .markAsWatched(item) + .doOnError(error -> { + ErrorUtil.showSnackbar( + fragment.requireContext(), + new ErrorInfo( + error, + UserAction.OPEN_INFO_ITEM_DIALOG, + "Got an error when trying to mark as watched" + ) + ); + }) .onErrorComplete() .observeOn(AndroidSchedulers.mainThread()) .subscribe() diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java index a5e1594d1..1f3772dd5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java @@ -1,6 +1,7 @@ package org.schabi.newpipe.local.bookmark; import static org.schabi.newpipe.local.bookmark.MergedPlaylistManager.getMergedOrderedPlaylists; +import static org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout; import android.content.DialogInterface; import android.os.Bundle; @@ -140,7 +141,7 @@ public final class BookmarkFragment extends BaseLocalListFragment playlists) { return playlists.stream() - .anyMatch(playlist -> playlist.timesStreamIsContained > 0); + .anyMatch(playlist -> playlist.getTimesStreamIsContained() > 0); } private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, @@ -146,9 +147,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog { @NonNull final List streams) { final String toastText; - if (playlist.timesStreamIsContained > 0) { + if (playlist.getTimesStreamIsContained() > 0) { toastText = getString(R.string.playlist_add_stream_success_duplicate, - playlist.timesStreamIsContained); + playlist.getTimesStreamIsContained()); } else { toastText = getString(R.string.playlist_add_stream_success); } @@ -160,8 +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) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt index ed65d4048..aacc6757e 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt @@ -177,7 +177,7 @@ class FeedDatabaseManager(context: Context) { .observeOn(AndroidSchedulers.mainThread()) } - fun oldestSubscriptionUpdate(groupId: Long): Flowable> { + fun oldestSubscriptionUpdate(groupId: Long): Flowable> { return when (groupId) { FeedGroupEntity.GROUP_ALL_ID -> feedTable.oldestSubscriptionUpdateFromAll() else -> feedTable.oldestSubscriptionUpdate(groupId) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt index 91f98f5d2..bbad7f689 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt @@ -507,7 +507,7 @@ class FeedFragment : BaseStateFragment() { .setTitle(R.string.feed_load_error) .setPositiveButton(R.string.unsubscribe) { _, _ -> SubscriptionManager(requireContext()) - .deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url) + .deleteSubscription(subscriptionEntity.serviceId, subscriptionEntity.url!!) .subscribe() handleItemsErrors(nextItemsErrors) } diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt index 728570b17..f916db2b5 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedViewModel.kt @@ -65,7 +65,7 @@ class FeedViewModel( feedDatabaseManager.oldestSubscriptionUpdate(groupId), Function6 { t1: FeedEventManager.Event, t2: Boolean, t3: Boolean, t4: Boolean, - t5: Long, t6: List -> + t5: Long, t6: List -> return@Function6 CombineResultEventHolder(t1, t2, t3, t4, t5, t6.firstOrNull()) } ) diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt index a40bf35dc..6fe311fb0 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/notifications/NotificationWorker.kt @@ -1,6 +1,8 @@ package org.schabi.newpipe.local.feed.notifications import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build import android.util.Log import androidx.core.app.NotificationCompat import androidx.work.Constraints @@ -83,7 +85,9 @@ class NotificationWorker( .setPriority(NotificationCompat.PRIORITY_LOW) .setContentTitle(applicationContext.getString(R.string.feed_notification_loading)) .build() - setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification)) + // ServiceInfo constants are not used below Android Q, so 0 is set here + val serviceType = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC else 0 + setForegroundAsync(ForegroundInfo(FeedLoadService.NOTIFICATION_ID, notification, serviceType)) } companion object { diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt index f960040de..4aa825ca8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt +++ b/app/src/main/java/org/schabi/newpipe/local/feed/service/FeedLoadService.kt @@ -31,6 +31,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.PendingIntentCompat import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Flowable import io.reactivex.rxjava3.disposables.Disposable @@ -200,7 +201,7 @@ class FeedLoadService : Service() { } } } - registerReceiver(broadcastReceiver, IntentFilter(ACTION_CANCEL)) + ContextCompat.registerReceiver(this, broadcastReceiver, IntentFilter(ACTION_CANCEL), ContextCompat.RECEIVER_NOT_EXPORTED) } // ///////////////////////////////////////////////////////////////////////// diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java index 336f5cfe3..528275d75 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java @@ -35,15 +35,15 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder { } final PlaylistMetadataEntry item = (PlaylistMetadataEntry) localItem; - itemTitleView.setText(item.name); + itemTitleView.setText(item.getOrderingName()); itemStreamCountView.setText(Localization.localizeStreamCountMini( - itemStreamCountView.getContext(), item.streamCount)); + itemStreamCountView.getContext(), item.getStreamCount())); itemUploaderView.setVisibility(View.INVISIBLE); - PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); + PicassoHelper.loadPlaylistThumbnail(item.getThumbnailUrl()).into(itemThumbnailView); if (item instanceof PlaylistDuplicatesEntry - && ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) { + && ((PlaylistDuplicatesEntry) item).getTimesStreamIsContained() > 0) { itemView.setAlpha(GRAYED_OUT_ALPHA); } else { itemView.setAlpha(1.0f); diff --git a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java index 765732063..3a339aec8 100644 --- a/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java +++ b/app/src/main/java/org/schabi/newpipe/local/holder/RemotePlaylistItemHolder.java @@ -34,7 +34,7 @@ public class RemotePlaylistItemHolder extends PlaylistItemHolder { } final PlaylistRemoteEntity item = (PlaylistRemoteEntity) localItem; - itemTitleView.setText(item.getName()); + itemTitleView.setText(item.getOrderingName()); itemStreamCountView.setText(Localization.localizeStreamCountMini( itemStreamCountView.getContext(), item.getStreamCount())); // Here is where the uploader name is set in the bookmarked playlists library diff --git a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java index f5562549c..1efc0a84c 100644 --- a/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistFragment.java @@ -111,7 +111,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment() { @Override public void selected(final LocalItem selectedItem) { - if (selectedItem instanceof PlaylistStreamEntry) { - final StreamEntity item = - ((PlaylistStreamEntry) selectedItem).getStreamEntity(); + if (selectedItem instanceof PlaylistStreamEntry entry) { + final StreamEntity item = entry.getStreamEntity(); NavigationHelper.openVideoDetailFragment(requireContext(), getFM(), item.getServiceId(), item.getUrl(), item.getTitle(), null, false); } @@ -496,6 +495,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment items = itemListAdapter.getItemsList(); final List streamIds = new ArrayList<>(items.size()); for (final LocalItem item : items) { - if (item instanceof PlaylistStreamEntry) { - streamIds.add(((PlaylistStreamEntry) item).getStreamId()); + if (item instanceof PlaylistStreamEntry entry) { + streamIds.add(entry.getStreamId()); } } @@ -767,6 +768,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment { - if (resultServiceIntent != null && getContext() != null) { - getContext().startService(resultServiceIntent); - } + requireContext().startService(resultServiceIntent); dismiss(); }) .create(); @@ -50,11 +45,7 @@ public class ImportConfirmationDialog extends DialogFragment { public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (resultServiceIntent == null) { - throw new IllegalStateException("Result intent is null"); - } - - Bridge.restoreInstanceState(this, savedInstanceState); + resultServiceIntent = requireArguments().getParcelable(EXTRA_RESULT_SERVICE_INTENT); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt index 474add4f4..c0783e812 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionManager.kt @@ -26,7 +26,7 @@ class SubscriptionManager(context: Context) { private val feedDatabaseManager = FeedDatabaseManager(context) fun subscriptionTable(): SubscriptionDAO = subscriptionTable - fun subscriptions() = subscriptionTable.all + fun subscriptions() = subscriptionTable.getAll() fun getSubscriptions( currentGroupId: Long = FeedGroupEntity.GROUP_ALL_ID, @@ -44,7 +44,7 @@ class SubscriptionManager(context: Context) { } } showOnlyUngrouped -> subscriptionTable.getSubscriptionsOnlyUngrouped(currentGroupId) - else -> subscriptionTable.all + else -> subscriptionTable.getAll() } } @@ -71,12 +71,12 @@ class SubscriptionManager(context: Context) { subscriptionTable.getSubscription(info.serviceId, info.url) .flatMapCompletable { Completable.fromRunnable { - it.setData( - info.name, - ImageStrategy.imageListToDbUrl(info.avatars), - info.description, - info.subscriberCount - ) + it.apply { + name = info.name + avatarUrl = ImageStrategy.imageListToDbUrl(info.avatars) + description = info.description + subscriberCount = info.subscriberCount + } subscriptionTable.update(it) } } diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java index 77a70afa9..16a8990a6 100644 --- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java +++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java @@ -89,8 +89,8 @@ public class SubscriptionsImportFragment extends BaseFragment { if (supportedSources.isEmpty() && currentServiceId != Constants.NO_SERVICE_ID) { ErrorUtil.showSnackbar(activity, new ErrorInfo(new String[]{}, UserAction.SUBSCRIPTION_IMPORT_EXPORT, - ServiceHelper.getNameOfServiceById(currentServiceId), "Service does not support importing subscriptions", + currentServiceId, R.string.general_error)); activity.finish(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java index 98da962e7..42f6cbf36 100644 --- a/app/src/main/java/org/schabi/newpipe/player/Player.java +++ b/app/src/main/java/org/schabi/newpipe/player/Player.java @@ -24,12 +24,12 @@ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJ import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP; import static com.google.android.exoplayer2.Player.DiscontinuityReason; import static com.google.android.exoplayer2.Player.Listener; +import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static com.google.android.exoplayer2.Player.RepeatMode; import static org.schabi.newpipe.extractor.ServiceList.YouTube; import static org.schabi.newpipe.extractor.utils.Utils.isNullOrEmpty; -import static org.schabi.newpipe.player.helper.PlayerHelper.nextRepeatMode; import static org.schabi.newpipe.player.helper.PlayerHelper.retrievePlaybackParametersFromPrefs; import static org.schabi.newpipe.player.helper.PlayerHelper.retrieveSeekDurationFromPreferences; import static org.schabi.newpipe.player.helper.PlayerHelper.savePlaybackParametersToPrefs; @@ -60,6 +60,8 @@ import android.view.LayoutInflater; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.content.IntentCompat; import androidx.core.math.MathUtils; import androidx.preference.PreferenceManager; @@ -108,6 +110,7 @@ import org.schabi.newpipe.player.playback.MediaSourceManager; import org.schabi.newpipe.player.playback.PlaybackListener; import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueueItem; +import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.resolver.AudioPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver; import org.schabi.newpipe.player.resolver.VideoPlaybackResolver.SourceType; @@ -117,6 +120,7 @@ import org.schabi.newpipe.player.ui.PlayerUiList; import org.schabi.newpipe.player.ui.PopupPlayerUi; import org.schabi.newpipe.player.ui.VideoPlayerUi; import org.schabi.newpipe.util.DependentPreferenceHelper; +import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ListHelper; import org.schabi.newpipe.util.NavigationHelper; import org.schabi.newpipe.util.SerializedCache; @@ -124,14 +128,17 @@ import org.schabi.newpipe.util.StreamTypeUtil; import org.schabi.newpipe.util.image.PicassoHelper; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.IntStream; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.disposables.SerialDisposable; +import io.reactivex.rxjava3.schedulers.Schedulers; public final class Player implements PlaybackListener, Listener { public static final boolean DEBUG = MainActivity.DEBUG; @@ -153,15 +160,13 @@ public final class Player implements PlaybackListener, Listener { // Intent //////////////////////////////////////////////////////////////////////////*/ - public static final String REPEAT_MODE = "repeat_mode"; public static final String PLAYBACK_QUALITY = "playback_quality"; public static final String PLAY_QUEUE_KEY = "play_queue_key"; - public static final String ENQUEUE = "enqueue"; - public static final String ENQUEUE_NEXT = "enqueue_next"; public static final String RESUME_PLAYBACK = "resume_playback"; public static final String PLAY_WHEN_READY = "play_when_ready"; public static final String PLAYER_TYPE = "player_type"; - public static final String IS_MUTED = "is_muted"; + public static final String PLAYER_INTENT_TYPE = "player_intent_type"; + public static final String PLAYER_INTENT_DATA = "player_intent_data"; /*////////////////////////////////////////////////////////////////////////// // Time constants @@ -246,6 +251,8 @@ public final class Player implements PlaybackListener, Listener { private final SerialDisposable progressUpdateDisposable = new SerialDisposable(); @NonNull private final CompositeDisposable databaseUpdateDisposable = new CompositeDisposable(); + @NonNull + private final CompositeDisposable streamItemDisposable = new CompositeDisposable(); // This is the only listener we need for thumbnail loading, since there is always at most only // one thumbnail being loaded at a time. This field is also here to maintain a strong reference, @@ -346,49 +353,121 @@ public final class Player implements PlaybackListener, Listener { @SuppressWarnings("MethodLength") public void handleIntent(@NonNull final Intent intent) { - // fail fast if no play queue was provided - final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); - if (queueCache == null) { + final var playerIntentType = IntentCompat.getSerializableExtra(intent, PLAYER_INTENT_TYPE, + PlayerIntentType.class); + if (playerIntentType == null) { return; } - final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class); - if (newQueue == null) { - return; + // TODO: this should be in the second switch below, but I’m not sure whether I + // can move the initUIs stuff without breaking the setup for edge cases somehow. + // when playing from a timestamp, keep the current player as-is. + if (playerIntentType != PlayerIntentType.TimestampChange) { + playerType = IntentCompat.getSerializableExtra(intent, PLAYER_TYPE, PlayerType.class); } - - final PlayerType oldPlayerType = playerType; - playerType = PlayerType.retrieveFromIntent(intent); initUIsForCurrentPlayerType(); - // We need to setup audioOnly before super(), see "sourceOf" isAudioOnly = audioPlayerSelected(); if (intent.hasExtra(PLAYBACK_QUALITY)) { videoResolver.setPlaybackQuality(intent.getStringExtra(PLAYBACK_QUALITY)); } - // Resolve enqueue intents - if (intent.getBooleanExtra(ENQUEUE, false) && playQueue != null) { - playQueue.append(newQueue.getStreams()); - return; + final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); - // Resolve enqueue next intents - } else if (intent.getBooleanExtra(ENQUEUE_NEXT, false) && playQueue != null) { - final int currentIndex = playQueue.getIndex(); - playQueue.append(newQueue.getStreams()); - playQueue.move(playQueue.size() - 1, currentIndex + 1); + switch (playerIntentType) { + case Enqueue -> { + if (playQueue != null) { + final PlayQueue newQueue = getPlayQueueFromCache(intent); + if (newQueue == null) { + return; + } + playQueue.append(newQueue.getStreams()); + return; + } + + // TODO: This falls through to the old logic, there was no playQueue + // yet so we should start the player and add the new video + break; + } + case EnqueueNext -> { + if (playQueue != null) { + final PlayQueue newQueue = getPlayQueueFromCache(intent); + if (newQueue == null) { + return; + } + final PlayQueueItem newItem = newQueue.getStreams().get(0); + playQueue.enqueueNext(newItem, false); + return; + } + + // TODO: This falls through to the old logic, there was no playQueue + // yet so we should start the player and add the new video + break; + } + case TimestampChange -> { + final var data = Objects.requireNonNull(IntentCompat.getParcelableExtra(intent, + PLAYER_INTENT_DATA, TimestampChangeData.class)); + final Single single = + ExtractorHelper.getStreamInfo(data.getServiceId(), data.getUrl(), false); + streamItemDisposable.add(single.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(info -> { + final @Nullable PlayQueue oldPlayQueue = playQueue; + info.setStartPosition(data.getSeconds()); + final PlayQueueItem playQueueItem = new PlayQueueItem(info); + + // If the stream is already playing, + // we can just seek to the appropriate timestamp + if (oldPlayQueue != null + && playQueueItem.isSameItem(oldPlayQueue.getItem())) { + // Player can have state = IDLE when playback is stopped or failed + // and we should retry in this case + if (simpleExoPlayer.getPlaybackState() + == com.google.android.exoplayer2.Player.STATE_IDLE) { + simpleExoPlayer.prepare(); + } + simpleExoPlayer.seekTo(oldPlayQueue.getIndex(), + data.getSeconds() * 1000L); + simpleExoPlayer.setPlayWhenReady(playWhenReady); + + } else { + final PlayQueue newPlayQueue; + + // If there is no queue yet, just add our item + if (oldPlayQueue == null) { + newPlayQueue = new SinglePlayQueue(playQueueItem); + + // else we add the timestamped stream behind the current video + // and start playing it. + } else { + oldPlayQueue.enqueueNext(playQueueItem, true); + oldPlayQueue.offsetIndex(1); + newPlayQueue = oldPlayQueue; + } + initPlayback(newPlayQueue, playWhenReady); + } + + }, throwable -> { + // This will only show a snackbar if the passed context has a root view: + // otherwise it will resort to showing a notification, so we are safe + // here. + final var info = new ErrorInfo(throwable, UserAction.PLAY_ON_POPUP, + data.getUrl(), null, data.getUrl()); + ErrorUtil.createNotification(context, info); + })); + return; + } + case AllOthers -> { + // fallthrough; TODO: put other intent data in separate cases + } + } + + final PlayQueue newQueue = getPlayQueueFromCache(intent); + if (newQueue == null) { return; } - final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); - final float playbackSpeed = savedParameters.speed; - final float playbackPitch = savedParameters.pitch; - final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString( - R.string.playback_skip_silence_key), getPlaybackSkipSilence()); - + // branching parameters for below final boolean samePlayQueue = playQueue != null && playQueue.equalStreamsAndIndex(newQueue); - final int repeatMode = intent.getIntExtra(REPEAT_MODE, getRepeatMode()); - final boolean playWhenReady = intent.getBooleanExtra(PLAY_WHEN_READY, true); - final boolean isMuted = intent.getBooleanExtra(IS_MUTED, isMuted()); /* * TODO As seen in #7427 this does not work: @@ -403,7 +482,7 @@ public final class Player implements PlaybackListener, Listener { if (!exoPlayerIsNull() && newQueue.size() == 1 && newQueue.getItem() != null && playQueue != null && playQueue.size() == 1 && playQueue.getItem() != null - && newQueue.getItem().getUrl().equals(playQueue.getItem().getUrl()) + && newQueue.getItem().isSameItem(playQueue.getItem()) && newQueue.getItem().getRecoveryPosition() != PlayQueueItem.RECOVERY_UNSET) { // Player can have state = IDLE when playback is stopped or failed // and we should retry in this case @@ -429,7 +508,8 @@ public final class Player implements PlaybackListener, Listener { } else if (intent.getBooleanExtra(RESUME_PLAYBACK, false) && DependentPreferenceHelper.getResumePlaybackEnabled(context) - && !samePlayQueue + // !samePlayQueue + && (playQueue == null || !playQueue.equalStreamsAndIndex(newQueue)) && !newQueue.isEmpty() && newQueue.getItem() != null && newQueue.getItem().getRecoveryPosition() == PlayQueueItem.RECOVERY_UNSET) { @@ -445,30 +525,30 @@ public final class Player implements PlaybackListener, Listener { newQueue.setRecovery(newQueue.getIndex(), state.getProgressMillis()); } - initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); + initPlayback(newQueue, playWhenReady); }, error -> { if (DEBUG) { Log.w(TAG, "Failed to start playback", error); } // In case any error we can start playback without history - initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); + initPlayback(newQueue, playWhenReady); }, () -> { // Completed but not found in history - initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, - playbackSkipSilence, playWhenReady, isMuted); + initPlayback(newQueue, playWhenReady); } )); } else { // Good to go... // In a case of equal PlayQueues we can re-init old one but only when it is disposed - initPlayback(samePlayQueue ? playQueue : newQueue, repeatMode, playbackSpeed, - playbackPitch, playbackSkipSilence, playWhenReady, isMuted); + initPlayback(samePlayQueue ? playQueue : newQueue, playWhenReady); } + } + + + public void handleIntentPost(final PlayerType oldPlayerType) { if (oldPlayerType != playerType && playQueue != null) { // If playerType changes from one to another we should reload the player // (to disable/enable video stream or to set quality) @@ -479,6 +559,19 @@ public final class Player implements PlaybackListener, Listener { NavigationHelper.sendPlayerStartedEvent(context); } + @Nullable + private static PlayQueue getPlayQueueFromCache(@NonNull final Intent intent) { + final String queueCache = intent.getStringExtra(PLAY_QUEUE_KEY); + if (queueCache == null) { + return null; + } + final PlayQueue newQueue = SerializedCache.getInstance().take(queueCache, PlayQueue.class); + if (newQueue == null) { + return null; + } + return newQueue; + } + private void initUIsForCurrentPlayerType() { if ((UIs.get(MainPlayerUi.class).isPresent() && playerType == PlayerType.MAIN) || (UIs.get(PopupPlayerUi.class).isPresent() && playerType == PlayerType.POPUP)) { @@ -512,16 +605,13 @@ public final class Player implements PlaybackListener, Listener { } private void initPlayback(@NonNull final PlayQueue queue, - @RepeatMode final int repeatMode, - final float playbackSpeed, - final float playbackPitch, - final boolean playbackSkipSilence, - final boolean playOnReady, - final boolean isMuted) { + final boolean playOnReady) { destroyPlayer(); initPlayer(playOnReady); - setRepeatMode(repeatMode); - setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence); + final boolean playbackSkipSilence = getPrefs().getBoolean(getContext().getString( + R.string.playback_skip_silence_key), getPlaybackSkipSilence()); + final PlaybackParameters savedParameters = retrievePlaybackParametersFromPrefs(this); + setPlaybackParameters(savedParameters.speed, savedParameters.pitch, playbackSkipSilence); playQueue = queue; playQueue.init(); @@ -529,7 +619,7 @@ public final class Player implements PlaybackListener, Listener { UIs.call(PlayerUi::initPlayback); - simpleExoPlayer.setVolume(isMuted ? 0 : 1); + simpleExoPlayer.setVolume(isMuted() ? 0 : 1); notifyQueueUpdateToListeners(); } @@ -611,6 +701,7 @@ public final class Player implements PlaybackListener, Listener { databaseUpdateDisposable.clear(); progressUpdateDisposable.set(null); + streamItemDisposable.clear(); cancelLoadingCurrentThumbnail(); UIs.destroyAll(Object.class); // destroy every UI: obviously every UI extends Object @@ -764,7 +855,8 @@ public final class Player implements PlaybackListener, Listener { private void registerBroadcastReceiver() { // Try to unregister current first unregisterBroadcastReceiver(); - context.registerReceiver(broadcastReceiver, intentFilter); + ContextCompat.registerReceiver(context, broadcastReceiver, intentFilter, + ContextCompat.RECEIVER_EXPORTED); } private void unregisterBroadcastReceiver() { @@ -1176,16 +1268,25 @@ public final class Player implements PlaybackListener, Listener { return exoPlayerIsNull() ? REPEAT_MODE_OFF : simpleExoPlayer.getRepeatMode(); } - public void setRepeatMode(@RepeatMode final int repeatMode) { + public void cycleNextRepeatMode() { if (!exoPlayerIsNull()) { + @RepeatMode final int repeatMode; + switch (simpleExoPlayer.getRepeatMode()) { + case REPEAT_MODE_OFF: + repeatMode = REPEAT_MODE_ONE; + break; + case REPEAT_MODE_ONE: + repeatMode = REPEAT_MODE_ALL; + break; + case REPEAT_MODE_ALL: + default: + repeatMode = REPEAT_MODE_OFF; + break; + } simpleExoPlayer.setRepeatMode(repeatMode); } } - public void cycleNextRepeatMode() { - setRepeatMode(nextRepeatMode(getRepeatMode())); - } - @Override public void onRepeatModeChanged(@RepeatMode final int repeatMode) { if (DEBUG) { @@ -1288,7 +1389,8 @@ public final class Player implements PlaybackListener, Listener { UserAction.PLAY_STREAM, "Loading failed for [" + currentMetadata.getTitle() + "]: " + currentMetadata.getStreamUrl(), - currentMetadata.getServiceId()); + currentMetadata.getServiceId(), + currentMetadata.getStreamUrl()); ErrorUtil.createNotification(context, errorInfo); } @@ -1504,7 +1606,7 @@ public final class Player implements PlaybackListener, Listener { errorInfo = new ErrorInfo(error, UserAction.PLAY_STREAM, "Player error[type=" + error.getErrorCodeName() + "] occurred while playing " + currentMetadata.getStreamUrl(), - currentMetadata.getServiceId()); + currentMetadata.getServiceId(), currentMetadata.getStreamUrl()); } ErrorUtil.createNotification(context, errorInfo); } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerIntentType.kt b/app/src/main/java/org/schabi/newpipe/player/PlayerIntentType.kt new file mode 100644 index 000000000..ed0c19c99 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerIntentType.kt @@ -0,0 +1,24 @@ +package org.schabi.newpipe.player + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +// We model this as an enum class plus one struct for each enum value +// so we can consume it from Java properly. After converting to Kotlin, +// we could switch to a sealed enum class & a proper Kotlin `when` match. +enum class PlayerIntentType { + Enqueue, + EnqueueNext, + TimestampChange, + AllOthers +} + +/** + * A timestamp on the given was clicked and we should switch the playing stream to it. + */ +@Parcelize +data class TimestampChangeData( + val serviceId: Int, + val url: String, + val seconds: Int +) : Parcelable diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java index adc050e4b..dba30f9e8 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerService.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerService.java @@ -40,6 +40,7 @@ import org.schabi.newpipe.player.mediabrowser.MediaBrowserImpl; import org.schabi.newpipe.player.mediabrowser.MediaBrowserPlaybackPreparer; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.player.notification.NotificationPlayerUi; +import org.schabi.newpipe.player.notification.NotificationUtil; import org.schabi.newpipe.util.ThemeHelper; import java.lang.ref.WeakReference; @@ -156,23 +157,24 @@ public final class PlayerService extends MediaBrowserServiceCompat { } } - if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction()) - && (player == null || player.getPlayQueue() == null)) { - /* - No need to process media button's actions if the player is not working, otherwise - the player service would strangely start with nothing to play - Stop the service in this case, which will be removed from the foreground and its - notification cancelled in its destruction - */ + if (player == null) { + // No need to process media button's actions or other system intents if the player is + // not running. However, since the current intent might have been issued by the system + // with `startForegroundService()` (for unknown reasons), we need to ensure that we post + // a (dummy) foreground notification, otherwise we'd incur in + // "Context.startForegroundService() did not then call Service.startForeground()". Then + // we stop the service again. + Log.d(TAG, "onStartCommand() got a useless intent, closing the service"); + NotificationUtil.startForegroundWithDummyNotification(this); destroyPlayerAndStopService(); return START_NOT_STICKY; } - if (player != null) { - player.handleIntent(intent); - player.UIs().get(MediaSessionPlayerUi.class) - .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); - } + final PlayerType oldPlayerType = player.getPlayerType(); + player.handleIntent(intent); + player.handleIntentPost(oldPlayerType); + player.UIs().get(MediaSessionPlayerUi.class) + .ifPresent(ui -> ui.handleMediaButtonIntent(intent)); return START_NOT_STICKY; } diff --git a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java index 171a70395..f74389d79 100644 --- a/app/src/main/java/org/schabi/newpipe/player/PlayerType.java +++ b/app/src/main/java/org/schabi/newpipe/player/PlayerType.java @@ -1,32 +1,7 @@ package org.schabi.newpipe.player; -import static org.schabi.newpipe.player.Player.PLAYER_TYPE; - -import android.content.Intent; - public enum PlayerType { MAIN, AUDIO, POPUP; - - /** - * @return an integer representing this {@link PlayerType}, to be used to save it in intents - * @see #retrieveFromIntent(Intent) Use retrieveFromIntent() to retrieve and convert player type - * integers from an intent - */ - public int valueForIntent() { - return ordinal(); - } - - /** - * @param intent the intent to retrieve a player type from - * @return the player type integer retrieved from the intent, converted back into a {@link - * PlayerType}, or {@link PlayerType#MAIN} if there is no player type extra in the - * intent - * @throws ArrayIndexOutOfBoundsException if the intent contains an invalid player type integer - * @see #valueForIntent() Use valueForIntent() to obtain valid player type integers - */ - public static PlayerType retrieveFromIntent(final Intent intent) { - return values()[intent.getIntExtra(PLAYER_TYPE, MAIN.valueForIntent())]; - } } diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java index a05990816..084336d54 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/AudioReactor.java @@ -154,9 +154,6 @@ public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An notifyAudioSessionUpdate(true, audioSessionId); } private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) { - if (!PlayerHelper.isUsingDSP()) { - return; - } final Intent intent = new Intent(active ? AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION : AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION); diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java index 266d65f36..0f9579352 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java @@ -1,8 +1,5 @@ package org.schabi.newpipe.player.helper; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF; -import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER; import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI; @@ -25,7 +22,6 @@ import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; import com.google.android.exoplayer2.PlaybackParameters; -import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.SeekParameters; import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection; @@ -145,11 +141,11 @@ public final class PlayerHelper { @ResizeMode final int resizeMode) { switch (resizeMode) { case AspectRatioFrameLayout.RESIZE_MODE_FIT: - return context.getResources().getString(R.string.resize_fit); + return context.getString(R.string.resize_fit); case AspectRatioFrameLayout.RESIZE_MODE_FILL: - return context.getResources().getString(R.string.resize_fill); + return context.getString(R.string.resize_fill); case AspectRatioFrameLayout.RESIZE_MODE_ZOOM: - return context.getResources().getString(R.string.resize_zoom); + return context.getString(R.string.resize_zoom); case AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT: case AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH: default: @@ -300,10 +296,6 @@ public final class PlayerHelper { AdaptiveTrackSelection.DEFAULT_BANDWIDTH_FRACTION); } - public static boolean isUsingDSP() { - return true; - } - @NonNull public static CaptionStyleCompat getCaptionStyle(@NonNull final Context context) { final CaptioningManager captioningManager = ContextCompat.getSystemService(context, @@ -410,23 +402,9 @@ public final class PlayerHelper { return singlePlayQueue; } - // endregion // region Utils used by player - @RepeatMode - public static int nextRepeatMode(@RepeatMode final int repeatMode) { - switch (repeatMode) { - case REPEAT_MODE_OFF: - return REPEAT_MODE_ONE; - case REPEAT_MODE_ONE: - return REPEAT_MODE_ALL; - case REPEAT_MODE_ALL: - default: - return REPEAT_MODE_OFF; - } - } - @ResizeMode public static int retrieveResizeModeFromPrefs(final Player player) { return player.getPrefs().getInt(player.getContext().getString(R.string.last_resize_mode), diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java index 97f2d6717..9edfc804a 100644 --- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java +++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHolder.java @@ -192,9 +192,11 @@ public final class PlayerHolder { startPlayerListener(); // ^ will call listener.onPlayerConnected() down the line if there is an active player - // notify the main activity that binding the service has completed, so that it can - // open the bottom mini-player - NavigationHelper.sendPlayerStartedEvent(localBinder.getService()); + if (playerService != null && playerService.getPlayer() != null) { + // notify the main activity that binding the service has completed and that there is + // a player, so that it can open the bottom mini-player + NavigationHelper.sendPlayerStartedEvent(localBinder.getService()); + } } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt index b7d57657d..d221d704b 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserImpl.kt @@ -315,7 +315,7 @@ class MediaBrowserImpl( } private fun populateHistory(): Single> { - val history = database.streamHistoryDAO().getHistory().firstOrError() + val history = database.streamHistoryDAO().history.firstOrError() return history.map { items -> items.map { this.createHistoryMediaItem(it) } } diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt index 2948eeaf8..072a8f332 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/MediaBrowserPlaybackPreparer.kt @@ -17,6 +17,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import org.schabi.newpipe.MainActivity import org.schabi.newpipe.NewPipeDatabase import org.schabi.newpipe.R +import org.schabi.newpipe.error.ErrorInfo import org.schabi.newpipe.extractor.InfoItem.InfoType import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler @@ -84,7 +85,7 @@ class MediaBrowserPlaybackPreparer( }, { throwable -> Log.e(TAG, "Failed to start playback of media ID [$mediaId]", throwable) - onPrepareError() + onPrepareError(throwable) } ) } @@ -115,9 +116,9 @@ class MediaBrowserPlaybackPreparer( ) } - private fun onPrepareError() { + private fun onPrepareError(throwable: Throwable) { setMediaSessionError.accept( - ContextCompat.getString(context, R.string.error_snackbar_message), + ErrorInfo.getMessage(throwable, null, null).getString(context), PlaybackStateCompat.ERROR_CODE_APP_ERROR ) } @@ -214,7 +215,7 @@ class MediaBrowserPlaybackPreparer( } val streamId = path[0].toLong() - return database.streamHistoryDAO().getHistory() + return database.streamHistoryDAO().history .firstOrError() .map { items -> val infoItems = items diff --git a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt index 973b11b37..05719b6d4 100644 --- a/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt +++ b/app/src/main/java/org/schabi/newpipe/player/mediabrowser/PackageValidator.kt @@ -147,18 +147,15 @@ internal class PackageValidator(context: Context) { private fun buildCallerInfo(callingPackage: String): CallerPackageInfo? { val packageInfo = getPackageInfo(callingPackage) ?: return null - val appName = packageInfo.applicationInfo.loadLabel(packageManager).toString() - val uid = packageInfo.applicationInfo.uid + val appName = packageInfo.applicationInfo?.loadLabel(packageManager).toString() + val uid = packageInfo.applicationInfo?.uid ?: -1 val signature = getSignature(packageInfo) - val requestedPermissions = packageInfo.requestedPermissions - val permissionFlags = packageInfo.requestedPermissionsFlags - val activePermissions = mutableSetOf() - requestedPermissions?.forEachIndexed { index, permission -> - if (permissionFlags[index] and REQUESTED_PERMISSION_GRANTED != 0) { - activePermissions += permission - } - } + val requestedPermissions = packageInfo.requestedPermissions?.asSequence().orEmpty() + val permissionFlags = packageInfo.requestedPermissionsFlags?.asSequence().orEmpty() + val activePermissions = (requestedPermissions zip permissionFlags) + .filter { (permission, flag) -> flag and REQUESTED_PERMISSION_GRANTED != 0 } + .mapTo(mutableSetOf()) { (permission, flag) -> permission } return CallerPackageInfo(appName, callingPackage, uid, signature, activePermissions.toSet()) } @@ -189,12 +186,12 @@ internal class PackageValidator(context: Context) { */ @Suppress("deprecation") private fun getSignature(packageInfo: PackageInfo): String? = - if (packageInfo.signatures == null || packageInfo.signatures.size != 1) { + if (packageInfo.signatures == null || packageInfo.signatures!!.size != 1) { // Security best practices dictate that an app should be signed with exactly one (1) // signature. Because of this, if there are multiple signatures, reject it. null } else { - val certificate = packageInfo.signatures[0].toByteArray() + val certificate = packageInfo.signatures!![0].toByteArray() getSignatureSha256(certificate) } diff --git a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java index 30420b0c7..9b9c47b0e 100644 --- a/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java +++ b/app/src/main/java/org/schabi/newpipe/player/notification/NotificationUtil.java @@ -5,7 +5,9 @@ import static androidx.media.app.NotificationCompat.MediaStyle; import static org.schabi.newpipe.player.notification.NotificationConstants.ACTION_CLOSE; import android.annotation.SuppressLint; +import android.app.Notification; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.content.pm.ServiceInfo; import android.graphics.Bitmap; @@ -23,6 +25,8 @@ import androidx.core.content.ContextCompat; import org.schabi.newpipe.MainActivity; import org.schabi.newpipe.R; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.PlayerIntentType; +import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.mediasession.MediaSessionPlayerUi; import org.schabi.newpipe.util.NavigationHelper; @@ -89,12 +93,9 @@ public final class NotificationUtil { Log.d(TAG, "createNotification()"); } notificationManager = NotificationManagerCompat.from(player.getContext()); - final NotificationCompat.Builder builder = - new NotificationCompat.Builder(player.getContext(), - player.getContext().getString(R.string.notification_channel_id)); - final MediaStyle mediaStyle = new MediaStyle(); // setup media style (compact notification slots and media session) + final MediaStyle mediaStyle = new MediaStyle(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { // notification actions are ignored on Android 13+, and are replaced by code in // MediaSessionPlayerUi @@ -107,18 +108,9 @@ public final class NotificationUtil { .ifPresent(mediaStyle::setMediaSession); // setup notification builder - builder.setStyle(mediaStyle) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setCategory(NotificationCompat.CATEGORY_TRANSPORT) - .setShowWhen(false) - .setSmallIcon(R.drawable.ic_newpipe_triangle_white) - .setColor(ContextCompat.getColor(player.getContext(), - R.color.dark_background_color)) + final var builder = setupNotificationBuilder(player.getContext(), mediaStyle) .setColorized(player.getPrefs().getBoolean( - player.getContext().getString(R.string.notification_colorize_key), true)) - .setDeleteIntent(PendingIntentCompat.getBroadcast(player.getContext(), - NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT, false)); + player.getContext().getString(R.string.notification_colorize_key), true)); // set the initial value for the video thumbnail, updatable with updateNotificationThumbnail setLargeIcon(builder); @@ -167,19 +159,17 @@ public final class NotificationUtil { && notificationBuilder.mActions.get(2).actionIntent != null); } + public static void startForegroundWithDummyNotification(final PlayerService service) { + final var builder = setupNotificationBuilder(service, new MediaStyle()); + startForeground(service, builder.build()); + } public void createNotificationAndStartForeground() { if (notificationBuilder == null) { notificationBuilder = createNotification(); } updateNotification(); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build(), - ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK); - } else { - player.getService().startForeground(NOTIFICATION_ID, notificationBuilder.build()); - } + startForeground(player.getService(), notificationBuilder.build()); } public void cancelNotificationAndStopForeground() { @@ -193,6 +183,34 @@ public final class NotificationUtil { } + ///////////////////////////////////////////////////// + // STATIC FUNCTIONS IN COMMON BETWEEN DUMMY AND REAL NOTIFICATION + ///////////////////////////////////////////////////// + + private static NotificationCompat.Builder setupNotificationBuilder(final Context context, + final MediaStyle style) { + return new NotificationCompat.Builder(context, + context.getString(R.string.notification_channel_id)) + .setStyle(style) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_TRANSPORT) + .setShowWhen(false) + .setSmallIcon(R.drawable.ic_newpipe_triangle_white) + .setColor(ContextCompat.getColor(context, R.color.dark_background_color)) + .setDeleteIntent(PendingIntentCompat.getBroadcast(context, + NOTIFICATION_ID, new Intent(ACTION_CLOSE), FLAG_UPDATE_CURRENT, false)); + } + + private static void startForeground(final PlayerService service, + final Notification notification) { + // ServiceInfo constants are not used below Android Q, so 0 is set here + final int serviceType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK : 0; + ServiceCompat.startForeground(service, NOTIFICATION_ID, notification, serviceType); + } + + ///////////////////////////////////////////////////// // ACTIONS ///////////////////////////////////////////////////// @@ -256,7 +274,9 @@ public final class NotificationUtil { } else { // We are playing in fragment. Don't open another activity just show fragment. That's it final Intent intent = NavigationHelper.getPlayerIntent( - player.getContext(), MainActivity.class, null, true); + player.getContext(), MainActivity.class, null, + PlayerIntentType.AllOthers); + intent.putExtra(Player.RESUME_PLAYBACK, true); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java index cfa2ab316..2a1b9d281 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueue.java @@ -23,7 +23,7 @@ import java.util.concurrent.atomic.AtomicInteger; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.core.Flowable; -import io.reactivex.rxjava3.subjects.BehaviorSubject; +import io.reactivex.rxjava3.subjects.PublishSubject; /** * PlayQueue is responsible for keeping track of a list of streams and the index of @@ -46,7 +46,7 @@ public abstract class PlayQueue implements Serializable { private List backup; private List streams; - private transient BehaviorSubject eventBroadcast; + private transient PublishSubject eventBroadcast; private transient Flowable broadcastReceiver; private transient boolean disposed = false; @@ -71,7 +71,7 @@ public abstract class PlayQueue implements Serializable { *

*/ public void init() { - eventBroadcast = BehaviorSubject.create(); + eventBroadcast = PublishSubject.create(); broadcastReceiver = eventBroadcast.toFlowable(BackpressureStrategy.BUFFER) .observeOn(AndroidSchedulers.mainThread()) @@ -291,6 +291,22 @@ public abstract class PlayQueue implements Serializable { broadcast(new AppendEvent(itemList.size())); } + /** + * Add the given item after the current stream. + * + * @param item item to add. + * @param skipIfSame if set, skip adding if the next stream is the same stream. + */ + public void enqueueNext(@NonNull final PlayQueueItem item, final boolean skipIfSame) { + final int currentIndex = getIndex(); + // if the next item is the same item as the one we want to enqueue, skip if flag is true + if (skipIfSame && item.isSameItem(getItem(currentIndex + 1))) { + return; + } + append(List.of(item)); + move(size() - 1, currentIndex + 1); + } + /** * Removes the item at the given index from the play queue. *

@@ -529,8 +545,7 @@ public abstract class PlayQueue implements Serializable { final PlayQueueItem stream = streams.get(i); final PlayQueueItem otherStream = other.streams.get(i); // Check is based on serviceId and URL - if (stream.getServiceId() != otherStream.getServiceId() - || !stream.getUrl().equals(otherStream.getUrl())) { + if (!stream.isSameItem(otherStream)) { return false; } } diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java index 759c51267..d1d897c39 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItem.java @@ -38,7 +38,7 @@ public class PlayQueueItem implements Serializable { private long recoveryPosition; private Throwable error; - PlayQueueItem(@NonNull final StreamInfo info) { + public PlayQueueItem(@NonNull final StreamInfo info) { this(info.getName(), info.getUrl(), info.getServiceId(), info.getDuration(), info.getThumbnails(), info.getUploaderName(), info.getUploaderUrl(), info.getStreamType()); @@ -71,6 +71,22 @@ public class PlayQueueItem implements Serializable { this.recoveryPosition = RECOVERY_UNSET; } + /** Whether these two items should be treated as the same stream + * for the sake of keeping the same player running when e.g. jumping between timestamps. + * + * @param other the {@link PlayQueueItem} to compare against. + * @return whether the two items are the same so the stream can be re-used. + */ + public boolean isSameItem(@Nullable final PlayQueueItem other) { + if (other == null) { + return false; + } + // We assume that the same service & URL uniquely determines + // that we can keep the same stream running. + return serviceId == other.serviceId + && url.equals(other.url); + } + @NonNull public String getTitle() { return title; diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java index 0eb0f235a..f13d7924d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java +++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/SinglePlayQueue.java @@ -16,7 +16,9 @@ public final class SinglePlayQueue extends PlayQueue { public SinglePlayQueue(final StreamInfo info) { super(0, List.of(new PlayQueueItem(info))); } - + public SinglePlayQueue(final PlayQueueItem item) { + super(0, List.of(item)); + } public SinglePlayQueue(final StreamInfo info, final long startPosition) { super(0, List.of(new PlayQueueItem(info))); getItem().setRecoveryPosition(startPosition); diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java index d8efb30df..bfcc82984 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/MainPlayerUi.java @@ -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 @@ -934,8 +936,7 @@ public final class MainPlayerUi extends VideoPlayerUi implements View.OnLayoutCh } fragmentListener.onFullscreenStateChanged(isFullscreen); - binding.titleTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); - binding.channelTextView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); + binding.metadataView.setVisibility(isFullscreen ? View.VISIBLE : View.GONE); binding.playerCloseButton.setVisibility(isFullscreen ? View.GONE : View.VISIBLE); setupScreenRotationButton(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java index 6c98ab0fa..24b734fe0 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/PopupPlayerUi.java @@ -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(); } diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java index 7157d6af2..b68d3d94d 100644 --- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java +++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java @@ -1554,6 +1554,11 @@ public abstract class VideoPlayerUi extends PlayerUi implements SeekBar.OnSeekBa @Override public void onVideoSizeChanged(@NonNull final VideoSize videoSize) { super.onVideoSizeChanged(videoSize); + // Starting with ExoPlayer 2.19.0, the VideoSize will report a width and height of 0 + // if the renderer is disabled. In that case, we skip updating the aspect ratio. + if (videoSize.width == 0 || videoSize.height == 0) { + return; + } binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height); } //endregion diff --git a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java index 321ad65da..baaa93e44 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BackupRestoreSettingsFragment.java @@ -40,6 +40,8 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class BackupRestoreSettingsFragment extends BasePreferenceFragment { @@ -96,10 +98,9 @@ public class BackupRestoreSettingsFragment extends BasePreferenceFragment { return true; }); - final Preference resetSettings = findPreference(getString(R.string.reset_settings)); + final Preference resetSettings = requirePreference(R.string.reset_settings); // Resets all settings by deleting shared preference and restarting the app // A dialogue will pop up to confirm if user intends to reset all settings - assert resetSettings != null; resetSettings.setOnPreferenceClickListener(preference -> { // Show Alert Dialogue final AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); @@ -155,9 +156,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()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java index 619579f3a..21cba3daa 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/BasePreferenceFragment.java @@ -48,8 +48,8 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat { } @NonNull - public final Preference requirePreference(@StringRes final int resId) { - final Preference preference = findPreference(getString(resId)); + public final T requirePreference(@StringRes final int resId) { + final T preference = findPreference(getString(resId)); Objects.requireNonNull(preference); return preference; } diff --git a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java index d78ade49d..82f2f5bb6 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/DebugSettingsFragment.java @@ -22,27 +22,20 @@ public class DebugSettingsFragment extends BasePreferenceFragment { addPreferencesFromResourceRegistry(); final Preference allowHeapDumpingPreference = - findPreference(getString(R.string.allow_heap_dumping_key)); + requirePreference(R.string.allow_heap_dumping_key); final Preference showMemoryLeaksPreference = - findPreference(getString(R.string.show_memory_leaks_key)); + requirePreference(R.string.show_memory_leaks_key); final Preference showImageIndicatorsPreference = - findPreference(getString(R.string.show_image_indicators_key)); + requirePreference(R.string.show_image_indicators_key); final Preference checkNewStreamsPreference = - findPreference(getString(R.string.check_new_streams_key)); + requirePreference(R.string.check_new_streams_key); final Preference crashTheAppPreference = - findPreference(getString(R.string.crash_the_app_key)); + requirePreference(R.string.crash_the_app_key); final Preference showErrorSnackbarPreference = - findPreference(getString(R.string.show_error_snackbar_key)); + requirePreference(R.string.show_error_snackbar_key); final Preference createErrorNotificationPreference = - findPreference(getString(R.string.create_error_notification_key)); + requirePreference(R.string.create_error_notification_key); - assert allowHeapDumpingPreference != null; - assert showMemoryLeaksPreference != null; - assert showImageIndicatorsPreference != null; - assert checkNewStreamsPreference != null; - assert crashTheAppPreference != null; - assert showErrorSnackbarPreference != null; - assert createErrorNotificationPreference != null; final Optional optBVLeakCanary = getBVDLeakCanary(); diff --git a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java index 32e33d55b..cb3de39a0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java @@ -25,7 +25,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Check if the app is updatable if (!ReleaseVersionUtil.INSTANCE.isReleaseApk()) { getPreferenceScreen().removePreference( - findPreference(getString(R.string.update_pref_screen_key))); + requirePreference(R.string.update_pref_screen_key)); defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply(); } @@ -33,7 +33,7 @@ public class MainSettingsFragment extends BasePreferenceFragment { // Hide debug preferences in RELEASE build variant if (!DEBUG) { getPreferenceScreen().removePreference( - findPreference(getString(R.string.debug_pref_screen_key))); + requirePreference(R.string.debug_pref_screen_key)); } } diff --git a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java index 0a5512c69..7cb1564b3 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java +++ b/app/src/main/java/org/schabi/newpipe/settings/NewPipeSettings.java @@ -103,12 +103,12 @@ public final class NewPipeSettings { } public static boolean useStorageAccessFramework(final Context context) { - // There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with a - // remote (see #6455). - if (DeviceUtils.isFireTv()) { - return false; - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return true; + } else if (DeviceUtils.isFireTv()) { + // There's a FireOS bug which prevents SAF open/close dialogs from being confirmed with + // a remote (see #6455). + return false; } final String key = context.getString(R.string.storage_use_saf); diff --git a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt index 2d3344c09..d6b0a84da 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/NotificationsSettingsFragment.kt @@ -29,8 +29,7 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.notifications_settings) - streamsNotificationsPreference = - findPreference(getString(R.string.enable_streams_notifications)) + streamsNotificationsPreference = requirePreference(R.string.enable_streams_notifications) // main check is done in onResume, but also do it here to prevent flickering updateEnabledState(NotificationHelper.areNotificationsEnabledOnDevice(requireContext())) @@ -125,8 +124,8 @@ class NotificationsSettingsFragment : BasePreferenceFragment(), OnSharedPreferen private fun updateSubscriptions(subscriptions: List) { val notified = subscriptions.count { it.notificationMode != NotificationMode.DISABLED } - val preference = findPreference(getString(R.string.streams_notifications_channels_key)) - preference?.apply { summary = "$notified/${subscriptions.size}" } + val preference = requirePreference(R.string.streams_notifications_channels_key) + preference.summary = "$notified/${subscriptions.size}" } private fun onError(e: Throwable) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java index 1158b3d83..81fddbcfb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java @@ -396,7 +396,8 @@ public class PeertubeInstanceListFragment extends Fragment { } } - private static class PeertubeInstanceCallback extends DiffUtil.ItemCallback { + private static final class PeertubeInstanceCallback + extends DiffUtil.ItemCallback { @Override public boolean areItemsTheSame(@NonNull final PeertubeInstance oldItem, @NonNull final PeertubeInstance newItem) { diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java index 37335421d..18e0816bb 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectChannelFragment.java @@ -174,7 +174,7 @@ public class SelectChannelFragment extends DialogFragment { void onCancel(); } - private class SelectChannelAdapter + private final class SelectChannelAdapter extends RecyclerView.Adapter { @NonNull @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java index 662379369..c106f5998 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectFeedGroupFragment.java @@ -175,7 +175,7 @@ public class SelectFeedGroupFragment extends DialogFragment { void onCancel(); } - private class SelectFeedGroupAdapter + private final class SelectFeedGroupAdapter extends RecyclerView.Adapter { @NonNull @Override diff --git a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java index 36abef9e5..880cbb282 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/SelectPlaylistFragment.java @@ -118,12 +118,12 @@ public class SelectPlaylistFragment extends DialogFragment { if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.name); + onSelectedListener.onLocalPlaylistSelected(entry.getUid(), entry.getOrderingName()); } else if (selectedItem instanceof PlaylistRemoteEntity) { final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); onSelectedListener.onRemotePlaylistSelected( - entry.getServiceId(), entry.getUrl(), entry.getName()); + entry.getServiceId(), entry.getUrl(), entry.getOrderingName()); } } dismiss(); @@ -138,7 +138,7 @@ public class SelectPlaylistFragment extends DialogFragment { void onRemotePlaylistSelected(int serviceId, String url, String name); } - private class SelectPlaylistAdapter + private final class SelectPlaylistAdapter extends RecyclerView.Adapter { @NonNull @Override @@ -157,14 +157,15 @@ public class SelectPlaylistFragment extends DialogFragment { if (selectedItem instanceof PlaylistMetadataEntry) { final PlaylistMetadataEntry entry = ((PlaylistMetadataEntry) selectedItem); - holder.titleView.setText(entry.name); + holder.titleView.setText(entry.getOrderingName()); holder.view.setOnClickListener(view -> clickedItem(position)); - PicassoHelper.loadPlaylistThumbnail(entry.thumbnailUrl).into(holder.thumbnailView); + PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl()) + .into(holder.thumbnailView); } else if (selectedItem instanceof PlaylistRemoteEntity) { final PlaylistRemoteEntity entry = ((PlaylistRemoteEntity) selectedItem); - holder.titleView.setText(entry.getName()); + holder.titleView.setText(entry.getOrderingName()); holder.view.setOnClickListener(view -> clickedItem(position)); PicassoHelper.loadPlaylistThumbnail(entry.getThumbnailUrl()) .into(holder.thumbnailView); diff --git a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java index b8d0aa556..8923972b0 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java @@ -34,9 +34,9 @@ public class UpdateSettingsFragment extends BasePreferenceFragment { public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) { addPreferencesFromResourceRegistry(); - findPreference(getString(R.string.update_app_key)) + requirePreference(R.string.update_app_key) .setOnPreferenceChangeListener(updatePreferenceChange); - findPreference(getString(R.string.manual_update_key)) + requirePreference(R.string.manual_update_key) .setOnPreferenceClickListener(manualUpdateClick); } diff --git a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java index a1f563724..c5c4c480c 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java +++ b/app/src/main/java/org/schabi/newpipe/settings/VideoAudioSettingsFragment.java @@ -90,12 +90,12 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { showHigherResolutions); // get resolution preferences - final ListPreference defaultResolution = findPreference( - getString(R.string.default_resolution_key)); - final ListPreference defaultPopupResolution = findPreference( - getString(R.string.default_popup_resolution_key)); - final ListPreference mobileDataResolution = findPreference( - getString(R.string.limit_mobile_data_usage_key)); + final ListPreference defaultResolution = requirePreference( + R.string.default_resolution_key); + final ListPreference defaultPopupResolution = requirePreference( + R.string.default_popup_resolution_key); + final ListPreference mobileDataResolution = requirePreference( + R.string.limit_mobile_data_usage_key); // update resolution preferences with new resolutions, entries & values for each defaultResolution.setEntries(resolutionListDescriptions.toArray(new String[0])); @@ -161,8 +161,7 @@ public class VideoAudioSettingsFragment extends BasePreferenceFragment { } } - final ListPreference durations = findPreference( - getString(R.string.seek_duration_key)); + final ListPreference durations = requirePreference(R.string.seek_duration_key); durations.setEntryValues(displayedDurationValues.toArray(new CharSequence[0])); durations.setEntries(displayedDescriptionValues.toArray(new CharSequence[0])); final int selectedDuration = Integer.parseInt(durations.getValue()); diff --git a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt index f61aa72ab..fd8abfa16 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt +++ b/app/src/main/java/org/schabi/newpipe/settings/notifications/NotificationModeConfigAdapter.kt @@ -31,7 +31,7 @@ class NotificationModeConfigAdapter( fun update(newData: List) { val items = newData.map { - SubscriptionItem(it.uid, it.name, it.notificationMode, it.serviceId, it.url) + SubscriptionItem(it.uid, it.name!!, it.notificationMode, it.serviceId, it.url!!) } submitList(items) } diff --git a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java index d6e2021a1..dd59ba86e 100644 --- a/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java +++ b/app/src/main/java/org/schabi/newpipe/settings/preferencesearch/PreferenceSearchAdapter.java @@ -69,7 +69,8 @@ class PreferenceSearchAdapter } } - private static class PreferenceCallback extends DiffUtil.ItemCallback { + private static final class PreferenceCallback + extends DiffUtil.ItemCallback { @Override public boolean areItemsTheSame(@NonNull final PreferenceSearchItem oldItem, @NonNull final PreferenceSearchItem newItem) { diff --git a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java index 266cec24a..7cdc84e22 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/OggFromWebMWriter.java @@ -1,8 +1,14 @@ package org.schabi.newpipe.streams; +import static org.schabi.newpipe.MainActivity.DEBUG; + +import android.util.Log; +import android.util.Pair; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.streams.WebMReader.Cluster; import org.schabi.newpipe.streams.WebMReader.Segment; import org.schabi.newpipe.streams.WebMReader.SimpleBlock; @@ -13,6 +19,10 @@ import java.io.Closeable; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; /** * @author kapodamy @@ -52,8 +62,10 @@ public class OggFromWebMWriter implements Closeable { private long segmentTableNextTimestamp = TIME_SCALE_NS; private final int[] crc32Table = new int[256]; + private final StreamInfo streamInfo; - public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target) { + public OggFromWebMWriter(@NonNull final SharpStream source, @NonNull final SharpStream target, + @Nullable final StreamInfo streamInfo) { if (!source.canRead() || !source.canRewind()) { throw new IllegalArgumentException("source stream must be readable and allows seeking"); } @@ -63,6 +75,7 @@ public class OggFromWebMWriter implements Closeable { this.source = source; this.output = target; + this.streamInfo = streamInfo; this.streamId = (int) System.currentTimeMillis(); @@ -271,12 +284,31 @@ public class OggFromWebMWriter implements Closeable { @Nullable private byte[] makeMetadata() { + if (DEBUG) { + Log.d("OggFromWebMWriter", "Downloading media with codec ID " + webmTrack.codecId); + } + if ("A_OPUS".equals(webmTrack.codecId)) { - return new byte[]{ - 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string - 0x00, 0x00, 0x00, 0x00, // writing application string size (not present) - 0x00, 0x00, 0x00, 0x00 // additional tags count (zero means no tags) - }; + final var metadata = new ArrayList>(); + if (streamInfo != null) { + metadata.add(Pair.create("COMMENT", streamInfo.getUrl())); + metadata.add(Pair.create("GENRE", streamInfo.getCategory())); + metadata.add(Pair.create("ARTIST", streamInfo.getUploaderName())); + metadata.add(Pair.create("TITLE", streamInfo.getName())); + metadata.add(Pair.create("DATE", streamInfo + .getUploadDate() + .getLocalDateTime() + .format(DateTimeFormatter.ISO_DATE))); + } + + if (DEBUG) { + Log.d("OggFromWebMWriter", "Creating metadata header with this data:"); + metadata.forEach(p -> { + Log.d("OggFromWebMWriter", p.first + "=" + p.second); + }); + } + + return makeOpusTagsHeader(metadata); } else if ("A_VORBIS".equals(webmTrack.codecId)) { return new byte[]{ 0x03, // ¿¿¿??? @@ -290,6 +322,59 @@ public class OggFromWebMWriter implements Closeable { return null; } + /** + * This creates a single metadata tag for use in opus metadata headers. It contains the four + * byte string length field and includes the string as-is. This cannot be used independently, + * but must follow a proper "OpusTags" header. + * + * @param pair A key-value pair in the format "KEY=some value" + * @return The binary data of the encoded metadata tag + */ + private static byte[] makeOpusMetadataTag(final Pair pair) { + final var keyValue = pair.first.toUpperCase() + "=" + pair.second.trim(); + + final var bytes = keyValue.getBytes(); + final var buf = ByteBuffer.allocate(4 + bytes.length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(bytes.length); + buf.put(bytes); + return buf.array(); + } + + /** + * This returns a complete "OpusTags" header, created from the provided metadata tags. + *

+ * You probably want to use makeOpusMetadata(), which uses this function to create + * a header with sensible metadata filled in. + * + * @param keyValueLines A list of pairs of the tags. This can also be though of as a mapping + * from one key to multiple values. + * @return The binary header + */ + private static byte[] makeOpusTagsHeader(final List> keyValueLines) { + final var tags = keyValueLines + .stream() + .filter(p -> !p.second.isBlank()) + .map(OggFromWebMWriter::makeOpusMetadataTag) + .collect(Collectors.toUnmodifiableList()); + + final var tagsBytes = tags.stream().collect(Collectors.summingInt(arr -> arr.length)); + + // Fixed header fields + dynamic fields + final var byteCount = 16 + tagsBytes; + + final var head = ByteBuffer.allocate(byteCount); + head.order(ByteOrder.LITTLE_ENDIAN); + head.put(new byte[]{ + 0x4F, 0x70, 0x75, 0x73, 0x54, 0x61, 0x67, 0x73, // "OpusTags" binary string + 0x00, 0x00, 0x00, 0x00, // vendor (aka. Encoder) string of length 0 + }); + head.putInt(tags.size()); // 4 bytes for tag count + tags.forEach(head::put); // dynamic amount of tag bytes + + return head.array(); + } + private void write(final ByteBuffer buffer) throws IOException { output.write(buffer.array(), 0, buffer.position()); buffer.position(0); diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java index 7aff655a0..8c8dc175b 100644 --- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java +++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java @@ -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"; @@ -24,7 +28,11 @@ public class SrtFromTtmlWriter { private final boolean ignoreEmptyFrames; private final Charset charset = StandardCharsets.UTF_8; - private int frameIndex = 0; + // According to the SubRip (.srt) specification, subtitle + // numbering must start from 1. + // Some players accept 0 or even negative indices, + // but to ensure compliance we start at 1. + private int frameIndex = 1; public SrtFromTtmlWriter(final SharpStream out, final boolean ignoreEmptyFrames) { this.out = out; @@ -39,7 +47,8 @@ public class SrtFromTtmlWriter { private void writeFrame(final String begin, final String end, final StringBuilder text) throws IOException { - writeString(String.valueOf(frameIndex++)); + writeString(String.valueOf(frameIndex)); + frameIndex += 1; writeString(NEW_LINE); writeString(begin); writeString(" --> "); @@ -54,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

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
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
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., ) 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 + /** + *

Recursive method to extract text from all nodes.

+ *

+ * This method processes {@link TextNode}s and {@code
} tags, + * recursively extracting text from nested tags + * (e.g. extracting text from nested {@code } tags). + * Newlines are added for {@code
} tags. + *

+ * @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) { + //
is a self-closing HTML tag used to insert a line break. + if (element.tagName().equalsIgnoreCase("br")) { + // Add a newline for
tags + text.append(NEW_LINE); + } + } + + traverseChildNodesForNestedTags(node, text); + } + // CHECKSTYLE:ON + public void build(final SharpStream ttml) throws IOException { /* * TTML parser with BASIC support @@ -74,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; diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java deleted file mode 100644 index 5aa332159..000000000 --- a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.schabi.newpipe.util; - -import android.content.Context; - -import org.schabi.newpipe.R; - -/** - * Created by Christian Schabesberger on 28.09.17. - * KioskTranslator.java is part of NewPipe. - *

- * NewPipe is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - *

- *

- * NewPipe is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - *

- *

- * You should have received a copy of the GNU General Public License - * along with NewPipe. If not, see . - *

- */ - -public final class KioskTranslator { - private KioskTranslator() { } - - public static String getTranslatedKioskName(final String kioskId, final Context c) { - switch (kioskId) { - case "Trending": - return c.getString(R.string.trending); - case "Top 50": - return c.getString(R.string.top_50); - case "New & hot": - return c.getString(R.string.new_and_hot); - case "Local": - return c.getString(R.string.local); - case "Recently added": - return c.getString(R.string.recently_added); - case "Most liked": - return c.getString(R.string.most_liked); - case "conferences": - return c.getString(R.string.conferences); - case "recent": - return c.getString(R.string.recent); - case "live": - return c.getString(R.string.duration_live); - case "Featured": - return c.getString(R.string.featured); - case "Radio": - return c.getString(R.string.radio); - case "trending_gaming": - return c.getString(R.string.trending_gaming); - case "trending_music": - return c.getString(R.string.trending_music); - case "trending_movies_and_shows": - return c.getString(R.string.trending_movies); - case "trending_podcasts_episodes": - return c.getString(R.string.trending_podcasts); - default: - return kioskId; - } - } - - public static int getKioskIcon(final String kioskId) { - switch (kioskId) { - case "Trending": - case "Top 50": - case "New & hot": - case "conferences": - return R.drawable.ic_whatshot; - case "Local": - return R.drawable.ic_home; - case "Recently added": - case "recent": - return R.drawable.ic_add_circle_outline; - case "Most liked": - return R.drawable.ic_thumb_up; - case "live": - return R.drawable.ic_live_tv; - case "Featured": - return R.drawable.ic_stars; - case "Radio": - return R.drawable.ic_radio; - case "trending_gaming": - return R.drawable.ic_videogame_asset; - case "trending_music": - return R.drawable.ic_music_note; - case "trending_movies_and_shows": - return R.drawable.ic_movie; - case "trending_podcasts_episodes": - return R.drawable.ic_podcasts; - default: - return 0; - } - } -} diff --git a/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt new file mode 100644 index 000000000..1f86f5db7 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/util/KioskTranslator.kt @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2017-2025 NewPipe contributors + * SPDX-FileCopyrightText: 2025 NewPipe e.V. + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package org.schabi.newpipe.util + +import android.content.Context +import org.schabi.newpipe.R + +object KioskTranslator { + @JvmStatic + fun getTranslatedKioskName(kioskId: String, context: Context): String { + return when (kioskId) { + "Trending" -> context.getString(R.string.trending) + "Top 50" -> context.getString(R.string.top_50) + "New & hot" -> context.getString(R.string.new_and_hot) + "Local" -> context.getString(R.string.local) + "Recently added" -> context.getString(R.string.recently_added) + "Most liked" -> context.getString(R.string.most_liked) + "conferences" -> context.getString(R.string.conferences) + "recent" -> context.getString(R.string.recent) + "live" -> context.getString(R.string.duration_live) + "Featured" -> context.getString(R.string.featured) + "Radio" -> context.getString(R.string.radio) + "trending_gaming" -> context.getString(R.string.trending_gaming) + "trending_music" -> context.getString(R.string.trending_music) + "trending_movies_and_shows" -> context.getString(R.string.trending_movies) + "trending_podcasts_episodes" -> context.getString(R.string.trending_podcasts) + else -> kioskId + } + } + + @JvmStatic + fun getKioskIcon(kioskId: String): Int { + return when (kioskId) { + "Trending", "Top 50", "New & hot", "conferences" -> R.drawable.ic_whatshot + "Local" -> R.drawable.ic_home + "Recently added", "recent" -> R.drawable.ic_add_circle_outline + "Most liked" -> R.drawable.ic_thumb_up + "live" -> R.drawable.ic_live_tv + "Featured" -> R.drawable.ic_stars + "Radio" -> R.drawable.ic_radio + "trending_gaming" -> R.drawable.ic_videogame_asset + "trending_music" -> R.drawable.ic_music_note + "trending_movies_and_shows" -> R.drawable.ic_movie + "trending_podcasts_episodes" -> R.drawable.ic_podcasts + else -> 0 + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java index ea41f3e81..409fcb30c 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ListHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ListHelper.java @@ -806,7 +806,7 @@ public final class ListHelper { final Locale preferredLanguage = Localization.getPreferredLocale(context); final boolean preferOriginalAudio = preferences.getBoolean(context.getString(R.string.prefer_original_audio_key), - false); + true); final boolean preferDescriptiveAudio = preferences.getBoolean(context.getString(R.string.prefer_descriptive_audio_key), false); diff --git a/app/src/main/java/org/schabi/newpipe/util/Localization.java b/app/src/main/java/org/schabi/newpipe/util/Localization.java index 1073afffd..49e27d108 100644 --- a/app/src/main/java/org/schabi/newpipe/util/Localization.java +++ b/app/src/main/java/org/schabi/newpipe/util/Localization.java @@ -153,9 +153,9 @@ public final class Localization { case (int) ListExtractor.ITEM_COUNT_UNKNOWN: return ""; case (int) ListExtractor.ITEM_COUNT_INFINITE: - return context.getResources().getString(R.string.infinite_videos); + return context.getString(R.string.infinite_videos); case (int) ListExtractor.ITEM_COUNT_MORE_THAN_100: - return context.getResources().getString(R.string.more_than_100_videos); + return context.getString(R.string.more_than_100_videos); default: return getQuantity(context, R.plurals.videos, R.string.no_videos, streamCount, localizeNumber(streamCount)); @@ -168,9 +168,9 @@ public final class Localization { case (int) ListExtractor.ITEM_COUNT_UNKNOWN: return ""; case (int) ListExtractor.ITEM_COUNT_INFINITE: - return context.getResources().getString(R.string.infinite_videos_mini); + return context.getString(R.string.infinite_videos_mini); case (int) ListExtractor.ITEM_COUNT_MORE_THAN_100: - return context.getResources().getString(R.string.more_than_100_videos_mini); + return context.getString(R.string.more_than_100_videos_mini); default: return String.valueOf(streamCount); } @@ -190,14 +190,20 @@ public final class Localization { final double value = (double) count; if (count >= 1000000000) { - return localizeNumber(round(value / 1000000000)) - + context.getString(R.string.short_billion); + final double shortenedValue = value / 1000000000; + final int scale = shortenedValue >= 100 ? 0 : 1; + return context.getString(R.string.short_billion, + localizeNumber(round(shortenedValue, scale))); } else if (count >= 1000000) { - return localizeNumber(round(value / 1000000)) - + context.getString(R.string.short_million); + final double shortenedValue = value / 1000000; + final int scale = shortenedValue >= 100 ? 0 : 1; + return context.getString(R.string.short_million, + localizeNumber(round(shortenedValue, scale))); } else if (count >= 1000) { - return localizeNumber(round(value / 1000)) - + context.getString(R.string.short_thousand); + final double shortenedValue = value / 1000; + final int scale = shortenedValue >= 100 ? 0 : 1; + return context.getString(R.string.short_thousand, + localizeNumber(round(shortenedValue, scale))); } else { return localizeNumber(value); } @@ -416,8 +422,8 @@ public final class Localization { } } - private static double round(final double value) { - return new BigDecimal(value).setScale(1, RoundingMode.HALF_UP).doubleValue(); + private static double round(final double value, final int scale) { + return new BigDecimal(value).setScale(scale, RoundingMode.HALF_UP).doubleValue(); } private static String getQuantity(@NonNull final Context context, diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java index e1d296297..f702c5bd5 100644 --- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java @@ -57,8 +57,10 @@ import org.schabi.newpipe.local.subscription.SubscriptionFragment; import org.schabi.newpipe.local.subscription.SubscriptionsImportFragment; import org.schabi.newpipe.player.PlayQueueActivity; import org.schabi.newpipe.player.Player; +import org.schabi.newpipe.player.PlayerIntentType; import org.schabi.newpipe.player.PlayerService; import org.schabi.newpipe.player.PlayerType; +import org.schabi.newpipe.player.TimestampChangeData; import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.playqueue.PlayQueue; @@ -67,6 +69,7 @@ import org.schabi.newpipe.settings.SettingsActivity; import org.schabi.newpipe.util.external_communication.ShareUtils; import java.util.List; +import java.util.Optional; public final class NavigationHelper { public static final String MAIN_FRAGMENT_TAG = "main_fragment_tag"; @@ -85,54 +88,32 @@ public final class NavigationHelper { public static Intent getPlayerIntent(@NonNull final Context context, @NonNull final Class targetClazz, @Nullable final PlayQueue playQueue, - final boolean resumePlayback) { - final Intent intent = new Intent(context, targetClazz); - - if (playQueue != null) { - final String cacheKey = SerializedCache.getInstance().put(playQueue, PlayQueue.class); - if (cacheKey != null) { - intent.putExtra(Player.PLAY_QUEUE_KEY, cacheKey); - } - } - intent.putExtra(Player.PLAYER_TYPE, PlayerType.MAIN.valueForIntent()); - intent.putExtra(Player.RESUME_PLAYBACK, resumePlayback); - intent.putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true); - - return intent; + @NonNull final PlayerIntentType playerIntentType) { + final String cacheKey = Optional.ofNullable(playQueue) + .map(queue -> SerializedCache.getInstance().put(queue, PlayQueue.class)) + .orElse(null); + return new Intent(context, targetClazz) + .putExtra(Player.PLAY_QUEUE_KEY, cacheKey) + .putExtra(Player.PLAYER_TYPE, PlayerType.MAIN) + .putExtra(PlayerService.SHOULD_START_FOREGROUND_EXTRA, true) + .putExtra(Player.PLAYER_INTENT_TYPE, playerIntentType); } @NonNull - public static Intent getPlayerIntent(@NonNull final Context context, - @NonNull final Class targetClazz, - @Nullable final PlayQueue playQueue, - final boolean resumePlayback, - final boolean playWhenReady) { - return getPlayerIntent(context, targetClazz, playQueue, resumePlayback) - .putExtra(Player.PLAY_WHEN_READY, playWhenReady); - } - - @NonNull - public static Intent getPlayerEnqueueIntent(@NonNull final Context context, - @NonNull final Class targetClazz, - @Nullable final PlayQueue playQueue) { - // when enqueueing `resumePlayback` is always `false` since: - // - if there is a video already playing, the value of `resumePlayback` just doesn't make - // any difference. - // - if there is nothing already playing, it is useful for the enqueue action to have a - // slightly different behaviour than the normal play action: the latter resumes playback, - // the former doesn't. (note that enqueue can be triggered when nothing is playing only - // by long pressing the video detail fragment, playlist or channel controls - return getPlayerIntent(context, targetClazz, playQueue, false) - .putExtra(Player.ENQUEUE, true); + public static Intent getPlayerTimestampIntent(@NonNull final Context context, + @NonNull final TimestampChangeData data) { + return new Intent(context, PlayerService.class) + .putExtra(Player.PLAYER_INTENT_TYPE, PlayerIntentType.TimestampChange) + .putExtra(Player.PLAYER_INTENT_DATA, data); } @NonNull public static Intent getPlayerEnqueueNextIntent(@NonNull final Context context, @NonNull final Class targetClazz, @Nullable final PlayQueue playQueue) { - // see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false - return getPlayerIntent(context, targetClazz, playQueue, false) - .putExtra(Player.ENQUEUE_NEXT, true); + return getPlayerIntent(context, targetClazz, playQueue, PlayerIntentType.EnqueueNext) + // see comment in `getPlayerEnqueueIntent` as to why `resumePlayback` is false + .putExtra(Player.RESUME_PLAYBACK, false); } /* PLAY */ @@ -166,8 +147,10 @@ public final class NavigationHelper { Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show(); - final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); - intent.putExtra(Player.PLAYER_TYPE, PlayerType.POPUP.valueForIntent()); + final var intent = getPlayerIntent(context, PlayerService.class, queue, + PlayerIntentType.AllOthers) + .putExtra(Player.PLAYER_TYPE, PlayerType.POPUP) + .putExtra(Player.RESUME_PLAYBACK, resumePlayback); ContextCompat.startForegroundService(context, intent); } @@ -177,8 +160,10 @@ public final class NavigationHelper { Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT) .show(); - final Intent intent = getPlayerIntent(context, PlayerService.class, queue, resumePlayback); - intent.putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO.valueForIntent()); + final Intent intent = getPlayerIntent(context, PlayerService.class, queue, + PlayerIntentType.AllOthers) + .putExtra(Player.PLAYER_TYPE, PlayerType.AUDIO) + .putExtra(Player.RESUME_PLAYBACK, resumePlayback); ContextCompat.startForegroundService(context, intent); } @@ -191,9 +176,18 @@ public final class NavigationHelper { } Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show(); - final Intent intent = getPlayerEnqueueIntent(context, PlayerService.class, queue); - intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent()); + // when enqueueing `resumePlayback` is always `false` since: + // - if there is a video already playing, the value of `resumePlayback` just doesn't make + // any difference. + // - if there is nothing already playing, it is useful for the enqueue action to have a + // slightly different behaviour than the normal play action: the latter resumes playback, + // the former doesn't. (note that enqueue can be triggered when nothing is playing only + // by long pressing the video detail fragment, playlist or channel controls + final Intent intent = getPlayerIntent(context, PlayerService.class, queue, + PlayerIntentType.Enqueue) + .putExtra(Player.RESUME_PLAYBACK, false) + .putExtra(Player.PLAYER_TYPE, playerType); ContextCompat.startForegroundService(context, intent); } @@ -215,9 +209,8 @@ public final class NavigationHelper { playerType = PlayerType.AUDIO; } Toast.makeText(context, R.string.enqueued_next, Toast.LENGTH_SHORT).show(); - final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue); - - intent.putExtra(Player.PLAYER_TYPE, playerType.valueForIntent()); + final Intent intent = getPlayerEnqueueNextIntent(context, PlayerService.class, queue) + .putExtra(Player.PLAYER_TYPE, playerType); ContextCompat.startForegroundService(context, intent); } diff --git a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java index 55193599e..969d787d7 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PermissionHelper.java @@ -9,12 +9,15 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.provider.Settings; +import android.text.Html; import android.widget.Toast; import androidx.annotation.RequiresApi; +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; @@ -87,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.getApp().getNotificationsRequested()) { + ActivityCompat.requestPermissions(activity, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, requestCode); + App.getApp().setNotificationsRequested(); + return false; + } } return true; } @@ -113,14 +119,47 @@ public final class PermissionHelper { @RequiresApi(api = Build.VERSION_CODES.M) public static boolean checkSystemAlertWindowPermission(final Context context) { if (!Settings.canDrawOverlays(context)) { - final Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, - Uri.parse("package:" + context.getPackageName())); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - context.startActivity(i); - } catch (final ActivityNotFoundException ignored) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + final Intent i = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + context.getPackageName())); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(i); + } catch (final ActivityNotFoundException ignored) { + } + return false; + // from Android R the ACTION_MANAGE_OVERLAY_PERMISSION will only point to the menu, + // so let’s add a dialog that points the user to the right setting. + } else { + final String appName = context.getApplicationInfo() + .loadLabel(context.getPackageManager()).toString(); + final String title = context.getString(R.string.permission_display_over_apps); + final String permissionName = + context.getString(R.string.permission_display_over_apps_permission_name); + final String appNameItalic = "" + appName + ""; + final String permissionNameItalic = "" + permissionName + ""; + final String message = + context.getString(R.string.permission_display_over_apps_message, + appNameItalic, + permissionNameItalic + ); + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(Html.fromHtml(message, Html.FROM_HTML_MODE_COMPACT)) + .setPositiveButton("OK", (dialog, which) -> { + // we don’t need the package name here, since it won’t do anything on >R + final Intent intent = + new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); + try { + context.startActivity(intent); + } catch (final ActivityNotFoundException ignored) { + } + }) + .setCancelable(true) + .show(); + return false; } - return false; + } else { return true; } diff --git a/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java b/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java index 9727c8083..655ea7092 100644 --- a/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/PlayButtonHelper.java @@ -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; diff --git a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java index 6e9ea7a47..05f26f178 100644 --- a/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java +++ b/app/src/main/java/org/schabi/newpipe/util/SparseItemUtil.java @@ -121,7 +121,7 @@ public final class SparseItemUtil { callback.accept(result); }, throwable -> ErrorUtil.createNotification(context, new ErrorInfo(throwable, UserAction.REQUESTED_STREAM, - "Loading stream info: " + url, serviceId) + "Loading stream info: " + url, serviceId, url) )); } } diff --git a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java index ab74e0305..24a0f457f 100644 --- a/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java +++ b/app/src/main/java/org/schabi/newpipe/util/ThemeHelper.java @@ -263,7 +263,7 @@ public final class ThemeHelper { private static String getSelectedThemeKey(final Context context) { final String themeKey = context.getString(R.string.theme_key); - final String defaultTheme = context.getResources().getString(R.string.default_theme_value); + final String defaultTheme = context.getString(R.string.default_theme_value); return PreferenceManager.getDefaultSharedPreferences(context) .getString(themeKey, defaultTheme); } diff --git a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java index 7524e5413..9fe351b4b 100644 --- a/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java +++ b/app/src/main/java/org/schabi/newpipe/util/external_communication/ShareUtils.java @@ -5,10 +5,9 @@ import static org.schabi.newpipe.MainActivity.DEBUG; import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; @@ -23,6 +22,7 @@ import androidx.core.content.FileProvider; import org.schabi.newpipe.BuildConfig; import org.schabi.newpipe.R; +import org.schabi.newpipe.RouterActivity; import org.schabi.newpipe.extractor.Image; import org.schabi.newpipe.util.image.ImageStrategy; import org.schabi.newpipe.util.image.PicassoHelper; @@ -62,8 +62,9 @@ public final class ShareUtils { } /** - * Open the url with the system default browser. If no browser is set as default, falls back to - * {@link #openAppChooser(Context, Intent, boolean)}. + * Open the url with the system default browser. If no browser is installed, falls back to + * {@link #openAppChooser(Context, Intent, boolean)} (for displaying that no apps are available + * to handle the action, or possible OEM-related edge cases). *

* This function selects the package to open based on which apps respond to the {@code http://} * schema alone, which should exclude special non-browser apps that are can handle the url (e.g. @@ -77,44 +78,26 @@ public final class ShareUtils { * @param url the url to browse **/ public static void openUrlInBrowser(@NonNull final Context context, final String url) { - // Resolve using a generic http://, so we are sure to get a browser and not e.g. the yt app. + // Target a generic http://, so we are sure to get a browser and not e.g. the yt app. // Note that this requires the `http` schema to be added to `` in the manifest. - final ResolveInfo defaultBrowserInfo; final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://")); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - defaultBrowserInfo = context.getPackageManager().resolveActivity(browserIntent, - PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY)); - } else { - defaultBrowserInfo = context.getPackageManager().resolveActivity(browserIntent, - PackageManager.MATCH_DEFAULT_ONLY); - } final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - if (defaultBrowserInfo == null) { - // No app installed to open a web URL, but it may be handled by other apps so try - // opening a system chooser for the link in this case (it could be bypassed by the - // system if there is only one app which can open the link or a default app associated - // with the link domain on Android 12 and higher) + // See https://stackoverflow.com/a/58801285 and `setSelector` documentation + intent.setSelector(browserIntent); + try { + context.startActivity(intent); + } catch (final ActivityNotFoundException e) { + // No browser is available. This should, in the end, yield a nice AOSP error message + // indicating that no app is available to handle this action. + // + // Note: there are some situations where modified OEM ROMs have apps that appear + // to be browsers but are actually app choosers. If starting the Activity fails + // related to this, opening the system app chooser is still the correct behavior. + intent.setSelector(null); openAppChooser(context, intent, true); - return; - } - - final String defaultBrowserPackage = defaultBrowserInfo.activityInfo.packageName; - - if (defaultBrowserPackage.equals("android")) { - // No browser set as default (doesn't work on some devices) - openAppChooser(context, intent, true); - } else { - try { - intent.setPackage(defaultBrowserPackage); - context.startActivity(intent); - } catch (final ActivityNotFoundException e) { - // Not a browser but an app chooser because of OEMs changes - intent.setPackage(null); - openAppChooser(context, intent, true); - } } } @@ -190,6 +173,18 @@ public final class ShareUtils { chooserIntent.putExtra(Intent.EXTRA_TITLE, context.getString(R.string.open_with)); } + // Avoid opening in NewPipe + // (Implementation note: if the URL is one for which NewPipe itself + // is set as handler on Android >= 12, we actually remove the only eligible app + // for this link, and browsers will not be offered to the user. For that, use + // `openUrlInBrowser`.) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + chooserIntent.putExtra( + Intent.EXTRA_EXCLUDE_COMPONENTS, + new ComponentName[]{new ComponentName(context, RouterActivity.class)} + ); + } + // Migrate any clip data and flags from the original intent. final int permFlags = intent.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION diff --git a/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java b/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java index 066515d6b..3288b4347 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/InternalUrlsHandler.java @@ -1,64 +1,27 @@ package org.schabi.newpipe.util.text; import android.content.Context; -import android.util.Log; +import android.content.Intent; +import androidx.core.content.ContextCompat; import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; - -import org.schabi.newpipe.MainActivity; -import org.schabi.newpipe.R; -import org.schabi.newpipe.error.ErrorPanelHelper; import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.linkhandler.LinkHandlerFactory; -import org.schabi.newpipe.extractor.stream.StreamInfo; -import org.schabi.newpipe.player.playqueue.PlayQueue; -import org.schabi.newpipe.player.playqueue.SinglePlayQueue; -import org.schabi.newpipe.util.ExtractorHelper; +import org.schabi.newpipe.player.TimestampChangeData; import org.schabi.newpipe.util.NavigationHelper; import java.util.regex.Matcher; import java.util.regex.Pattern; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.CompositeDisposable; -import io.reactivex.rxjava3.schedulers.Schedulers; - public final class InternalUrlsHandler { - private static final String TAG = InternalUrlsHandler.class.getSimpleName(); - private static final boolean DEBUG = MainActivity.DEBUG; - private static final Pattern AMPERSAND_TIMESTAMP_PATTERN = Pattern.compile("(.*)&t=(\\d+)"); - private static final Pattern HASHTAG_TIMESTAMP_PATTERN = - Pattern.compile("(.*)#timestamp=(\\d+)"); private InternalUrlsHandler() { } - /** - * Handle a YouTube timestamp comment URL in NewPipe. - *

- * This method will check if the provided url is a YouTube comment description URL ({@code - * https://www.youtube.com/watch?v=}video_id{@code #timestamp=}time_in_seconds). If yes, the - * popup player will be opened when the user will click on the timestamp in the comment, - * at the time and for the video indicated in the timestamp. - * - * @param disposables a field of the Activity/Fragment class that calls this method - * @param context the context to use - * @param url the URL to check if it can be handled - * @return true if the URL can be handled by NewPipe, false if it cannot - */ - public static boolean handleUrlCommentsTimestamp(@NonNull final CompositeDisposable - disposables, - final Context context, - @NonNull final String url) { - return handleUrl(context, url, HASHTAG_TIMESTAMP_PATTERN, disposables); - } - /** * Handle a YouTube timestamp description URL in NewPipe. *

@@ -67,36 +30,13 @@ public final class InternalUrlsHandler { * player will be opened when the user will click on the timestamp in the video description, * at the time and for the video indicated in the timestamp. * - * @param disposables a field of the Activity/Fragment class that calls this method * @param context the context to use * @param url the URL to check if it can be handled * @return true if the URL can be handled by NewPipe, false if it cannot */ - public static boolean handleUrlDescriptionTimestamp(@NonNull final CompositeDisposable - disposables, - final Context context, + public static boolean handleUrlDescriptionTimestamp(final Context context, @NonNull final String url) { - return handleUrl(context, url, AMPERSAND_TIMESTAMP_PATTERN, disposables); - } - - /** - * Handle an URL in NewPipe. - *

- * This method will check if the provided url can be handled in NewPipe or not. If this is a - * service URL with a timestamp, the popup player will be opened and true will be returned; - * else, false will be returned. - * - * @param context the context to use - * @param url the URL to check if it can be handled - * @param pattern the pattern to use - * @param disposables a field of the Activity/Fragment class that calls this method - * @return true if the URL can be handled by NewPipe, false if it cannot - */ - private static boolean handleUrl(final Context context, - @NonNull final String url, - @NonNull final Pattern pattern, - @NonNull final CompositeDisposable disposables) { - final Matcher matcher = pattern.matcher(url); + final Matcher matcher = AMPERSAND_TIMESTAMP_PATTERN.matcher(url); if (!matcher.matches()) { return false; } @@ -121,7 +61,7 @@ public final class InternalUrlsHandler { } if (linkType == StreamingService.LinkType.STREAM && seconds != -1) { - return playOnPopup(context, matchedUrl, service, seconds, disposables); + return playOnPopup(context, matchedUrl, service, seconds); } else { NavigationHelper.openRouterActivity(context, matchedUrl); return true; @@ -135,15 +75,12 @@ public final class InternalUrlsHandler { * @param url the URL of the content * @param service the service of the content * @param seconds the position in seconds at which the floating player will start - * @param disposables disposables created by the method are added here and their lifecycle - * should be handled by the calling class * @return true if the playback of the content has successfully started or false if not */ public static boolean playOnPopup(final Context context, final String url, @NonNull final StreamingService service, - final int seconds, - @NonNull final CompositeDisposable disposables) { + final int seconds) { final LinkHandlerFactory factory = service.getStreamLHFactory(); final String cleanUrl; @@ -153,25 +90,14 @@ public final class InternalUrlsHandler { return false; } - final Single single = - ExtractorHelper.getStreamInfo(service.getServiceId(), cleanUrl, false); - disposables.add(single.subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(info -> { - final PlayQueue playQueue = - new SinglePlayQueue(info, seconds * 1000L); - NavigationHelper.playOnPopupPlayer(context, playQueue, false); - }, throwable -> { - if (DEBUG) { - Log.e(TAG, "Could not play on popup: " + url, throwable); - } - new AlertDialog.Builder(context) - .setTitle(R.string.player_stream_failure) - .setMessage( - ErrorPanelHelper.Companion.getExceptionDescription(throwable)) - .setPositiveButton(R.string.ok, null) - .show(); - })); + final Intent intent = NavigationHelper.getPlayerTimestampIntent(context, + new TimestampChangeData( + service.getServiceId(), + cleanUrl, + seconds + )); + ContextCompat.startForegroundService(context, intent); + return true; } } diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java b/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java index 1419ac85a..4221da398 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/TextLinkifier.java @@ -192,7 +192,7 @@ public final class TextLinkifier { *

* Instead of using an {@link android.content.Intent#ACTION_VIEW} intent in the description of * a content, this method will parse the {@link CharSequence} and replace all current web links - * with {@link ShareUtils#openUrlInBrowser(Context, String, boolean)}. + * with {@link ShareUtils#openUrlInBrowser(Context, String)}. *

* *

@@ -240,7 +240,7 @@ public final class TextLinkifier { for (final URLSpan span : urls) { final String url = span.getURL(); final LongPressClickableSpan longPressClickableSpan = - new UrlLongPressClickableSpan(context, disposables, url); + new UrlLongPressClickableSpan(context, url); textBlockLinked.setSpan(longPressClickableSpan, textBlockLinked.getSpanStart(span), diff --git a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java index f5864794a..35a9fd996 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/TimestampLongPressClickableSpan.java @@ -46,7 +46,7 @@ final class TimestampLongPressClickableSpan extends LongPressClickableSpan { @Override public void onClick(@NonNull final View view) { playOnPopup(context, relatedStreamUrl, relatedInfoService, - timestampMatchDTO.seconds(), disposables); + timestampMatchDTO.seconds()); } @Override diff --git a/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java b/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java index 61c1a546d..ec3cefc62 100644 --- a/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java +++ b/app/src/main/java/org/schabi/newpipe/util/text/UrlLongPressClickableSpan.java @@ -7,29 +7,22 @@ import androidx.annotation.NonNull; import org.schabi.newpipe.util.external_communication.ShareUtils; -import io.reactivex.rxjava3.disposables.CompositeDisposable; - final class UrlLongPressClickableSpan extends LongPressClickableSpan { @NonNull private final Context context; @NonNull - private final CompositeDisposable disposables; - @NonNull private final String url; UrlLongPressClickableSpan(@NonNull final Context context, - @NonNull final CompositeDisposable disposables, @NonNull final String url) { this.context = context; - this.disposables = disposables; this.url = url; } @Override public void onClick(@NonNull final View view) { - if (!InternalUrlsHandler.handleUrlDescriptionTimestamp( - disposables, context, url)) { + if (!InternalUrlsHandler.handleUrlDescriptionTimestamp(context, url)) { ShareUtils.openUrlInApp(context, url); } } diff --git a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java index 175c81e46..7452fff09 100644 --- a/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java +++ b/app/src/main/java/org/schabi/newpipe/views/ExpandableSurfaceView.java @@ -35,12 +35,12 @@ public class ExpandableSurfaceView extends SurfaceView { && resizeMode != RESIZE_MODE_FIT && verticalVideo ? maxHeight : baseHeight; - if (height == 0) { + if (width == 0 || height == 0) { return; } final float viewAspectRatio = width / ((float) height); - final float aspectDeformation = videoAspectRatio / viewAspectRatio - 1; + final float aspectDeformation = (videoAspectRatio / viewAspectRatio) - 1; scaleX = 1.0f; scaleY = 1.0f; @@ -100,7 +100,7 @@ public class ExpandableSurfaceView extends SurfaceView { } public void setAspectRatio(final float aspectRatio) { - if (videoAspectRatio == aspectRatio) { + if (videoAspectRatio == aspectRatio || aspectRatio == 0 || !Float.isFinite(aspectRatio)) { return; } diff --git a/app/src/main/java/us/shandian/giga/get/DownloadMission.java b/app/src/main/java/us/shandian/giga/get/DownloadMission.java index 04930b002..54340ce5d 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadMission.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadMission.java @@ -661,7 +661,8 @@ public class DownloadMission extends Mission { * @return {@code true}, if storage is invalid and cannot be used */ public boolean hasInvalidStorage() { - return errCode == ERROR_PROGRESS_LOST || storage == null || !storage.existsAsFile(); + // Don't consider ERROR_PROGRESS_LOST as invalid storage - it can be recovered + return storage == null || !storage.existsAsFile(); } /** diff --git a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java index eed5db463..1d2483e79 100644 --- a/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java +++ b/app/src/main/java/us/shandian/giga/get/DownloadRunnableFallback.java @@ -85,6 +85,7 @@ public class DownloadRunnableFallback extends Thread { if (mMission.unknownLength || mConn.getResponseCode() == 200) { // restart amount of bytes downloaded mMission.done = mMission.offsets[mMission.current] - mMission.offsets[0]; + start = 0; // reset position to avoid writing at wrong offset } mF = mMission.storage.getStream(); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java index dc46ced5d..badb5f7ed 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/OggFromWebmDemuxer.java @@ -34,7 +34,7 @@ class OggFromWebmDemuxer extends Postprocessing { @Override int process(SharpStream out, @NonNull SharpStream... sources) throws IOException { - OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out); + OggFromWebMWriter demuxer = new OggFromWebMWriter(sources[0], out, streamInfo); demuxer.parseSource(); demuxer.selectTrack(0); demuxer.build(); diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java index 7f5c85d27..1c9143252 100644 --- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java +++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java @@ -4,6 +4,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.streams.io.SharpStream; import java.io.File; @@ -30,7 +31,8 @@ public abstract class Postprocessing implements Serializable { public transient static final String ALGORITHM_M4A_NO_DASH = "mp4D-m4a"; public transient static final String ALGORITHM_OGG_FROM_WEBM_DEMUXER = "webm-ogg-d"; - public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args) { + public static Postprocessing getAlgorithm(@NonNull String algorithmName, String[] args, + StreamInfo streamInfo) { Postprocessing instance; switch (algorithmName) { @@ -56,6 +58,7 @@ public abstract class Postprocessing implements Serializable { } instance.args = args; + instance.streamInfo = streamInfo; return instance; } @@ -75,8 +78,8 @@ public abstract class Postprocessing implements Serializable { */ private final String name; - private String[] args; + protected StreamInfo streamInfo; private transient DownloadMission mission; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 9b90fa14b..7a2055aaa 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -24,6 +24,8 @@ import org.schabi.newpipe.streams.io.StoredFileHelper; import us.shandian.giga.util.Utility; import static org.schabi.newpipe.BuildConfig.DEBUG; +import static us.shandian.giga.get.DownloadMission.ERROR_NOTHING; +import static us.shandian.giga.get.DownloadMission.ERROR_PROGRESS_LOST; public class DownloadManager { private static final String TAG = DownloadManager.class.getSimpleName(); @@ -149,12 +151,31 @@ public class DownloadManager { if (sub.getName().equals(".tmp")) continue; DownloadMission mis = Utility.readFromFile(sub); - if (mis == null || mis.isFinished() || mis.hasInvalidStorage()) { + if (mis == null) { //noinspection ResultOfMethodCallIgnored sub.delete(); continue; } + // DON'T delete missions that are truly finished - let them be moved to finished list + if (mis.isFinished()) { + // Move to finished missions instead of deleting + setFinished(mis); + //noinspection ResultOfMethodCallIgnored + sub.delete(); + continue; + } + + // DON'T delete missions with storage issues - try to recover them + if (mis.hasInvalidStorage() && mis.errCode != ERROR_PROGRESS_LOST) { + // Only delete if it's truly unrecoverable (not just progress lost) + if (mis.storage == null) { + //noinspection ResultOfMethodCallIgnored + sub.delete(); + continue; + } + } + mis.threads = new Thread[0]; boolean exists; @@ -163,16 +184,13 @@ public class DownloadManager { exists = !mis.storage.isInvalid() && mis.storage.existsAsFile(); } catch (Exception ex) { Log.e(TAG, "Failed to load the file source of " + mis.storage.toString(), ex); - mis.storage.invalidate(); + // Don't invalidate storage immediately - try to recover first exists = false; } if (mis.isPsRunning()) { if (mis.psAlgorithm.worksOnSameFile) { // Incomplete post-processing results in a corrupted download file - // because the selected algorithm works on the same file to save space. - // the file will be deleted if the storage API - // is Java IO (avoid showing the "Save as..." dialog) if (exists && mis.storage.isDirect() && !mis.storage.delete()) Log.w(TAG, "Unable to delete incomplete download file: " + sub.getPath()); } @@ -181,10 +199,11 @@ public class DownloadManager { mis.errCode = DownloadMission.ERROR_POSTPROCESSING_STOPPED; } else if (!exists) { tryRecover(mis); - - // the progress is lost, reset mission state - if (mis.isInitialized()) - mis.resetState(true, true, DownloadMission.ERROR_PROGRESS_LOST); + // Keep the mission even if recovery fails - don't reset to ERROR_PROGRESS_LOST + // This allows user to see the failed download and potentially retry + if (mis.isInitialized() && mis.errCode == ERROR_NOTHING) { + mis.resetState(true, true, ERROR_PROGRESS_LOST); + } } if (mis.psAlgorithm != null) { @@ -265,7 +284,7 @@ public class DownloadManager { } } - public void deleteMission(Mission mission) { + public void deleteMission(Mission mission, boolean alsoDeleteFile) { synchronized (this) { if (mission instanceof DownloadMission) { mMissionsPending.remove(mission); @@ -274,7 +293,9 @@ public class DownloadManager { mFinishedMissionStore.deleteMission(mission); } - mission.delete(); + if (alsoDeleteFile) { + mission.delete(); + } } } @@ -446,7 +467,7 @@ public class DownloadManager { continue; resumeMission(mission); - if (mission.errCode != DownloadMission.ERROR_NOTHING) continue; + if (mission.errCode != ERROR_NOTHING) continue; if (mPrefQueueLimit) return true; flag = true; @@ -510,6 +531,15 @@ public class DownloadManager { } } + public boolean canRecoverMission(DownloadMission mission) { + if (mission == null) return false; + + // Can recover missions with progress lost or storage issues + return mission.errCode == ERROR_PROGRESS_LOST || + mission.storage == null || + !mission.storage.existsAsFile(); + } + public MissionState checkForExistingMission(StoredFileHelper storage) { synchronized (this) { DownloadMission pending = getPendingMission(storage); @@ -582,8 +612,13 @@ public class DownloadManager { ArrayList finished = new ArrayList<>(mMissionsFinished); List remove = new ArrayList<>(hidden); - // hide missions (if required) - remove.removeIf(mission -> pending.remove(mission) || finished.remove(mission)); + // Don't hide recoverable missions + remove.removeIf(mission -> { + if (mission instanceof DownloadMission dm && canRecoverMission(dm)) { + return false; // Don't remove recoverable missions + } + return pending.remove(mission) || finished.remove(mission); + }); int fakeTotal = pending.size(); if (fakeTotal > 0) fakeTotal++; diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java index 45211211f..76da18b2d 100755 --- a/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManagerService.java @@ -40,6 +40,7 @@ import androidx.preference.PreferenceManager; import org.schabi.newpipe.R; import org.schabi.newpipe.download.DownloadActivity; +import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.player.helper.LockManager; import org.schabi.newpipe.streams.io.StoredDirectoryHelper; import org.schabi.newpipe.streams.io.StoredFileHelper; @@ -74,12 +75,12 @@ public class DownloadManagerService extends Service { private static final String EXTRA_THREADS = "DownloadManagerService.extra.threads"; private static final String EXTRA_POSTPROCESSING_NAME = "DownloadManagerService.extra.postprocessingName"; private static final String EXTRA_POSTPROCESSING_ARGS = "DownloadManagerService.extra.postprocessingArgs"; - private static final String EXTRA_SOURCE = "DownloadManagerService.extra.source"; private static final String EXTRA_NEAR_LENGTH = "DownloadManagerService.extra.nearLength"; private static final String EXTRA_PATH = "DownloadManagerService.extra.storagePath"; private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath"; private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag"; private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo"; + private static final String EXTRA_STREAM_INFO = "DownloadManagerService.extra.streamInfo"; private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished"; private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished"; @@ -353,13 +354,13 @@ public class DownloadManagerService extends Service { * @param kind type of file (a: audio v: video s: subtitle ?: file-extension defined) * @param threads the number of threads maximal used to download chunks of the file. * @param psName the name of the required post-processing algorithm, or {@code null} to ignore. - * @param source source url of the resource + * @param streamInfo stream metadata that may be written into the downloaded file. * @param psArgs the arguments for the post-processing algorithm. * @param nearLength the approximated final length of the file * @param recoveryInfo array of MissionRecoveryInfo, in case is required recover the download */ public static void startMission(Context context, String[] urls, StoredFileHelper storage, - char kind, int threads, String source, String psName, + char kind, int threads, StreamInfo streamInfo, String psName, String[] psArgs, long nearLength, ArrayList recoveryInfo) { final Intent intent = new Intent(context, DownloadManagerService.class) @@ -367,14 +368,14 @@ public class DownloadManagerService extends Service { .putExtra(EXTRA_URLS, urls) .putExtra(EXTRA_KIND, kind) .putExtra(EXTRA_THREADS, threads) - .putExtra(EXTRA_SOURCE, source) .putExtra(EXTRA_POSTPROCESSING_NAME, psName) .putExtra(EXTRA_POSTPROCESSING_ARGS, psArgs) .putExtra(EXTRA_NEAR_LENGTH, nearLength) .putExtra(EXTRA_RECOVERY_INFO, recoveryInfo) .putExtra(EXTRA_PARENT_PATH, storage.getParentUri()) .putExtra(EXTRA_PATH, storage.getUri()) - .putExtra(EXTRA_STORAGE_TAG, storage.getTag()); + .putExtra(EXTRA_STORAGE_TAG, storage.getTag()) + .putExtra(EXTRA_STREAM_INFO, streamInfo); context.startService(intent); } @@ -387,9 +388,9 @@ public class DownloadManagerService extends Service { char kind = intent.getCharExtra(EXTRA_KIND, '?'); String psName = intent.getStringExtra(EXTRA_POSTPROCESSING_NAME); String[] psArgs = intent.getStringArrayExtra(EXTRA_POSTPROCESSING_ARGS); - String source = intent.getStringExtra(EXTRA_SOURCE); long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0); String tag = intent.getStringExtra(EXTRA_STORAGE_TAG); + StreamInfo streamInfo = (StreamInfo)intent.getSerializableExtra(EXTRA_STREAM_INFO); final var recovery = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_RECOVERY_INFO, MissionRecoveryInfo.class); Objects.requireNonNull(recovery); @@ -405,11 +406,11 @@ public class DownloadManagerService extends Service { if (psName == null) ps = null; else - ps = Postprocessing.getAlgorithm(psName, psArgs); + ps = Postprocessing.getAlgorithm(psName, psArgs, streamInfo); final DownloadMission mission = new DownloadMission(urls, storage, kind, ps); mission.threadCount = threads; - mission.source = source; + mission.source = streamInfo.getUrl(); mission.nearLength = nearLength; mission.recoveryInfo = recovery.toArray(new MissionRecoveryInfo[0]); diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 9722a9a1f..54ae2cfa4 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -2,6 +2,7 @@ package us.shandian.giga.ui.adapter; import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; import static android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; +import static android.content.Intent.createChooser; import static us.shandian.giga.get.DownloadMission.ERROR_CONNECT_HOST; import static us.shandian.giga.get.DownloadMission.ERROR_FILE_CREATION; import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_NO_CONTENT; @@ -349,11 +350,15 @@ public class MissionAdapter extends Adapter implements Handler.Callb if (BuildConfig.DEBUG) Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider"); - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndType(resolveShareableUri(mission), mimeType); - intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); - intent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); - ShareUtils.openIntentInApp(mContext, intent); + Intent viewIntent = new Intent(Intent.ACTION_VIEW); + viewIntent.setDataAndType(resolveShareableUri(mission), mimeType); + viewIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); + viewIntent.addFlags(FLAG_GRANT_PREFIX_URI_PERMISSION); + + Intent chooserIntent = createChooser(viewIntent, null); + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | FLAG_GRANT_READ_URI_PERMISSION); + + ShareUtils.openIntentInApp(mContext, chooserIntent); } private void shareFile(Mission mission) { @@ -364,8 +369,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb shareIntent.putExtra(Intent.EXTRA_STREAM, resolveShareableUri(mission)); shareIntent.addFlags(FLAG_GRANT_READ_URI_PERMISSION); - final Intent intent = new Intent(Intent.ACTION_CHOOSER); - intent.putExtra(Intent.EXTRA_INTENT, shareIntent); + final Intent intent = createChooser(shareIntent, null); // unneeded to set a title to the chooser on Android P and higher because the system // ignores this title on these versions if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { @@ -563,16 +567,16 @@ public class MissionAdapter extends Adapter implements Handler.Callb } request.append("]"); - String service; + Integer service; try { - service = NewPipe.getServiceByUrl(mission.source).getServiceInfo().getName(); + service = NewPipe.getServiceByUrl(mission.source).getServiceId(); } catch (Exception e) { - service = ErrorInfo.SERVICE_NONE; + service = null; } ErrorUtil.createNotification(mContext, new ErrorInfo(ErrorInfo.Companion.throwableToStringList(mission.errObject), action, - service, request.toString(), reason)); + request.toString(), service, reason)); } public void clearFinishedDownloads(boolean delete) { @@ -614,7 +618,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb while (i.hasNext()) { Mission mission = i.next(); if (mission != null) { - mDownloadManager.deleteMission(mission); + mDownloadManager.deleteMission(mission, true); mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); } i.remove(); @@ -667,7 +671,14 @@ public class MissionAdapter extends Adapter implements Handler.Callb shareFile(h.item.mission); return true; case R.id.delete: - mDeleter.append(h.item.mission); + // delete the entry and the file + mDeleter.append(h.item.mission, true); + applyChanges(); + checkMasterButtonsVisibility(); + return true; + case R.id.delete_entry: + // just delete the entry + mDeleter.append(h.item.mission, false); applyChanges(); checkMasterButtonsVisibility(); return true; @@ -676,7 +687,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb final StoredFileHelper storage = h.item.mission.storage; if (!storage.existsAsFile()) { Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show(); - mDeleter.append(h.item.mission); + mDeleter.append(h.item.mission, true); applyChanges(); return true; } diff --git a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java index 1902076d6..0f285fd74 100644 --- a/app/src/main/java/us/shandian/giga/ui/common/Deleter.java +++ b/app/src/main/java/us/shandian/giga/ui/common/Deleter.java @@ -13,7 +13,9 @@ import com.google.android.material.snackbar.Snackbar; import org.schabi.newpipe.R; import java.util.ArrayList; +import java.util.Optional; +import kotlin.Pair; import us.shandian.giga.get.FinishedMission; import us.shandian.giga.get.Mission; import us.shandian.giga.service.DownloadManager; @@ -30,7 +32,8 @@ public class Deleter { private static final int DELAY_RESUME = 400;// ms private Snackbar snackbar; - private ArrayList items; + // list of missions to be deleted, and whether to also delete the corresponding file + private ArrayList> items; private boolean running = true; private final Context mContext; @@ -51,7 +54,7 @@ public class Deleter { items = new ArrayList<>(2); } - public void append(Mission item) { + public void append(Mission item, boolean alsoDeleteFile) { /* If a mission is removed from the list while the Snackbar for a previously * removed item is still showing, commit the action for the previous item * immediately. This prevents Snackbars from stacking up in reverse order. @@ -60,13 +63,13 @@ public class Deleter { commit(); mIterator.hide(item); - items.add(0, item); + items.add(0, new Pair<>(item, alsoDeleteFile)); show(); } private void forget() { - mIterator.unHide(items.remove(0)); + mIterator.unHide(items.remove(0).getFirst()); mAdapter.applyChanges(); show(); @@ -84,7 +87,19 @@ public class Deleter { private void next() { if (items.size() < 1) return; - String msg = mContext.getString(R.string.file_deleted).concat(":\n").concat(items.get(0).storage.getName()); + final Optional fileToBeDeleted = items.stream() + .filter(Pair::getSecond) + .map(p -> p.getFirst().storage.getName()) + .findFirst(); + + String msg; + if (fileToBeDeleted.isPresent()) { + msg = mContext.getString(R.string.file_deleted) + .concat(":\n") + .concat(fileToBeDeleted.get()); + } else { + msg = mContext.getString(R.string.entry_deleted); + } snackbar = Snackbar.make(mView, msg, Snackbar.LENGTH_INDEFINITE); snackbar.setAction(R.string.undo, s -> forget()); @@ -98,11 +113,13 @@ public class Deleter { if (items.size() < 1) return; while (items.size() > 0) { - Mission mission = items.remove(0); + Pair missionAndAlsoDeleteFile = items.remove(0); + Mission mission = missionAndAlsoDeleteFile.getFirst(); + boolean alsoDeleteFile = missionAndAlsoDeleteFile.getSecond(); if (mission.deleted) continue; mIterator.unHide(mission); - mDownloadManager.deleteMission(mission); + mDownloadManager.deleteMission(mission, alsoDeleteFile); if (mission instanceof FinishedMission) { mContext.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, mission.storage.getUri())); @@ -137,7 +154,11 @@ public class Deleter { pause(); - for (Mission mission : items) mDownloadManager.deleteMission(mission); + for (Pair missionAndAlsoDeleteFile : items) { + Mission mission = missionAndAlsoDeleteFile.getFirst(); + boolean alsoDeleteFile = missionAndAlsoDeleteFile.getSecond(); + mDownloadManager.deleteMission(mission, alsoDeleteFile); + } items = null; } } diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml index 99b514bb0..a6a0884c7 100644 --- a/app/src/main/res/layout/player.xml +++ b/app/src/main/res/layout/player.xml @@ -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" /> + android:orientation="horizontal"> - + android:layout_marginTop="6dp" + android:layout_marginEnd="8dp" + android:layout_weight="1" + android:gravity="top" + android:orientation="vertical" + tools:ignore="NestedWeights,RtlHardcoded"> - + + + + + + + android:layout_weight="1"> + + + + - - @@ -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" /> @@ -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" /> + android:title="@string/delete_file" /> + + الملفات المحملة لا يوجد مثل هذا الملف/مصدر المحتوى الأكثر إعجابًا - بليون تعذر تحميل موجز \'%s\'. ؟ التحقق من وجود تحديثات مثيلات خوادم پيرتيوب +100 فيديو - ألف مثيل الخادم موجود بالفعل طلب تأكيد قبل مسح قائمة الانتظار المشتركون @@ -643,13 +641,11 @@ الصوت : %s خطوة حل - %s يقدم هذا السبب: الدفق المحدد غير مدعوم من قبل المشغلون الخارجيون عن تطبيق نيوپايپ تسريع إلى الأمام/-ترجيع وقت البحث تم رفضها من قبل النظام ليس هناك تعليقات - مليون جاري التحقق من وجود تحديثات… المحتوى اسأل عن مكان التنزيل diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 7e002d88a..895314ad5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -116,9 +116,6 @@ لا شيء هنا سوى الصراصير الصوت إعادة المحاولة - ألف - مليون - بليون ليس هناك مشترِكون %s مشارك @@ -652,7 +649,6 @@ تمكين تحديد نص في الوصف يمكنك الآن تحديد نص داخل الوصف. لاحظ أن الصفحة قد تومض وقد لا تكون الروابط قابلة للنقر أثناء وضع التحديد. فتح الموقع - %s يقدم هذا السبب: تم إنهاء الحساب لا يوفر وضع التغذية السريعة مزيدًا من المعلومات حول هذا الموضوع. حساب منشئ المحتوى قد تم إنهائه. @@ -890,4 +886,24 @@ البحث %1$s (%2$s) تمت إزالة صفحة أفضل 50 من SoundCloud أوقفت SoundCloud صفحة أفضل 50 الأصلية. تمت إزالة علامة التبويب المقابلة من صفحتك الرئيسية. + تمت إزالة تريندات YouTube المجمعة + أوقف YouTube صفحة الترند المدمجة اعتبارًا من 21 يوليو 2025. استبدلت NewPipe صفحة الموضوعات المتداولة الافتراضية بصفحة الموضوعات المتداولة الشائعة مع البث المباشر المتداول.\n\nيمكنك أيضًا تحديد صفحات رائجة مختلفة في \"الإعدادات > المحتوى > محتوى الصفحة الرئيسية\". + توجهات الألعاب + توجهات البث الصوتي + الأفلام والعروض الأكثر رواجاً + الموسيقى الرائجة + %s الف + %s مليون + %sمليار + لاستخدام المشغل المنبثق، يرجى تحديد %1$s في قائمة إعدادات اندرويد التالية وتمكين %2$s. + “السماح بالعرض فوق التطبيقات الاخرى” + حذف ملف + حذف المدخلات + تم إنهاء الحساب\n\n%1$s يقدم هذا السبب: %2$s + تم حذف المدخلات + تم تلقي خطأ HTTP 403 من الخادم أثناء التشغيل، ويرجح أن يكون السبب هو انتهاء صلاحية عنوان URL للبث أو حظر عنوان IP + حدث خطأ HTTP %1$s من الخادم أثناء التشغيل + تم تلقي خطأ HTTP 403 من الخادم أثناء التشغيل، ويرجح أن يكون السبب هو حظر عنوان IP أو مشكلات في إزالة التعتيم عن عنوان URL للبث + رفض %1$s تقديم البيانات، وطلب تسجيل الدخول للتأكد من أن الطالب ليس روبوتًا.\n\nربما تم حظر عنوان IP الخاص بك مؤقتًا من قبل %1$s، يمكنك الانتظار بعض الوقت أو التبديل إلى عنوان IP مختلف (على سبيل المثال عن طريق تشغيل/إيقاف تشغيل VPN، أو التبديل من WiFi إلى بيانات الهاتف المحمول). + هذا المحتوى غير متاح للبلد المحدد حاليًا.\n\nقم بتغيير اختيارك من ”الإعدادات > المحتوى > البلد الافتراضي للمحتوى“. diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 682dda98f..689cf4937 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -192,10 +192,10 @@ Sil Hələ ki, kanal abunəliyi yoxdur Kanal seç - Kanal Səhifəsi + Kanal səhifəsi Standart Bölmə - Kənar Səhifə - Boş Səhifə + Kənar səhifə + Boş səhifə Əsas səhifədə hansı tablar göstərilir Əsas səhifə məzmunu Yeni versiya mövcud olduqda tətbiq yeniləməsini xatırlatmaq üçün bildiriş göstər @@ -295,11 +295,8 @@ Nə baş verdi: Yükləyənin avatar miniatürü Bəyən - Bəyənmə + Bəyənməmə Yenidən sıralamaq üçün sürüklə - min - Mln - Mlrd Xidməti dəyiş, hazırda seçilmiş: Abunəçi yoxdur Baxış yoxdur @@ -504,7 +501,6 @@ Endirmə növbəsini məhdudlaşdır Eyni vaxtda ancaq bir endirmə həyata keçiriləcək Hesab ləğv edildi - %s bu səbəbi təmin edir: Yükləmə başladı Açıqlamadakı mətni seçməyi qeyri-aktiv et Kateqoriya @@ -589,7 +585,7 @@ Serveri təsdiqləmək mümkün olmadı %s-də bəyəndiyiniz serverləri tapın Video \"Təfsilatlar\" səhifəsində fon və ya ani görüntü düyməsin basarkən ipucu göstər - Oynadıcı titr mətn miqyasını və arxa fon üslublarını dəyişdir. Effektiv olması üçün tətbiqi yenidən başlatmaq tələb olunur + Oynadıcı titr mətn miqyasını və arxa plan üslublarını dəyişdir. Effektiv olması üçün tətbiqi yenidən başlatmaq tələb olunur Xəta baş verdi: %1$s Fayl mövcud deyil, yaxud oxumaq və ya yazmaq icazəsi yoxdur Veb saytı təhlil etmək alınmadı @@ -807,4 +803,29 @@ Axın qrupu seçin Hələ heç bir axın qrupu yaradılmayıb Kanal qrupu səhifəsi + %1$s axtar + %1$s (%2$s) axtar + Bəyənmə + SoundCloud Top 50 səhifəsi silindi + SoundCloud ilk Ən yaxşı 50 siyahısın ləğv etdi. Uyğun səhifə əsas səhifənizdən silindi. + %sMin + %sMln + %sMlrd + YouTube birləşmiş trend silindi + 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. + Trenddə olan Oyun + Trenddə olan podkastlar + Trend film və tamaşalar + Trenddə olan musiqilər + 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. + \"Digər tətbiqlər üzərində göstərməyə icazə verin\" + Faylı sil + Girişi silin + Giriş silindi + Hesab ləğv edilib\n\n %1$s bu səbəbi təmin edir: %2$s + 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 + HTTP xətası %1$s oynadarkən serverdən alındı + 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 + %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ə). + 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. diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml index 51b4fdec0..81b212f80 100644 --- a/app/src/main/res/values-b+ast/strings.xml +++ b/app/src/main/res/values-b+ast/strings.xml @@ -43,9 +43,6 @@ Tarrezmes Formatu de videu predetermináu Prietu - mil - mill. - mil mill. Precísase esti permisu p\'abrir \nnel mou ventanu Retu de reCAPTCHA diff --git a/app/src/main/res/values-b+uz+Latn/strings.xml b/app/src/main/res/values-b+uz+Latn/strings.xml index b556da756..2be37ea7c 100644 --- a/app/src/main/res/values-b+uz+Latn/strings.xml +++ b/app/src/main/res/values-b+uz+Latn/strings.xml @@ -267,9 +267,6 @@ Obunachilar yo\'q Hozirda tanlangan xizmatni yoqish: - B - M - k Qayta Audio Video diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 9f40ae48f..ef7ef98c3 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -85,7 +85,7 @@ Прайграванне ў фонавым рэжыме Прайграванне ва ўсплывальным акне Кантэнт - Паказваць кантэнт 18+ + Кантэнт з ўзроставым абмежаваннем Ужывую Спампоўкі Спампоўкі @@ -106,8 +106,8 @@ Апавяшчэнне NewPipe Апавяшчэнні для прайгравальніка NewPipe [Невядома] - Перайсці ў фон - Перайсці ў акно + Перайсці ў фонавы рэжым + Перайсці ў аконны рэжым Перайсці ў галоўнае акно Імпартаваць даныя Экспартаваць даныя @@ -159,9 +159,6 @@ Відэа Аўдыя Паспрабаваць зноў - тыс. - млн - млрд Няма падпісчыкаў %s падпісчык @@ -430,7 +427,7 @@ Уключыць гук Адключыць гук Дадаць у чаргу - Даданае ў чаргу + Дададзена у чаргу Чарга прайгравання Найбольш папулярнае Лакальнае @@ -579,7 +576,7 @@ Шукайце серверы, якія вам даспадобы, на %s Паказваць метаданыя Ігнараваць падзеі апаратных медыякнопак - Паказваць змесціва, магчыма непрыдатнае для дзяцей, таму што яно мае ўзроставыя абмежаванні (напрыклад, 18+) + Паказваць змесціва, якое можа быць непрыдатным для дзяцей, бо мае ўзроставыя абмежаванні (напрыклад, 18+) Праверце, ці не існуе заяўкі з абмеркаваннем вашай праблемы. Дублікаты марнуюць наш час і праз гэта адцягваецца вырашэнне сапраўдных задач. Адбылася памылка, глядзіце апавяшчэнне Збой плэера @@ -600,8 +597,8 @@ %s новых трансляцый Каментарыі - У чаргу далей - У чарзе наступны + Дадаць у чаргу наступным + Дададзена у чаргу (наступным) Загрузка звестак аб стрыме… Ідзе апрацоўка… Крыху пачакайце Дублікат дададзены %d раз(ы) @@ -695,23 +692,20 @@ Радыё Паказваць наступныя патокі Паказаць/схаваць трансляцыі - Гэты кантэнт яшчэ не падтрымліваецца NewPipe. -\n -\nСпадзяюся, ён будзе падтрымлівацца ў наступных версіях. + Гэты кантэнт яшчэ не падтрымліваецца NewPipe.\n\nСпадзяёмся, што падтрымка з\'явіцца ў наступных версіях. Старонка плэй-ліста Паказваць мініяцюру Выкарыстоўваць мініяцюру як фон для экрана блакіроўкі і апавяшчэнняў Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар або паспрабуйце адключыць «%s» у наладах спампоўвання Гэты кантэнт недаступны ў вашай краіне. - Гэта трэк SoundCloud Go+, прынамсі ў вашай краіне, таму NewPipe не можа трансляваць ці спампоўваць яго. - Гэта змесціва з\'яўляецца прыватным, таму NewPipe не можа яго трансляваць або спампоўваць. - Гэта відэа даступна толькі для падпісчыкаў YouTube Music Premium, таму NewPipe не можа яго трансляваць або спампоўваць. + Гэта трэк SoundCloud Go+ (прынамсі ў вашай краіне), таму NewPipe не можа яго прайграць або спампаваць. + Гэты кантэнт прыватны, таму NewPipe не можа яго прайграць або спампаваць. + Гэта відэа даступна толькі для падпісчыкаў YouTube Music Premium, таму NewPipe не можа яго прайграць або спампаваць. Уліковы запіс спынены - %s дае наступную прычыну: Вартае ўвагі Унутраная Прагледжаныя цалкам - Гэты кантэнт даступны толькі для аплачаных карыстальнікаў, таму NewPipe не можа яго трансляваць або спампоўваць. + Гэты кантэнт даступны карыстальнікам толькі за плату, таму NewPipe не можа яго прайграць або спампаваць. Даступна для некаторых сэрвісаў, звычайна значна хутчэй, але можа перадаваць абмежаваную колькасць элементаў і не ўсю інфармацыю (можа адсутнічаць працягласць, тып элемента, паказчык трансляцыі) Узроставае абмежаванне Для гэтага дзеяння не знойдзены прыдатны файлавы менеджар. \nУсталюйце файлавы менеджар, сумяшчальны з Storage Access Framework @@ -733,7 +727,7 @@ Аддаваць перавагу арыгінальнаму гуку Аддаваць перавагу апісальнаму гуку Выбіраць гукавую дарожку з апісаннем для людзей са слабым зрокам, калі яна ёсць - Аўдыя: %s + Аўдыядарожка: %s Гукавая дарожка Выберыце гукавую дарожку для знешніх прайгравальнікаў Невядомая @@ -823,4 +817,19 @@ Пошук %1$s Пошук %1$s (%2$s) Спадабалася + «Дазволіць паказ па-над астатнімі праграмамі» + %s тыс. + %s млн + %s млрд + Выдаліць файл + Выдаліць запіс + Старонка SoundCloud Top 50 выдалена + Трэнды – музыка + Запіс выдалены + Трэнды – гульні + Трэнды – падкасты + Трэнды – фільмы і перадачы + Гэты кантэнт недаступны для цяперашняй краіны кантэнту.\n\nЯе можна змяніць праз «Налады > Кантэнт > Прадвызначаная краіна кантэнту». + 21 ліпеня 2025 года YouTube спыніў падтрымку аб\'яднанай старонкі трэндаў. NewPipe замяніў старонку трэндаў на трэнды трансляцый.\n\nТаксама можна выбраць іншыя старонкі трэндаў праз «Налады > Кантэнт > Змесціва галоўнай старонкі». + Аб\'яднаныя трэнды YouTube выдалены diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml index 3912381fc..39247f49b 100644 --- a/app/src/main/res/values-ber/strings.xml +++ b/app/src/main/res/values-ber/strings.xml @@ -45,9 +45,6 @@ ∞ ⵉⴼⵉⴷⵢⵓⵜⵏ 100+ ⵉⴼⵉⴷⵢⵓⵜⵏ - - - ⴰⵎⵙⵍⴰⵢ ⴰⴼⵉⴷⵢⵓ ⵉⵔⵉⵜⵏ diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 0d83f6c0c..69f8a8e9b 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -163,8 +163,8 @@ Само веднъж Файл Мини във фонов режим - Мини към нов прозорец - Мини в основен режим + Мини в нов прозорец + Мини към основен режим Внасяне на база данни Изнасяне на база данни Замества текущата ви история, абонаменти, списъци за възпроизвеждане и (по избор) настройки @@ -183,9 +183,6 @@ Името на файла не може да бъде празно Възникна грешка: %1$s Не са налични източници за изтегляне - хил. - млн. - млрд. Няма абонати Създай Откажи @@ -261,7 +258,7 @@ Нов Плейлист Преименувай Име - Добави Към Плейлист + Добави към плейлист Задай като миниатюра на плейлиста Миниатюрата на плейлиста е сменена Премахни отметката @@ -280,7 +277,7 @@ Докладвай за извънредни грешки Внасяне Внасяне от - Изнеси в + Изнасяне във Внасяне… Изнасяне… Файл с данни за внасяне @@ -419,7 +416,6 @@ Страница на плейлиста Глави Лиценз - %s посочва следната причина: Маркери Поверителност Език @@ -664,7 +660,7 @@ Въведете URL адреса на инстанцията Аудио: %s Покажи информация за канала - Автоматично генерирани (не е намерен ъплоудер) + Авто-генерирани (не е намерен ъплоудер) Създай известие за грешка NewPipe може автоматично да проверява за нови версии от време на време и да ви известява при наличие. \nИскате ли да го включите? @@ -819,4 +815,24 @@ Харесвания Страница SoundCloud Top 50 е премахната SoundCloud преустанови оригиналните класации Топ 50. Съответният раздел е премахнат от главната ви страница. + YouTube преустанови комбинираната страница с популярни от 21 юли 2025 г. NewPipe замени стандартната страница с популярни с популярни предавания на живо.\n\nМожете също да изберете различни популярни страници в „Настройки > Съдържание > Съдържание на главната страница“. + YouTube комбинирани популярни са премахнати + Популярни игри + Популярни подкасти + Популярни филми и сериали + Популярна музика + %s хил. + %s млн. + %s млрд. + За да използвате изскачащия плейър, моля, изберете %1$s в следното меню с настройки на Android и активирайте %2$s. + “Разреши показване върху други приложения” + Изтриване на файл + Изтриване на запис + Записът е изтрит + Профилът е прекратен\n\n%1$s предоставя тази причина: %2$s + HTTP грешка 403, получена от сървъра по време на възпроизвеждане, вероятно причинена от изтичане на URL адреса за стрийминг или забрана на IP адреса + HTTP грешка %1$s получена от сървъра по време на възпроизвеждане + HTTP грешка 403, получена от сървъра по време на възпроизвеждане, вероятно причинена от забрана на IP адреса или проблеми с деобфускацията на URL адреси за стрийминг + %1$s отказа да предостави данни, като поиска вход, за да потвърди, че заявителят не е бот.\n\nВашият IP адрес може да е временно забранен от %1$s. Можете да изчакате известно време или да превключите към друг IP адрес (например като включите/изключите VPN или като превключите от WiFi към мобилни данни). + Това съдържание не е налично за текущо избраната държава на съдържанието.\n\nПроменете избора си от \"Настройки > Съдържание > Държава на съдържанието по подразбиране\". diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index ddc32e418..2b0ffe918 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -82,9 +82,6 @@ ভিডিও অডিও পুনরায় চেষ্টা করো - হা - M - বি শুরু বিরতি diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index e6269b5b9..a79319ee3 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -307,4 +307,8 @@ বিজ্ঞপ্তিতে প্রদর্শিত ভিডিও থাম্বনেল 16:9 থেকে 1:1 অনুপাতের করুন (বিকৃতি দেখা যেতে পারে) অদলবদল কিছু না + হ্যাঁ + না + সার্চ + খুঁজুন diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 715c9146e..8d767a173 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -104,9 +104,6 @@ কোন ভিডিও নেই কোন ভিউ নেই কোন সাবস্ক্রাইবার নেই - B - M - K পুনরায় চেষ্টা করো অডিও ভিডিও @@ -299,7 +296,7 @@ এক প্লেয়ার থেকে অন্য প্লেয়ারে পরিবর্তন করলে তোমার সারি প্রতিস্থাপিত হতে পারে কিউ মোছার আগে নিশ্চিত করো কমপ্যাক্ট বিজ্ঞপ্তিতে প্রদর্শন করতে তুমি সর্বাধিক তিনটি ক্রিয়া নির্বাচন করতে পারো! - নিচের প্রতিটি প্রজ্ঞাপন ক্রিয়া সম্পাদনা করো। ডান দিকের চেকবাক্স ব্যবহার করে কম্প্যাক্ট নোটিফিকেশনে দেখানোর জন্য তিনটি পর্যন্ত নির্বাচন করো + নিচের প্রতিটি প্রজ্ঞাপন ক্রিয়া সম্পাদনা করুন । ডান দিকের চেকবাক্স ব্যবহার করে কম্প্যাক্ট নোটিফিকেশনে দেখানোর জন্য তিনটি পর্যন্ত নির্বাচন করুন । প্রদর্শিত ভিডিও থাম্বনেইল ১৬:৯ থেকে ১:১অনুপাতে পরিবর্তন করো ফিড ওভাররাইট @@ -543,7 +540,6 @@ \'%s\' এর জন্য ফিড প্রক্রিয়া করা যাচ্ছে না। বর্ণনার লেখা নির্বাচন করা নিষ্ক্রিয় করো বর্ণনার লেখা নির্বাচন করা সক্ষম করো - %s এই কারণ বলছে: প্রক্রিয়াকরণ ফিডে ত্রুটি ওয়েবসাইট খুলুন অ্যাকাউন্ট ধ্বংসকৃত @@ -632,4 +628,8 @@ ভুক্তি মুছতে ডানে-বামে সরাও সম্প্রচার বিষয়ক তথ্য প্রক্রিয়ারত… প্লেব্যাক লোড বিরতির আকার + হ্যা + না + প্লেলিস্ট + উদাহরণস্বরূপ, যদি আপনি ভাঙা ফিজিক্যাল বোতাম সহ একটি হেডসেট ব্যবহার করেন তবে এটি কার্যকর diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index a0a7744f6..dfff3c2cc 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -117,5 +117,4 @@ Lenn ar video, pad: Titouroù: Video - diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 4921a6aa8..ff0b7003a 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -28,7 +28,7 @@ Otkazana pratnja kanala Nije moguće promijeniti pratnju Nije moguće ažurirati pratnju - Instalirajte nedostajeću Kode aplikaciju\? + Instalirati nedostajuću Kore aplikaciju? Obilježeni Popisi Iskačni prozor Izaberite Podprozor @@ -43,9 +43,9 @@ Zadani režim za iskačući prozor Prekinite pokretač Prikažite postavku da biste video preko KODI medijskog centra video pokrenuli - Skalirajte sličicu na 1:1 omjer + Izrežite sličicu na omjer slike 1:1 Prvo radno dugme - Skalirajte video sličicu prikazanu u obavijesti sa 16:9 na 1:1 omjer (može uvesti poremećaje) + Izrežite sličicu videa prikazanu u obavještenju sa omjera stranica 16:9 na 1:1 Drugo radno dugme Treće radno dugme Četvrto radno dugme @@ -53,7 +53,7 @@ Možete najviše tri radnje odabrati za prikaz u kompaktnom obavještaju! Ponovi Pomiješajte - 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 + 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. Ništa Obojite obavještenje Učitavanje @@ -62,7 +62,7 @@ Zvuk Zadani video format Tema - Noćna Tema + Noćna tema Svijetla Tamna Zapamtite podešavanja za iskočne prozore @@ -114,4 +114,709 @@ Pokrenite s KODI-jem Vrijeme premotavanja naprijed/nazad Aktivni pokretni red će biti zamijenjen - \ No newline at end of file + Da + Ne + Pretraži %1$s + Pretraži %1$s (%2$s) + Plejliste + 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. + Veličina intervala učitavanja reprodukcije + Promijenite veličinu intervala učitavanja progresivnog sadržaja (trenutno %s). Niža vrijednost može ubrzati njihovo početno učitavanje + Zanemari događaje hardverskih medijskih tipki + Korisno, na primjer, ako koristite slušalice s pokvarenim fizičkim tipkama + Preferiraj originalni audio + Odaberite originalni audio zapis bez obzira na jezik + Preferiraj opisni audio + Odaberite audio zapis s opisima za osobe s oštećenim vidom ako su dostupni + Odaberite gestu za lijevu polovinu ekrana igrača + Radnja lijevog pokreta + Odaberite gestu za desnu polovinu ekrana igrača + Radnja desnog gesta + Svjetlina + Volumen + Nema + Prikaži savjet \"Drži za dodavanje u red\" + Prikaži savjet prilikom pritiska na pozadinu ili iskačuće dugme u videu \"Detalji:\" + Nepodržani URL + URL nije prepoznat. Otvoriti s drugom aplikacijom? + Zadana zemlja sadržaja + Zadani jezik sadržaja + PeerTube instance + Odaberite svoje omiljene PeerTube instance + Pronađite instance koje vam se sviđaju na %s + Dodaj instancu + Unesite URL instance + Nije moguće validirati instancu + Podržani su samo HTTPS URL-ovi + Instanca već postoji + Pokretač + Ponašanje + Video i audio + Historija i keš memorija + Izgled + Debug + Nadogradnje + Obavještenje za igrača + Konfigurišite obavještenja o trenutno reprodukovanom toku + Sigurnosna kopija i vraćanje + Reprodukcija u pozadini + Reprodukcija u skočnom modu + Sadržaj + Prikaži sadržaj s ograničenjem za uzrast + Prikaži sadržaj koji je možda neprikladan za djecu jer ima starosno ograničenje (npr. 18+) + Uključite \"Ograničeni način rada\" na YouTubeu + YouTube nudi \"Ograničeni način rada\" koji skriva potencijalno sadržaj za odrasle + Ovaj video ima dobno ograničenje.\n\nUključite \"%1$s\" u postavkama ako ga želite pogledati. + 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. + Uživo + Preuzimanja + Preuzimanja + Učitavanje metapodataka… + Izvještaj o grešci + Sve + Kanali + Plejliste + Videozapisi + Snimke + Korisnici + Događanja + Pjesme + Albuma + Umjetnici + Onemogućeno + Rasčisti + Najbolja rezolucija + Poništi + Datoteka je izbrisana + Reproduciraj sve + Uvijek + Samo jednom + Datoteka + Obavijesti + Obavještenje o novoj cijevi + Obavještenja za NewPipeovog igrača + Obavještenje o ažuriranju aplikacije + Obavještenja za nove verzije NewPipe-a + Obavještenje o hešu videa + Obavještenja o napretku heširanja videa + Novi tokovi + Obavještenja o novim tokovima za pretplatnike + Obavještenje o grešci + Obavještenja za prijavu grešaka + [Nepoznato] + Prebaci na pozadinu + Prebaci na skočni prozor + Prebaci na glavni + Uvoz baze podataka + Izvoz baze podataka + Obriši reCAPTCHA kolačiće + reCAPTCHA kolačići su obrisani + Zaobilazi vašu trenutnu historiju, pretplate, liste pjesama i (opcionalno) postavke + Izvoz historije, pretplata, plejlista i postavki + Obrišite kolačiće koje NewPipe pohranjuje kada riješite reCAPTCHA + Obriši historiju gledanja + Briše historiju reprodukovanih tokova i pozicije reprodukcije + Izbrisati cijelu historiju gledanja? + Historija gledanja je izbrisana + Brisanje pozicija reprodukcije + Briše sve pozicije reprodukcije + Izbrisati sve pozicije reprodukcije? + Pozicije reprodukcije su izbrisane + Obriši historiju pretraživanja + Briše historiju ključnih riječi pretrage + Izbrisati cijelu historiju pretraživanja? + Historija pretrage je izbrisana + Brzi način rada + Pomakni glavni birač kartica na dno + Položaj glavnih kartica + Greška + Vanjska pohrana nije dostupna + Preuzimanje na eksternu SD karticu nije moguće. Poništiti lokaciju mape za preuzimanje? + Greška mreže + Nije moguće učitati sve sličice + Nije moguće analizirati web stranicu + Sadržaj nije dostupan + Nije moguće postaviti meni za preuzimanje + Aplikacija/korisnički interfejs se srušio/la + Nije moguće reproducirati ovaj tok + Došlo je do nepopravljive greške igrača + Oporavak od greške igrača + Vanjski playeri ne podržavaju ove vrste linkova + Nisu pronađeni video tokovi + Nisu pronađeni audio tokovi + Datoteka je premještena ili izbrisana + Fascikla sa popisima + Nema takve datoteke/izvora sadržaja + Datoteka ne postoji ili nedostaje dozvola za čitanje ili pisanje u nju + Naziv datoteke ne može biti prazan + Došlo je do greške: %1$s + Nema dostupnih tokova za preuzimanje + Nije moguće pročitati sačuvane kartice, pa se koriste zadane + Vrati zadane postavke + Želite li vratiti zadane postavke? + Dozvoli prikaz preko drugih aplikacija + Da biste koristili Popup Player, odaberite %1$s u sljedećem meniju postavki Androida i omogućite %2$s. + \"Dozvoli prikaz preko drugih aplikacija\" + NewPipe je naišao na grešku, dodirnite za prijavu + Došlo je do greške, pogledajte obavještenje + Žao mi je, to se nije trebalo desiti. + Prijavi putem e-pošte + Kopiraj formatirani izvještaj + Izvještaj na GitHubu + 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. + Izvinite, ali nešto je pošlo po zlu. + Prijavi + Info: + Šta se dogodilo: + Šta:\\nZahtjev:\\nJezik sadržaja:\\nZemlja sadržaja:\\nJezik aplikacije:\\nUsluga:\\nVremenska oznaka:\\nPaket:\\nVerzija:\\nVerzija OS-a: + Vaš komentar (na engleskom): + Detalji: + Reproduciraj video, trajanje: + Sličica avatara osobe koja je postavila sliku + Sviđanja + Nesviđa mi se + Komentari + Povezane stavke + Opis + Bez rezultata + Ovdje nema ničega osim cvrčaka + Uvoz ili izvoz pretplata iz menija s tri tačke + Prevucite da promijenite redoslijed + Video + Audio + Pokušaj ponovo + %sK + %sM + %sB + Uključi/isključi uslugu, trenutno odabrana: + Nema pretplatnika + Broj pretplatnika nije dostupan + Nema pregleda + Niko ne gleda + Niko ne sluša + Nema videozapisa + 100+ videa + ∞ videozapisi + Nema komentara + Komentari su onemogućeni + Nema tokova + Nema prijenosa uživo + Početak + Pauziraj + Napravi + Izbriši + Izbriši datoteku + Izbriši unos + Kontrolni zbir + Raspusti + Preimenuj + Naziv datoteke + Teme + Greška + Preuzimanje NewPipe-a + Dodirnite za detalje + Izračunavanje heša + Molimo pričekajte… + Kopirano u međuspremnik + Kopiranje u međuspremnik nije uspjelo + Molimo vas da kasnije u postavkama definišete folder za preuzimanje + Još nije postavljen folder za preuzimanje, odaberite zadani folder za preuzimanje sada + Ova dozvola je potrebna za \notvaranje u skočnom prozoru + 1 stavka je izbrisana. + reCAPTCHA izazov + Pritisnite \"Gotovo\" kada riješite problem + Zatražen je reCAPTCHA izazov + Riješi + Gotovo + Preuzimanje + Dozvoljeni znakovi u nazivima datoteka + Nevažeći znakovi se zamjenjuju ovom vrijednošću + Zamjenski lik + Slova i brojevi + Većina specijalnih znakova + O NewPipe-u + Licence trećih strana + © %1$s od %2$s pod %3$s + Dozvole + Besplatno lagano tokanje na Androidu. + Doprinesite + 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! + Pogledajte na GitHubu + Donirajte + 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. + Vratite + Web stranica + Posjetite web stranicu NewPipe za više informacija i novosti. + Politika privatnosti kompanije NewPipe + 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. + Pročitajte politiku privatnosti + NewPipe-ova licenca + 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. + Pročitaj licencu + Često postavljana pitanja + Ako imate problema s korištenjem aplikacije, obavezno pogledajte ove odgovore na česta pitanja! + Pogledajte na web stranici + Historija + Historija + Želite li izbrisati ovu stavku iz historije pretrage? + Posljednje igrano + Najigranije + Sadržaj glavne stranice + Koje kartice se prikazuju na glavnoj stranici + Prevucite stavke da biste ih uklonili + Prazna stranica + Stranica kioska + Zadani kiosk + Stranica kanala + Odaberite kanal + Još nema pretplata na kanale + Odaberite listu za reprodukciju + Još nema oznaka za plejlistu + Odaberite kiosk + Izvezeno + Uvezeno + Nema važeće ZIP datoteke + Upozorenje: Nije moguće uvesti sve datoteke. + Ovo će poništiti vašu trenutnu postavku. + Želite li uvesti i postavke? + Nije moguće učitati komentare + Odaberite grupu feedova + Još nije kreirana nijedna grupa feedova + U trendu + Top 50 + Novo i popularno + Lokalno + Nedavno dodano + Najpopularnije + Konferencije + Red za reprodukciju + Ukloni + Detalji + Postavke zvuka + Audio: %s + Zvučni zapis + Držite za dodavanje u red + Prikaži detalje kanala + Stavi u red + Stavljeno u red čekanja + Stavi sljedeće u red + Sljedeće u redu čekanja + Počni reprodukciju u pozadini + Počnite igrati u iskačućem prozoru + Učitavanje detalja toka… + Otvori ladicu + Zatvori ladicu + Preferirana akcija \'otvaranja\' + Zadana radnja pri otvaranju sadržaja — %s + Video plejer + Pozadinski plejer + Iskačući plejer + Uvijek pitajte + Dobijanje informacija… + Učitavanje traženog sadržaja + Nova plejlista + Liste za reprodukciju koje su sive već sadrže ovu stavku. + Preimenuj + Ime + Dodaj na listu pjesama + Obrada… Može potrajati trenutak + Isključi zvuk + Uključi zvuk + Postavi kao sličicu za reprodukciju + Poništi trajnu sličicu + Označi plejlistu + Ukloni oznaku + Izbrisati ovaj popis? + Plejlista je kreirana + Plejlista + Duplikat dodan %d puta + Sličica plejliste je promijenjena. + Automatski generirano (nije pronađen korisnik koji je otpremio) + Nema titlova + Prilagođeno + Popuni + Uvećanje + Automatski generirano + Titlovi + Izmijenite veličinu teksta titlova i stilove pozadine za player. Za primjenu je potrebno ponovno pokretanje aplikacije + LeakCanary nije dostupan + Praćenje curenja memorije može uzrokovati da aplikacija prestane reagirati prilikom ispisa heap memorije + Prikaži curenje memorije + Prijavi greške izvan životnog ciklusa + Prisilno prijavljivanje izuzetaka neisporučenih Rx zahtjeva izvan životnog ciklusa fragmenta ili aktivnosti nakon odlaganja + Prikaži originalno vrijeme prije stavki + Originalni tekstovi iz usluga bit će vidljivi u stavkama toka + Onemogući tuneliranje medija + Onemogućite tuneliranje medija ako se pojavi crni ekran ili se prilikom reprodukcije videa pojavi prekid. + 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. + Prikaži indikatore slike + Prikažite Picasso obojene trake preko slika koje označavaju njihov izvor: crvena za mrežu, plava za disk i zelena za memoriju + Prikaži \"Sruši plejer\" + Prikazuje opciju pada sistema prilikom korištenja plejera + Pokreni provjeru za nove tokove + Sruši aplikaciju + Prikaži traku s upozorenjem o grešci + Kreiraj obavještenje o grešci + Uvoz + Uvoz iz + Izvoz u + Uvoz… + Izvoz… + Uvoz datoteke + Prethodni izvoz + Nije moguće uvesti pretplate + Nije moguće izvesti pretplate + 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 + 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. + tvoj ID, soundcloud.com/tvojID + Imajte na umu da ova operacija može biti skupa za mrežu.\n\nŽelite li nastaviti? + Kontrole brzine reprodukcije + Brzina + Točka glasa + Otkačite (može uzrokovati distorziju) + Premotavanje unaprijed tokom tišine + Korak + Resetuj + Postotak + Poluton + 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. + Prihvati + Odbij + Bez ograničenja + Ograničenje rezolucije prilikom korištenja mobilnih podataka + Obavještenja o novim tokovima + Obavijesti me o novim tokovima s pretplata + Učestalost provjere + Potrebna mrežna veza + Bilo koja mreža + Nadogradnje + Prikaži obavještenje za podsticanje ažuriranja aplikacije kada je dostupna nova verzija + Provjeri ažuriranja + NewPipe može automatski provjeravati nove verzije s vremena na vrijeme i obavijestiti vas kada budu dostupne.\nŽelite li ovo omogućiti? + Ručno provjerite nove verzije + Minimiziraj pri prebacivanju aplikacija + Radnja prilikom prelaska na drugu aplikaciju iz glavnog video plejera — %s + Nema + Minimiziraj na pozadinski plejer + Minimiziraj da bi se player pojavio u skočnom prozoru + Automatski pokreni reprodukciju — %s + Samo na Wi-Fi mreži + Nikad + Način prikaza liste + Spisak + Rešetka + Kartica + Automatski + Pregled sličice trake za pretraživanje + Visok kvalitet (veći) + Nizak kvalitet (manji) + Ne prikazuj + Koristite najnoviju verziju NewPipe-a + Ažuriranje NewPipe-a je dostupno! + Dodirnite za preuzimanje %s + Završeno + Na čekanju + pauzirano + u redu čekanja + naknadna obrada + oporavlja se + Stavi u red + Sistem je odbio akciju + Provjera ažuriranja… + Preuzimanje nije uspjelo + Resetiraj postavke + Resetujte sve postavke na njihove zadane vrijednosti + Resetovanjem svih postavki poništit ćete sve svoje željene postavke i ponovo pokrenuti aplikaciju.\n\nJeste li sigurni da želite nastaviti? + Generiraj jedinstveno ime + Prebriši + Datoteka s ovim imenom već postoji + Preuzeta datoteka s ovim nazivom već postoji + ne može prepisati datoteku + U toku je preuzimanje s ovim imenom + Postoji preuzimanje s ovim nazivom na čekanju + Prikaži grešku + Datoteka ne može biti kreirana + Nije moguće kreirati odredišnu mapu + Nije moguće uspostaviti sigurnu vezu + Nije moguće pronaći server + Ne mogu se povezati sa serverom + Server ne šalje podatke + Server ne prihvata višenitna preuzimanja, pokušajte ponovo sa @string/msg_threads = 1 + Nije pronađeno + Naknadna obrada nije uspjela + NewPipe je zatvoren tokom rada na datoteci + Nema dovoljno slobodnog prostora na uređaju + Nema više prostora na uređaju + Napredak je izgubljen jer je datoteka izbrisana + Vremensko ograničenje veze + Nije moguće oporaviti ovo preuzimanje + Obriši historiju preuzimanja + Želite li obrisati historiju preuzimanja ili izbrisati sve preuzete datoteke? + Izbriši preuzete datoteke + Izbrisati sve preuzete datoteke s diska? + Zaustavi + Maksimalan broj ponovnih pokušaja + Maksimalan broj pokušaja prije otkazivanja preuzimanja + Prekid na mrežama s ograničenim pristupom + Korisno prilikom prelaska na mobilne podatke, iako se neka preuzimanja ne mogu obustaviti + Zatvori + Ograniči red čekanja za preuzimanje + Jedno preuzimanje će se pokrenuti istovremeno + Započni preuzimanja + Pauziraj preuzimanja + Pitaj gdje preuzeti + Bit ćete upitani gdje želite sačuvati svako preuzimanje.\nOmogućite birač sistemskih foldera (SAF) ako želite preuzeti na eksternu SD karticu + Bit ćete upitani gdje sačuvati svako preuzimanje + Koristi birač sistemskih foldera (SAF) + \'Okvir za pristup pohrani\' omogućava preuzimanje na eksternu SD karticu + Počevši od Androida 10, podržan je samo \'Storage Access Framework\' + Odaberite instancu + Jezik aplikacije + Zadano sistemsko + Ukloni gledano + Ukloniti gledane videozapise? + Ukloni duplikate + Ukloniti duplikate? + Želite li ukloniti sve duplikatne tokove na ovoj listi za reprodukciju? + 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! + Da, i djelimično odgledani videozapisi + Zbog ograničenja ExoPlayera, trajanje pretraživanja je postavljeno na %d sekundi + Šta je novo + Stranica grupe kanala + Grupe kanala + Sažetak zadnji put ažuriran: %s + Nije učitano: %d + Učitavanje feeda… + Obrada feeda… + Nove stavke feeda + Odaberite pretplate + Nije odabrana pretplata + Prazan naziv grupe + Želite li izbrisati ovu grupu? + Novo + Prikaži samo negrupirane pretplate + Sažetak + Prag ažuriranja feeda + Vrijeme nakon posljednjeg ažuriranja prije nego što se pretplata smatra zastarjelom — %s + Uvijek ažuriraj + Greška pri učitavanju feeda + Nije moguće učitati feed za \'%s\'. + 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? + Režim brzog hranjenja ne pruža više informacija o ovome. + Preuzmi iz namjenskog feeda kada je dostupan + 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) + Omogući brzi način rada + Onemogući brzi način rada + 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. + Prikaži sljedeće tokove + Prikaži/Sakrij tokove + Dohvati kartice kanala + Kartice koje treba preuzeti prilikom ažuriranja feeda. Ova opcija nema efekta ako se kanal ažurira pomoću brzog načina rada. + Ovaj sadržaj još uvijek nije podržan od strane NewPipe-a.\n\nNadamo se da će biti podržan u budućoj verziji. + Sličica avatara kanala + Kreirao/la %s + Napisao %s + Stranica s popisom za reprodukciju + Prikaži sličicu + Koristite sličicu i za pozadinu zaključanog ekrana i za obavještenja + Nedavno + Poglavlja + Nijedna aplikacija na vašem uređaju ne može ovo otvoriti + Nije pronađen odgovarajući upravitelj datoteka za ovu radnju.\nMolimo instalirajte upravitelj datoteka ili pokušajte onemogućiti \'%s\' u postavkama preuzimanja + Nije pronađen odgovarajući upravitelj datoteka za ovu radnju.\nMolimo instalirajte upravitelj datoteka kompatibilan sa Storage Access Frameworkom + Ovaj sadržaj nije dostupan u vašoj zemlji. + Ovo je pjesma na SoundCloud Go+ platformi, barem u vašoj zemlji, tako da je NewPipe ne može strimovati ili preuzeti. + Ovaj sadržaj je privatan, tako da ga NewPipe ne može strimovati ili preuzimati. + Ovaj video je dostupan samo članovima YouTube Music Premium-a, tako da ga NewPipe ne može strimovati ili preuzeti. + Račun ukinut + Račun ukinut\n\n%1$s navodi ovaj razlog: %2$s + Ovaj sadržaj je dostupan samo korisnicima koji su platili, tako da ga NewPipe ne može strimovati ili preuzimati. + Istaknuto + Radio + Automatski (tema uređaja) + Odaberite svoju omiljenu noćnu temu — %s + Možete odabrati svoju omiljenu noćnu temu ispod + Ova opcija je dostupna samo ako je za temu odabrana %s + Preuzimanje je počelo + 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. + Omogući odabir teksta u opisu + Onemogući odabir teksta u opisu + Kategorija + Oznake + Dozvola + Privatnost + Starosna granica + Jezik + Podrška + Domaćin + Sličice + Avatari koji su postavili profil + Avatari podkanala + Avatari + Baneri + Javno + Nije navedeno + Privatno + Unutrašnje + Pretplatnici + Zakačen komentar + Srce od strane kreatora + Otvori web stranicu + Tabletni način rada + Upaljeno + Ugašeno + Zadano za ExoPlayer + Obavještenja su onemogućena + Primajte obavještenja + Sada ste pretplaćeni na ovaj kanal + , + Prikaži/Uključi sve + Strimovi koje program za preuzimanje još ne podržava nisu prikazani + Zvučni zapis bi već trebao biti prisutan u ovom toku + Odabrani tok nije podržan od strane eksternih plejera + Nema dostupnih audio tokova za vanjske uređaje za reprodukciju + Nema video tokova dostupnih za vanjske uređaje za reprodukciju + Odaberite kvalitet za vanjske uređaje za reprodukciju + Odaberite audio zapis za vanjske uređaje za reprodukciju + Nepoznati format + Nepoznat kvalitet + Nepoznato + Potpuno odgledano + Djelomično gledano + Nadolazeći + Sortiraj + Postavke ExoPlayera + Upravljajte nekim postavkama ExoPlayera. Ove promjene zahtijevaju ponovno pokretanje plejera da bi stupile na snagu + Koristite ExoPlayer-ovu rezervnu funkciju dekodera + 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 + Uvijek koristite ExoPlayer-ovo rješenje za podešavanje površine video izlaza + 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 + %1$s %2$s + original + sinhronizovano + opisni + sekundarni + Videozapisi + Snimke + Kratke hlače + Uživo + Kanali + Plejliste + Albuma + Sviđanja + O tome + Kartice kanala + Koje kartice se prikazuju na stranicama kanala + Otvori red za reprodukciju + Prikaz preko cijelog ekrana + Uključi/isključi orijentaciju ekrana + Prethodni tok + Sljedeći tok + Pokrenuti + Ponovna reprodukcija + Više opcija + Trajanje + Premotavanje unazad + Naprijed + Kvalitet slike + 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 + Ne učitavaj slike + Niska kvaliteta + Srednji kvalitet + Visoka kvaliteta + \? + Dijeli plejlistu + Podijeli s naslovima + Podijeli listu URL-ova + Podijeli kao privremenu YouTube plejlistu + - %1$s: %2$s + %1$s\n%2$s + Prikaži više + Prikaži manje + 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. + Stranica SoundCloud Top 50 uklonjena + SoundCloud je ukinuo originalne Top 50 liste. Odgovarajuća kartica je uklonjena sa vaše glavne stranice. + Uklonjen je kombinovani prikaz trendova na YouTubeu + Trendovi u igrama + Trendovi podcasti + Popularni filmovi i serije + Popularna muzika + Unos izbrisan + HTTP greška 403 primljena od servera tokom reprodukcije, vjerovatno uzrokovana istekom URL-a za tokove ili zabranom IP adrese + HTTP greška %1$s primljena od servera tokom reprodukcije + HTTP greška 403 primljena od servera tokom reprodukcije, vjerovatno uzrokovana zabranom IP adrese ili problemima s deobfuskacijom URL-a za tokove + %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). + + %s pretplatnik + %s pretplatnika + %s pretplatnika + + + %s pregled + %s pregleda + %s pregleda + + + %s gledatelj + %s gledatelja + %s gledatelja + + + %s slušatelj + %s slušatelja + %s slušatelja + + + %s video + %s videozapisa + %s videozapisa + + + %s novi tok + %s nova toka + %s novih tokova + + O aplikaciji i pitanja + + %s preuzimanje je gotovo + %s preuzimanja su gotova + %s preuzimanja je gotovo + + + Izbrisano %1$s preuzimanje + Izbrisana %1$s preuzimanja + Izbrisano %1$s preuzimanja + + + %d sekunda + %d sekunde + %d sekundi + + + %d minut + %d minute + %d minuta + + + %d sat + %d sata + %d sati + + + %d dan + %d dana + %d dana + + + %d odabrana + %d odabrane + %d odabranih + + + %s odgovor + %s odgovora + %s odgovora + + 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\". + Ovaj sadržaj nije dostupan za trenutno odabranu zemlju sadržaja.\n\nPromijenite svoj odabir u \"Postavke > Sadržaj > Zadana zemlja sadržaja\". + diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 123a5ef67..31183d15b 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -215,7 +215,7 @@ Controls de la velocitat de reproducció Tempo To - Toca \"Cerca\" per començar. + Toqueu la lupa per començar. Elimina l\'àudio en algunes resolucions Reproductor d\'àudio extern Emmagatzema les cerques localment @@ -231,9 +231,6 @@ S\'està recuperant el reproductor després de l\'error Bé, és lamentable. Arrossegueu per reordenar la llista - mil - milions - mil milions Inicia Feu un toc aquí per a més detalls Defineix una carpeta de baixades més endavant als paràmetres @@ -615,7 +612,6 @@ Pot seleccionar el seu tema fosc favorit aqui sota Selecciona el teu tema fosc favorit — %s Automàtic (tema del dispositiu) - %s dóna aquesta raó: Usuari suspes El compte de l\'autor ha estat esborrat. \nNewPipe no serà capaç de carregar aquest fil en el futur. diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 649e6e73c..f0ddfb6e9 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -25,7 +25,6 @@ ژمارەی بەژداری نادیارە ناتوانرێت لەسەر ئەو فایله‌وه‌ جێگیر بکرێت په‌ڕه‌ هەڵبژێرە - ملیۆن +١٠٠ ڤیدیۆیان لێده‌ر هاوردە @@ -46,7 +45,6 @@ ئەمە لەسەر ڕێکخستنەکانی ئێستات جێگیر دەبێت. پەیامەکانی نیوپایپ نیوپایپ لەلایەن چەند خۆبەخشێکەوە دروستکراوە کە کاته‌كانی خۆیان پێ بەخشیووە تاکو باشترین خزمەتگوزاریت پێشکەش بکەن. هیچ نەبێت بە کڕینی کوپێک قاوە یارمەتی گەشەپێدەرەکانمان بدە بۆ ئەوەی کاتی زیاتر تەرخان بکەین بۆ بەرەوپێشبردنی نیوپایپ. - ملیار گەڕانی پێشنیارکراوەکان خێرا فایل سڕایەوە @@ -373,7 +371,6 @@ ناتوانرێت داببه‌زێنرێت ناتوانرێت بە ڕاژەكه‌وە پەیوەست ببیت لێدانی ڤیدیۆ، مه‌ودا: - هەزار زۆرترین بەدڵ سڕینەوە جۆری بنەڕەتی ڤیدیۆ @@ -599,7 +596,6 @@ ڕادیۆ تایبەتکراو ئه‌م بابه‌ته‌ ته‌نیا بۆ ئه‌و كه‌سانه‌ به‌رده‌سته‌ كه‌ پاره‌یان داوه‌ ، بۆیه‌ ناتوانرێت له‌ نیوپایپه‌وه‌ داببه‌زێنرێت. - %s ئه‌م هۆكاره‌ دابین ده‌كات: هه‌ژمار له‌ناوبراوه‌ ئه‌م ڤیدیۆیه‌ ته‌نیا له‌ وه‌شانی نایابی یوتوب میوزیك به‌رده‌سته‌ ، بۆیه‌ ناتوانرێت له‌ نیوپایپه‌وه‌ داببه‌زێنرێت. ئه‌مه‌ تراكی SoundCloud Go+ ه‌ ، لانی كه‌م له‌ وڵاته‌كه‌ی تۆدا، ناتوانرێت له‌لایه‌ن نیوپایپه‌وه‌ داببه‌زێنرێت. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index c1f23457d..853b8c1cb 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -83,9 +83,7 @@ Určete prosím složku pro stahování později v nastavení Co:\\nŽádost:\\nJazyk obsahu:\\nZemě obsahu:\\nJazyk aplikace:\\nSlužba:\\nČas GMT:\\nBalíček:\\nVerze:\\nVerze OS: Vše - tis. Otevřít ve vyskakovacím okně - mil. Toto oprávnění je vyžadováno \npro otevření ve vyskakovacím okně Odstraňuje zvuk v některých rozlišeních @@ -124,7 +122,6 @@ Oznámení pro NewPipe přehrávač Žádné výsledky Je tu sranda jak v márnici - mld. Žádní odběratelé %s odběratel @@ -489,7 +486,7 @@ Prázdné jméno skupiny Přejete si odstranit tuto skupinu? - Nová + Nový Novinky Limit aktualizace novinek Doba po poslední aktualizaci, po níž je odběr považován za zastaralý — %s @@ -622,7 +619,6 @@ Vypnout výběr textu v popisu Zapnout výběr textu v popisu Nyní můžete vybrat v popisu text. Pamatujte, že v režimu výběru může stránka blikat a odkazy nemusí reagovat na kliknutí. - %s udává teno důvod: Účet uzavřen Režim rychlého feedu o tom neposkytuje více informací. Autorův účet byl uzavřen. @@ -723,7 +719,7 @@ Klepnutím stáhnete %s Rychlý režim Používáte nejnovější verzi NewPipe - Import nebo export odběrů z 3-tečkové nabídky + Importujte nebo exportujte odběry z 3tečkové nabídky Tato možnost je dostupná pouze při vybraném motivu %s Zrušení nastavení trvalého náhledu Karta @@ -848,4 +844,24 @@ Líbí se Stránka SoundCloud Top 50 odstraněna SoundCloud zrušil původní žebříčky Top 50. Příslušná karta byla odstraněna z vaší hlavní stránky. + YouTube kombinované trendy odstraněny + 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“. + Populární hry + Populární podcasty + Populární filmy a seriály + Populární hudba + %s tis. + %s mil. + %s mld. + Pro používání Popup Playeru vyberte v následující nabídce nastavení Androidu možnost %1$s a povolte %2$s. + \"Povolit zobrazení přes jiné aplikace\" + Vymazat soubor + Vymazat položku + Položka vymazána + Ukončení účtu\n\n%1$s uvádí tento důvod: %2$s + 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 + Chyba HTTP %1$s obdržená ze serveru během přehrávání + 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 + %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). + 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\". diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index cecdcc4fe..78f08ee85 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -232,7 +232,7 @@ Mest Afspillet Indhold af hovedside Hvilke faner vises på hovedsiden - Tom Side + Tom side Kioskside Kanalside Vælg en kanal @@ -305,9 +305,6 @@ Stop Hændelser Ikke andet end fårekyllinger her - t - mio. - mia. %s abonnent %s abonnenter @@ -557,7 +554,6 @@ Dette indhold er privat, så det kan ikke streames eller hentes af NewPipe. Nyligt tilføjede Fremhævede - %s giver denne grund: %s lytter %s lyttere @@ -829,4 +825,20 @@ Kanalgruppeside Vælg en feed-gruppe Ingen feed-gruppe oprettet endnu + Søg %1$s + Søg %1$s (%2$s) + For at kunne bruge pop op-afspilleren skal du vælge %1$s i følgende Android-indstillingsmenu og aktivere %2$s. + “Tillad visning over andre apps” + %sK + %sM + %sB + Slet fil + Kontoen er blevet lukket\n\n%1$s angiver følgende årsag: %2$s + Likes + SoundCloud Top 50-siden fjernet + SoundCloud har udfaset de oprindelige Top 50-hitlister. Den tilhørende fane er blevet fjernet fra din hovedside. + YouTube kombineret trending fjernet + YouTube har udfaset den kombinerede trending-side pr. 21. juli 2025. NewPipe har erstattet standardsiden for trending med trending livestreams.\n\nDu kan også vælge andre trending-sider under \"Indstillinger > Indhold > Indhold på hovedsiden\". + Gaming-trends + Trending podcasts diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1f06121a8..b36b8555b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -69,9 +69,6 @@ Fehlerbericht Löschen Prüfsumme - Tsd. - Mio. - Mrd. Dateiname Fehler Bitte warten … @@ -630,7 +627,6 @@ Du wirst jedes Mal gefragt werden, wohin der Download gespeichert werden soll Fehler beim Laden des Feeds Konnte Feed für \'%s\' nicht laden. - %s gibt diesen Grund an: An Tablet-Modus Aus @@ -834,4 +830,24 @@ Gefällt mir SoundCloud-Top-50-Seite entfernt SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt. + %sMio. + %sMrd. + %sTsd. + Gaming-Trends + Beliebte Filme und Shows + Beliebte Musik + Beliebte Podcasts + YouTube hat die kombinierten „beliebten Seiten“ entfernt + 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. + Um den Pop-up-Player zu verwenden, bitte in den folgenden Android-Einstellungen %1$s auswählen und %2$s aktivieren. + „Über anderen Apps einblenden“ + Datei löschen + Eintrag löschen + Eintrag gelöscht + Konto geschlossen\n\n%1$s gibt folgenden Grund an: %2$s + HTTP-Fehler 403 vom Server während der Wiedergabe erhalten, wahrscheinlich verursacht durch Ablauf der Streaming-URL oder eine IP-Sperre + HTTP-Fehler %1$s vom Server während der Wiedergabe erhalten + HTTP-Fehler 403 vom Server während der Wiedergabe erhalten, wahrscheinlich verursacht durch eine IP-Sperre oder Probleme beim Entschlüsseln der Streaming-URL + %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). + 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“. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 84da93a0f..02bf75b30 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -41,7 +41,6 @@ Μικρογραφία εικόνας προφίλ του χρήστη Like Dislike - δισ/ρια Άνοιγμα σε αναδυόμενο παράθυρο Εγγραφή Εγγεγραμμένος @@ -169,8 +168,6 @@ Δεν υπάρχει τίποτα εδώ Σύρετε για ταξινόμηση Προσπάθεια εκ νέου - χιλ. - εκ/ρια Κανένας συνδρομητής %s συνδρομητής @@ -611,7 +608,6 @@ Ενεργοποίηση επιλογής κειμένου στην περιγραφή Τώρα μπορείτε να επιλέξετε κείμενο εντός της περιγραφής. Σημειώστε ότι, η σελίδα μπορεί να παρουσιάζει αστάθεια κατά τη διάρκεια της κατάστασης επιλογής κειμένου. Ανοικτή ιστοσελίδα - Το %s παρέχει αυτή την αιτία: Ο λογαριασμός διαγράφηκε Η κατάσταση γρήγορης τροφοδοσίας δεν παρέχει περισσότερες πληροφορίες. Ο λογαριασμός του δημιουργού έχει διαγραφεί. @@ -831,7 +827,27 @@ Σελίδα καναλιού ομάδας Αναζήτηση %1$s Αναζήτηση %1$s (%2$s) - Likes + Μου αρέσει Η σελίδα των SoundCloud Top 50 αφαιρέθηκε Το SoundCloud έχει καταργήσει τα αρχικά charts με τα Top 50. Η αντίστοιχη καρτέλα έχει αφαιρεθεί από την κύρια σελίδα σας. + Οι συνδυασμένες τάσεις στο YouTube καταργήθηκαν + Το YouTube έχει καταργήσει τη συνδυασμένη σελίδα με τάσεις από την 21 Ιουλίου 2025. Το NewPipe αντικατέστησε την προεπιλεγμένη σελίδα τάσεων με τις ζωντανές ροές τάσεων.\n\nΜπορείτε επίσης να επιλέξετε διαφορετικές σελίδες με τάσεις στις \"Ρυθμίσεις > Περιεχόμενο > Περιεχόμενο κύριας σελίδας\". + Τάσεις παιχνιδιών + Τάσεις podcasts + Τάσεις ταινιών και εκπομπών + Μουσικές τάσεις + %sK + %sM + %sB + Για να χρησιμοποιήσετε το Αναδυόμενο Πρόγραμμα Αναπαραγωγής, επιλέξτε %1$s στο ακόλουθο μενού ρυθμίσεων Android και ενεργοποιήστε το %2$s. + «Να επιτρέπεται η εμφάνιση πάνω από άλλες εφαρμογές» + Διαγραφή αρχείου + Διαγραφή καταχώρησης + Η καταχώρηση διαγράφηκε + Ο λογαριασμός έκλεισε\n\n%1$s παρέχει αυτήν την αιτία: %2$s + Σφάλμα HTTP 403 που ελήφθη από τον διακομιστή κατά την αναπαραγωγή, πιθανώς λόγω λήξης διεύθυνσης URL ροής ή αποκλεισμού IP + Σφάλμα HTTP %1$s ελήφθη από τον διακομιστή κατά την αναπαραγωγή + Σφάλμα HTTP 403 ελήφθη από τον διακομιστή κατά την αναπαραγωγή, πιθανώς λόγω αποκλεισμού IP ή προβλημάτων απεμπλοκής URL ροής + Ο %1$s αρνήθηκε να παράσχει δεδομένα, ζητώντας σύνδεση για να επιβεβαιώσει ότι ο αιτών δεν είναι bot.\n\nΗ IP σας ενδέχεται να έχει αποκλειστεί προσωρινά από τον %1$s. Μπορείτε να περιμένετε λίγο ή να αλλάξετε IP (για παράδειγμα, ενεργοποιώντας/απενεργοποιώντας ένα VPN ή αλλάζοντας από WiFi σε δεδομένα κινητής τηλεφωνίας). + Αυτό το περιεχόμενο δεν είναι διαθέσιμο για την τρέχουσα επιλεγμένη χώρα περιεχομένου.\n\nΑλλάξτε την επιλογή σας από \"Ρυθμίσεις > Περιεχόμενο > Προεπιλεγμένη χώρα περιεχομένου\". diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 8b022bcff..1a80eefd9 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -84,4 +84,14 @@ Okay Open in browser No stream player found (you can install VLC to play it). + Yes + No + Mark as watched + Open in popup mode + Open with + Share + Download + Download stream file + Search + Settings diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 1da4d536c..3dde69618 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -261,9 +261,6 @@ %s spekto %s spektoj - k - M - Mrd Pri NewPipe Eksteraj permesiloj © %1$s de %2$s sub %3$s @@ -507,7 +504,6 @@ Ŝaltita Etikedoj Elŝutado komenciĝis - %s donas tiun kialon: Tiu enaĵo ne disponeblas en via lando. Freŝaj De %s diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 610d900ec..72d1f58b5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -81,9 +81,6 @@ Qué:\\nSolicitud:\\nIdioma del contenido:\\nPaís del contenido:\\nIdioma de la aplicación:\\nServicio:\\nMarca de tiempo:\\nPaquete:\\nVersión:\\nVersión del SO: Negro Todo - k - M - MM Abrir en modo emergente Se necesita este permiso \npara abrir en modo emergente @@ -614,7 +611,6 @@ Deshabilitar la selección de texto de la descripción Habilitar la selección de texto de la descripción Ahora puede seleccionar el texto dentro de la descripción. Note que la página puede parpadear y los links no serán cliqueables mientras está en el modo de selección. - %s da esta razón: No fue posible cargar el feed por \'%s\'. Cuenta cancelada El modo de muro rápido no arroja más información sobre esto. @@ -833,4 +829,29 @@ Selecciona un grupo de feed Aún no se ha creado ningún grupo de feed Página de grupo de canales + Buscar %1$s + Buscar %1$s (%2$s) + Me gusta + Página Top 50 de SoundCloud eliminada + SoundCloud ha descontinuado las listas originales del Top 50. La pestaña correspondiente se ha eliminado de la página principal. + YouTube tendencias combinadas eliminado + YouTube ha descontinuado la página de tendencias combinadas a partir del 21 de julio de 2025. NewPipe reemplazó la página de tendencias predeterminada con tendencias en directo.\n\nTambién puedes seleccionar diferentes páginas de tendencias en \"Ajustes > Contenido > Contenido de la página principal\". + Tendencias videojuegos + Tendencias pódcasts + Tendencias películas y programas + Tendencias música + “Permitir mostrar sobre otras aplicaciones” + Eliminar archivo + %sM + %sM + Eliminar entrada + Cuenta cancelada\n\n%1$s proporciona esta razón: %2$s + Entrada eliminada + Error HTTP 403 recibido del servidor durante la reproducción, probablemente causado por la expiración de la URL de transmisión o una prohibición de IP + Error HTTP %1$s recibido del servidor durante la reproducción + Error HTTP 403 recibido del servidor durante la reproducción, probablemente causado por una prohibición de IP o problemas de desofuscación de la URL de transmisión + %1$s se negó a proporcionar datos y solicitó un inicio de sesión para confirmar que el solicitante no es un bot.\n\nEs posible que tu IP haya sido bloqueada temporalmente por %1$s. Puedes esperar un tiempo o cambiar a una IP diferente (por ejemplo, habilitando o deshabilitando una VPN, o cambiando de WiFi a datos móviles). + %sMM + Este contenido no está disponible para el país seleccionado actualmente.\n\nCambia tu selección en «Ajustes > Contenido > País predefinido del contenido». + Para usar el reproductor emergente, seleccione %1$s en el siguiente menú de la configuración de Android y habilite %2$s. diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index af8cfde88..05f4cf505 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -154,9 +154,6 @@ Video Audio Proovi uuesti - tuh - mln - mld Tellijaid pole %s tellija @@ -565,7 +562,6 @@ Näita pisipilte Kasuta pisipilti nii lukustusvaate kui teavituste taustana Kasutajakonto on suletud - %s toob põhjuseks: Võimalda valida kirjelduse teksti Ära võimalda valida kirjelduse teksti Kategooria @@ -607,7 +603,7 @@ Hangi võimalusel spetsiaalsest voost Kiirvoo režiim ei paku selle kohta täiendavat teavet. Autori konto on suletud. \nTulevikus ei saa NewPipe seda meediavoogu laadida. \nKas soovid tühistada selle kanali tellimuse? - Voo \'%s\' laadimine nurjus. + Voo \'%s\' laadimine ei õnnestnud. Via voo laadimisel Värskenda alati Aeg pärast viimast värskendust, mille möödudes loetakse tellimus aegunuks — %s @@ -819,4 +815,24 @@ Meeldimisi SoundCloudi „Top 50“ leht on eemaldatud SoundCloud on lõpetanud oma algse „Top 50“ edetabeli pidamise. Seega on ka vastav vahekaart meie rakenduse põhivaatest eemaldatud. + YouTube\'i kombineeritud populaarsust koguvad videovoog on eemaldatud + YouTube on alates 21.07.25 lõpetanud ühendatud populaarsust koguvate videote lehe kasutamise. Mistõttu ka NewPipe on asendanud vaikimisi populaarsust koguvate videote lehe sarnase otse-eetri lehega.\n\n„Seadistused -> Sisu -> Avalehe sisu“ alt saad ka muid sarnaseid lehti seadistada. + Populaarsust koguvad taskuhäälingud + Populaarsust koguvad filmid ja telesarjad + Populaarsust koguv muusika + Populaarsust koguvad mängud + %s tuh + %s mln + %s mld + Kasutamaks meediaesitajat hüpikaknas palun vali järgnevast Androidi seadistuste valikust „%1$s“ ja lülita sisse „%2$s“. + Luba kuvamine teiste rakenduste peal + Kustuta fail + Kustuta kirje + Kirje on kustutatud + Kasutajakonto on suletud\n\n%1$s on märkinud põhjuseks: %2$s + Esitamise ajal lisas server andmevoogu HTTP oleku 403 ning tavaliselt tähendab see, et voogedastuse võrguaadress on aegunud või sinu seadme IP-aadress on keelatud + Esitamise ajal lisas server andmevoogu HTTP oleku %1$s + Esitamise ajal lisas server andmevoogu HTTP oleku 403 ning tavaliselt tähendab see, et sinu seadme IP-aadress on keelatud või voogedastuse võrguaadressi hägustamisvastastes meetmetes on viga + See sisu pole saadaval hetkel kehtvas riigis.\n\nRiiki saad muuta: Seadistused > Sisu > Sisu vaikimisi riik. + %1$s keeldus andmete edastamisest ning eeldab sisselogimist tuvastamaks, et tegemist pole robotiga.\n\nLisaks võib olla juhtunud, et %1$s on lisanud sinu seadme ip-aadressi ajutisse keelunimekirja. Sa võid oodata natuke aega või vahetada võrguühendus viisi (näiteks lülitades VPN sisse/välja või kasutades WiFi asemel mobiilset internetiühendust). diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 16054d9ae..5c963d6b7 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -84,9 +84,6 @@ Bideoa Audioa Saiatu berriro - k - M - MM Hasi Pausatu Ezabatu @@ -618,7 +615,6 @@ Non gorde galdetuko zaizu deskarga bakoitzean Ez da deskargatzeko karpetarik ezarri oraindik, aukeratu lehenetsitako deskargatzeko karpeta orain Pribatutasuna - %s arrazoi hau ematen du: Kontua ezabatu da Jario azkarrak ez du honi buruz informazio gehiagorik ematen. Adin muga diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 3bba8efb8..403fb4c11 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -148,9 +148,6 @@ خطایی رخ داد: %1$s جریانی برای بارگیری در دسترس نیست بدون نتیجه - K - M - B %s مشترک %s مشترک @@ -629,7 +626,6 @@ \nنیوپایپ قادر به بار کردن این خوراک در آینده نیست. \nمی‌خواهید اشتراک این کانال را لغو کنید؟ حالت خوراک سریع، اطَلاعات بیش‌تری در این باره نمی‌دهد. - %s این دلیل را آورد: پیش‌نمایش بندانگشتی نوار جویش قلب‌شده به دست ایجادگر پیشنهادهای جست‌وجوی محلّی diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 20734dd26..22c1fab12 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -102,9 +102,6 @@ Video Ääni Toista uudelleen - t. - milj. - bilj. Ei tilaajia %s tilaaja @@ -595,7 +592,6 @@ Yöteema Poista käytöstä tekstinvalinta kuvauskentän sisältä Voit nyt valita tekstin kuvauskentän sisältä. Huomioithan, että valintatilan aikana sivu voi vilkkua ja linkit eivät ehkä ole klikattavia. - %s tuo tämän syyn: Säätövivun kuvakkeen esikatselu Poista median tunnelointi käytöstä, jos havaitset mustan näyttöruudun tai änkytystä videon toistossa. Poista median tunnelointi käytöstä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9a9590a07..eb28d0e21 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -87,8 +87,6 @@ Lecture en mode flottant Désactivés Quoi :\\nRequest :\\nContent Language :\\nContent Country :\\nApp Language :\\nService :\\nGMT Time :\\nPackage :\\nVersion :\\nOS version : - k - M Cette autorisation est nécessaire pour \nutiliser le mode flottant Arrière-plan @@ -100,7 +98,6 @@ Mémoriser les propriétés de la fenêtre flottante Mémoriser les dernières taille et position de la fenêtre flottante Effacer - G Le son peut être absent à certaines définitions Suggestions de recherche Sélectionner les suggestions à afficher lors d’une recherche @@ -210,15 +207,15 @@ Chargement du contenu demandé Importer la base de données Exporter la base de données - Remplace votre historique, vos abonnements, vos listes de lecture et (en option) vos paramètres - Exporte l’historique, les abonnements, les listes de lecture et les paramètres + Remplace votre historique, vos abonnements, vos playlists et (en option) vos paramètres + Exporte l’historique, les abonnements, les playlists et les paramètres Exporté Importé Fichier ZIP non valide Avertissement : impossible d’importer tous les fichiers. Cela effacera vos paramètres actuels. Afficher les informations - Listes de lecture enregistrées + Playlists enregistrées Ajouter à Glisser pour réordonner Créer @@ -227,17 +224,17 @@ Dernière lecture Vidéos les plus vues Toujours demander - Nouvelle liste de lecture + Nouvelle playlist Renommer Nom - Ajouter à la liste de lecture - Définir comme miniature de la liste de lecture - Enregister la liste de lecture + Ajouter à la playlist + Définir comme miniature de la playlist + Enregister la playlist Supprimer le signet - Voulez-vous supprimer cette liste de lecture \? - Liste de lecture créée - Ajouté à la liste de lecture - Miniature de la liste de lecture changée. + Voulez-vous supprimer cette playlist ? + Playlist créée + Ajouté à la playlist + Miniature de la playlist changée. Aucun sous-titre Ajuster Zoomer @@ -321,7 +318,7 @@ Aucune limite Limiter la définition lors de l’utilisation des données mobiles Chaînes - Listes de lecture + Playlists Morceaux Utilisateurs Accélérer pendant les silences @@ -358,7 +355,7 @@ Téléchargement échoué Délai de connexion expiré Conférences - ajouté à la liste de lecture + ajouté à la playlist Générer un nom unique Écraser Un fichier avec ce nom existe déjà @@ -389,7 +386,7 @@ Aucun commentaire Impossible de charger les commentaires Fermer - Reprendre la liste de lecture + Reprendre la playlist Effacer les données Fichier déplacé ou supprimé impossible d’écraser le fichier @@ -525,8 +522,7 @@ \nActivez « %1$s » dans les paramètres si vous voulez la voir. Supprimer les vidéos visionnées Oui ainsi que les vidéos partiellement visionnées - Les vidéos qui ont été visionnées avant et après avoir été ajoutées à la liste de lecture seront supprimées. -\nÊtes-vous certain(e)  \? Cette action est irréversible ! + Les vidéos qui ont été visionnées avant et après avoir été ajoutées à la playlist seront supprimées. \nÊtes-vous certain(e) ? Cette action est irréversible ! Supprimer les vidéos visionnées \? Miniature de l\'avatar de la chaine De %s @@ -535,9 +531,9 @@ Afficher la date originelle sur les items Activer le « Mode restreint » de YouTube Afficher uniquement les abonnements non groupés - Page des listes de lecture - Aucune liste de lecture encore enregistrée - Sélectionner une liste de lecture + Page des playlists + Aucune playlist encore enregistrée + Sélectionner une playlist Veuillez vérifier si un ticket concernant votre problème existe déjà. Lorsque vous créez des tickets dupliqués, cela nous prend du temps que nous pourrions passer à résoudre effectivement le problème. Signaler sur GitHub Copier le rapport formaté @@ -623,7 +619,6 @@ Étiquettes Catégorie Vous pouvez maintenant sélectionner du texte à l’intérieur de la description. Notez que la page peut scintiller et que les liens peuvent ne pas être cliquables en mode sélection. - %s indique le motif : Aucun dossier de téléchargement n’est défini pour le moment, sélectionnez le dossier de téléchargement par défaut Ouvrir le site web Compte résilié @@ -728,7 +723,7 @@ Appuyez pour télécharger %s Échec de la copie dans le presse-papiers Cette option est disponible seulement si %s est sélectionné pour le thème - Les listes de lecture grisées contiennent déjà cet élément. + Les playlists grisées contiennent déjà cet élément. Carte Utile si, par exemple, vous utilisez un casque avec des boutons dysfonctionnels Effacer les doublons @@ -789,7 +784,7 @@ Albums Qualité moyenne Bannières - Listes de lecture + Playlists Plus d’options Miniatures Pistes @@ -805,7 +800,7 @@ Partager une liste d\'URLs %1$s \n%2$s - Partager la liste de lecture + Partager la playlist - %1$s : %2$s Choisir quels onglets seront visibles sur les pages de chaîne Changer l’orientation de l’écran @@ -840,14 +835,34 @@ Pas assez d\'espace disponible sur l\'appareil Les paramètres de l\'export en cours d\'importation utilisent un format vulnérable qui a été déprécié depuis NewPipe 0.27.0. Assurez-vous que l\'export en cours d\'importation provient d\'une source fiable. Privilégiez les exports obtenues à partir de NewPipe 0.27.0 ou des versions plus récentes à l\'avenir. Le support pour l\'importation des paramètres dans ce format vulnérable sera bientôt complètement supprimé et les anciennes versions de NewPipe ne pourront plus importer les paramètres des exports des nouvelles versions. secondaire - Partager comme liste de lecture YouTube temporaire - Listes de lecture + Partager comme playlist YouTube temporaire + Playlists Sélectionnez un groupe de flux - Aucun groupe de flux n\'a encore été créé + Encore aucun groupe de flux créé Page du groupe de chaînes Rechercher %1$s Rechercher %1$s (%2$s) - Likes + J’aime Page SoundCloud Top 50 supprimée SoundCloud a abandonné le classement original du Top 50. L\'onglet correspondant a été supprimé de votre page d\'accueil. + Suppression des tendances combinées sur YouTube + YouTube a supprimé la page des tendances combinées depuis le 21 juillet 2025. NewPipe a remplacé la page des tendances par défaut par les diffusions en direct les plus populaires.\n\nVous pouvez également sélectionner différentes pages de tendances dans « Paramètres > Contenu > Contenu de la page principale ». + Tendances jeu vidéo + Tendances podcasts + Tendances films et séries + Tendances musique + %sK + %sM + %sB + Pour utiliser le lecteur contextuel, veuillez sélectionner %1$s dans le menu des paramètres Android suivant et activer %2$s. + « Autoriser l\'affichage sur d\'autres applications » + Supprimer le fichier + Supprimer l\'entrée + Entrée supprimée + Compte fermé\n\n%1$s fournit la raison suivante : %2$s + Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par l\'expiration de l\'URL de streaming ou une interdiction d\'IP + Erreur HTTP %1$s reçue du serveur pendant la lecture + Erreur HTTP 403 reçue du serveur pendant la lecture, probablement causée par un bannissement d\'IP ou des problèmes de désobfuscation de l\'URL de streaming + %1$s a refusé de fournir des données et a demandé un identifiant pour confirmer que le demandeur n\'est pas un robot.\n\nVotre adresse IP a peut-être été temporairement bannie par %1$s. Vous pouvez patienter un peu ou changer d\'adresse IP (par exemple en activant/désactivant un VPN, ou en passant du Wi-Fi aux données mobiles). + Ce contenu n\'est pas disponible pour le pays actuellement sélectionné.\n\nModifiez votre sélection dans « Paramètres > Contenu > Pays par défaut ». diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index d345b9385..aacaf9288 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -158,9 +158,6 @@ Vídeo Audio Tentar de novo - k - M - B Ningún subscrito %s subscrito @@ -557,7 +554,6 @@ Agora pode seleccionar o texto na descrición. Teña en conta que a páxina pode cintilar e as ligazóns poden non ser clicábeis no modo selección. Automático (Tema do dispositivo) Radio - %s dá este motivo: Este contido non está dispoñíbel no seu país. Capítulos Recentes diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index a09c0da03..c35281207 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -112,9 +112,6 @@ סרטון שמע ניסיון חוזר - אלפ. - מיל. - מיליארד אין מנויים מנוי אחד @@ -598,7 +595,7 @@ פתיחה באמצעות אין יישומון על המכשיר שלך שיכול לפתוח את זה להקריס את היישומון - תוכן זה זמין רק למשתמשים ששילמו, לכן לא ניתן להזרים או להוריד אותו עם NewPipe. + התוכן הזה זמין רק למשתמשים ששילמו, לכן לא ניתן להזרים או להוריד אותו עם NewPipe. סרטון זה זמין רק למנויי YouTube Music Premium, לכן לא ניתן להזרים או להוריד אותו עם NewPipe. זה תוכן פרטי, לכן לא ניתן להזרים או להוריד אותו עם NewPipe. רצועה זו של SoundCloud Go+‎ מוגבלת, לפחות במדינה שלך, לכן לא ניתן להזרים או להוריד אותה עם NewPipe. @@ -635,7 +632,6 @@ \nל־NewPipe לא תהיה אפשרות להוריד את ההזנה הזאת בעתיד. \nלהסיר את המינוי מהערוץ הזה\? פתיחת האתר - %s מספק את הסיבה הבאה: החשבון הושמד מצב ההזנה המהירה לא מספק מידע נוסף על כך. לא ניתן לטעון את ההזנה עבור ‚%s’. @@ -859,4 +855,26 @@ חיפוש ב־%1$s חיפוש ב־%1$s‏ (%2$s) לייקים + חשבון הושמד\n\n%1$s מספק את הסיבה הבאה: %2$s + כדי להשתמש בנגן צף, נא לבחור ב־%1$s בתפריט ההגדרות הבא של Android ולהפעיל את %2$s. + „תמיד להציג מעל יישומונים אחרים” + %s אלף + %s מיליון + %s מיליארד + המובילים המשולבים של YouTube הוסרו + מחיקת קובץ + מחיקת רשומה + עמוד 50 המובילים ב־SoundCloud הוסר + מגמות במשחקים + פודקאסטים מובילים + סרטים וסדרות מובילים + מוזיקה מובילה + הרשומה נמחקה + SoundCloud הפסיקו את מצעדי 50 הלהיטים המקוריים. הלשונית התואמת הוסרה מהעמוד הראשי שלך. + ב־YouTube הושבת עמוד המובילים המשולב החל מ־21 ביולי 2025. NewPipe החליפה את עמוד המובילים המוגדר כברירת מחדל בשידורים חיים מובילים.\n\nניתן גם לבחור עמודי מובילים שונים תחת „הגדרות > תוכן > תוכן הדף הראשי”. + שגיאת HTTP 403 שהתקבלה מהשרת בזמן השמעה, ככל הנראה נגרמת עקב פקיעת כתובת URL של סטרימינג או חסימת IP + שגיאת HTTP %1$s התקבלה מהשרת בזמן הניגון + שגיאת HTTP‏ 403 שהתקבלה מהשרת בזמן הניגון, ככל הנראה נגרמה עקב חסימת IP או בעיות בהסרת ערפול כתובת תזרים + %1$s סירב לספק נתונים, וביקש התחברות כדי לאשר שהמבקש אינו בוט.\n\nכנראה שה־IP שלך נחסם זמנית על ידי %1$s, ניתן להמתין זמן מה או לעבור ל־IP אחר (לדוגמה על ידי הפעלה/כיבוי של VPN, או על ידי מעבר מרשת אלחוטית לתקשורת נתונים סלולרית). + תוכן זה אינו זמין עבור מדינת התוכן שנבחרה כעת.\n\nניתן לשנות את בחירתך דרך „הגדרות > תוכן > מדינת תוכן ברירת מחדל”. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 7d405bcef..d2e6a422e 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -74,7 +74,7 @@ देखे गए वीडियोज़ की सूची रखें प्लेबैक फिर से शुरू करें रुकावटें (जैसे कि फ़ोन कॉल) खत्म होने के बाद वीडियो प्ले जारी रखें - \'अगले\' और \'सबंधित\' वीडियो दिखाएं + \'अगला\' और \'संबंधित\' वीडियो दिखाएं \"कतार में जोड़ने के लिए स्पर्श बनाये रखें\" दिखाएं जब बैकग्राउंड और पॉपअप बटन वीडियो के विवरण पन्ने में दबाई जाए तो सलाह दिखाएं असमर्थित URL @@ -93,7 +93,7 @@ बंद किया साफ करें उत्तम रिजॉल्युशन - वापिस + अन-डू करें सभी प्ले करें न्यूपाइप की नोटीफिकेशन न्यूपाइप के प्लेयर के लिए नोटीफिकेशन @@ -104,7 +104,7 @@ वैबसाइट parse नहीं हो सकी विषय वस्तु उपलब्ध नहीं है डाउनलोड मेनू स्थापित नहीं किया जा सका - APP/UI करैश हो गई + ऐप/UI करैश हो गई इस वीडियो को चलाने में असफल हुए अनचाही वीडियो प्लेयर त्रुटी आयी है वीडियो प्लेयर त्रुटी से ठीक हो रहा है @@ -126,9 +126,6 @@ वीडियो ऑडियो फिर से कोशिश करें - हज़ार - मिलियन - अरब कोई सब्सक्राइबर नहीं %s सब्सक्राइबर @@ -394,7 +391,7 @@ प्लेबैक स्थानों को मिटाएं सारे प्लेबैक स्थानों को मिटाता है सारे प्लेबैक स्थानों को मिटाएं\? - निपटान के बाद खंड या गतिविधि जीवन चक्र के बाहर अविभाज्य आरएक्स अपवादों की रिपोर्टिंग को बलपूर्वक लागू करें + हैंडलिंग के बाद फ्रैगमेंट या एक्टिविटी लूप के बाहर अनहैंडल्ड Rx एक्सेप्शन की रिपोर्टिंग को बलपूर्वक लागू करें साउंडक्लाउड प्रोफाइल निर्यात करने के लिए आईडी या युआरएल दीजिये: \n \n1. अपने वेब ब्राउज़र में \"डेस्कटॉप मोड\" चालू करें (वेबसाइट मोबाइल उपकरणों के लिए उपलब्ध नहीं है) @@ -409,7 +406,7 @@ ग्रिड ऑटो त्रुटि दिखाएं - सर्वर मल्टी थ्रेडेड डाउनलोड स्वीकार नहीं करता, पुनः कोशिश करे @string/msg_threads = 1 के साथ + सर्वर मल्टी थ्रेडेड डाउनलोड स्वीकार नहीं करता, @string/msg_threads = 1 के साथ पुनः कोशिश करें \'स्टोरेज एक्सेस फ्रेमवर्क\' आपको बाहरी एसडी कार्ड पर डाउनलोड करने देता है सेवा चुनें, वर्तमान चुनाव : डिफ़ॉल्ट कियोस्क @@ -505,7 +502,7 @@ अनगिनत विडीओज़ 100+ विडीओज़ विवरण - संबंधित स्ट्रीमस + संबंधित आइटम्स टिप्पणियाँ कृपया जांचें लें कि क्या आपके क्रैश पर चर्चा करने वाला मुद्दा पहले से मौजूद है। डुप्लिकेट टिकट बनाते समय, आप हमसे समय लेते हैं जो हम वास्तविक बग को ठीक करने के लिए खर्च कर सकते हैं। गिटहब पर रिपोर्ट करें @@ -658,7 +655,6 @@ मीडिया टनलिंग अक्षम करें \"क्रैश द प्लेयर\" दिखाएं लोड नहीं हुआ: %d - %s इसका कारण प्रदान करता है: टैग लाइसेंस यदि आपको ऐप का उपयोग करने में परेशानी हो रही है, तो सामान्य प्रश्नों के इन उत्तरों को देखना सुनिश्चित करें! @@ -832,4 +828,26 @@ चैनल समूह पेज पसंद यूट्यूब अस्थायी प्लेलिस्ट के रूप में साझा करें + एंटरी मिटा दी गई + फाईल डिलीट करें + एंटरी मिटाऐं + %sहज़ार + पॉपअप प्लेयर इस्तेमाल करने के लिए, कृपया नीचे दिए गए Android सेटिंग्स मेनू में %1$s चुनें और %2$s चालू करें। + “अन्य ऐप्स पर डिस्प्ले की अनुमति दें” + %sमिलीअन + %sअरब + अकाउंट बंद कर दिया गया\n\n%1$s यह कारण बताता है: %2$s + साउंडक्लाउड टॉप 50 पेज हटा दिया गया + साउंडक्लाउड ने ओरिजिनल टॉप 50 चार्ट बंद कर दिए हैं। इससे जुड़ा टैब आपके मेन पेज से हटा दिया गया है। + YouTube कंबाइंड ट्रेंडिंग हटा दी गई + YouTube ने 21 जुलाई 2025 से कंबाइंड ट्रेंडिंग पेज बंद कर दिया है। NewPipe ने डिफ़ॉल्ट ट्रेंडिंग पेज को ट्रेंडिंग लाइवस्ट्रीम से बदल दिया है।\n\nआप \"सेटिंग्स > कंटेंट > मेन पेज कंटेंट\" में अलग-अलग ट्रेंडिंग पेज भी चुन सकते हैं। + गेमिंग ट्रेंडस + ट्रेंडिंग पॉडकास्ट + ट्रेंडिंग फिल्में और शो + ट्रेंडिंग संगीत + पले करते समय सर्वर से HTTP error 403 मिला, शायद स्ट्रीमिंग URL एक्सपायर होने या IP बैन की वजह से हुआ + पले करते समय सर्वर से HTTP error %1$s मिला + पले करते समय सर्वर से HTTP error 403 मिला, जो शायद IP बैन या स्ट्रीमिंग URL डीओबफस्केशन की दिक्कतों की वजह से हुआ है + %1$s ने डेटा देने से मना कर दिया, और यह कन्फर्म करने के लिए लॉगिन मांगा कि रिक्वेस्ट करने वाला बोट नहीं है।\n\nहो सकता है कि %1$s ने आपके IP को कुछ समय के लिए बैन कर दिया हो, आप कुछ समय इंतज़ार कर सकते हैं या किसी दूसरे IP पर स्विच कर सकते हैं (जैसे VPN ऑन/ऑफ करके, या WiFi से मोबाइल डेटा पर स्विच करके)। + यह कंटेंट अभी चुने गए देश के कंटेंट के लिए उपलब्ध नहीं है।\n\n\"सेटिंग्स > कंटेंट > डिफ़ॉल्ट कंटेंट देश\" से अपना चुनाव बदलें। diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index c4cd60c0b..0eafb2930 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -2,7 +2,7 @@ Počni dodirom na povećalo. Objavljeno %1$s - Nije pronađen nijedan player streamova. Želiš li instalirati VLC\? + Nije pronađen nijedan player tokova. Želiš li instalirati VLC? Instaliraj Odustani Otvori u pregledniku @@ -97,9 +97,6 @@ Video Audio Pokušaj ponovo - tis. - mil - mlrd. Počni Pauziraj Izbriši @@ -128,7 +125,7 @@ © %1$s od %2$s pod %3$s O aplikaciji i ČPP Licence - Slobodan i mali streaming na Android uređaju. + Slobodan i mali tok na Android uređaju. Pogledaj na GitHubu NewPipe licenca Ako imate ideja za prijevod, promjene u dizajnu, čišćenje koda ili neke veće promjene u kodu, pomoć je uvijek dobro došla. Što više radimo, to bolji postajemo! @@ -163,7 +160,7 @@ %s videa Reproduciraj sve - Nije bilo moguće reproducirati ovaj stream + Nije bilo moguće reproducirati ovaj tok Dogodila se neoporavljiva greška playera Oporavljanje od greške playera Prikaži savjet za držanje @@ -234,7 +231,7 @@ Poništava tvoju trenutačnu povijest, pretplate, playliste i (opcionalno) postavke Izvezi povijest, pretplate, playliste i postavke Izbriši povijest gledanja - Briše povijest reproduciranih streamova i pozicije reprodukcije + Briše povijest reproduciranih tokova i pozicije reprodukcije Izbrisati cijelu povijest gledanja\? Povijest gledanja je izbrisana Izbriši povijest pretraživanja @@ -291,8 +288,8 @@ Bez ograničenja Ograniči rezoluciju tijekom korištenja mobilnih podataka Nijedan - Nije pronađen nijedan player streamova (možeš instalirati VLC za reprodukciju). - Preuzmi datoteku streama + Nije pronađen nijedan player tokova (možeš instalirati VLC za reprodukciju). + Preuzmi datoteku toka Koristi brzo netočno premotavanje Netočno premotavanje omogućuje playeru brže premotavanje uz manju točnost. Premotavanje od 5, 15 ili 25 sekundi s ovime ne radi Otkaži pretplatu @@ -312,8 +309,8 @@ Prikaži pogrešku Izbriši sve podatke web-stranica iz predmemorije Metapodaci su izbrisani - Automatski dodaj sljedeći stream u popisa izvođenja - Nastavi završavati (ne ponavljajući) popis reprodukcija dodavanjem povezanog streama + Automatski dodaj sljedeći tok u popisa izvođenja + Nastavi završavati (ne ponavljajući) popis reprodukcija dodavanjem povezanog toka Zadana zemlja sadržaja Otkrivanje grešaka Obavijest o novoj verziji aplikacije @@ -577,7 +574,7 @@ Obavijest šifriranja videa Obavijesti o napretku šifriranja videa Nedavni - Isključi za skrivanje polja metapodataka s dodatnim podacima o autoru streama, sadržaju streama ili zahtjevu za pretraživanje + Isključi za skrivanje polja metapodataka s dodatnim podacima o autoru toka, sadržaju toka ili zahtjevu za pretraživanje Prikaži metapodatke Povezane stavke Nijedna aplikacija na tvom uređaju ovo ne može otvoriti @@ -642,14 +639,13 @@ Interno Privatnost Sada možeš odabrati tekst u opisu. Napomena: stranica će možda treperiti i možda nećeš moći kliknuti poveznice u načinu rada za odabir teksta. - %s pruža ovaj razlog: Obrada u tijeku … Može malo potrajati Za ukljanjanje stavki povuci ih Prikaži indikatore slike - Preuzimanje je gotovo + %s preuzimanje je gotovo %s preuzimanja su gotova - %s preuzimanja su gotova + %s preuzimanja je gotovo Pokreni glavni player u cjeloekranskom prikazu Dodaj u popis kao sljedeći @@ -674,23 +670,23 @@ Došlo je do greške, pogledaj obavijest Prekini rad playera Obavijest playera - Konfiguriraj obavijest trenutačno reproduciranog streama + Konfiguriraj obavijest trenutačno reproduciranog toka Obavijesti Novi videozapisi - Obavijesti novih streamova od pretplaćenih kanala + Obavijesti novih tokova od pretplaćenih kanala Želiš li izbrisati sve preuzete datoteke\? Obavijesti su isljučene Pretplatio/la si se na ovaj kanal , Uključi/isključi sve Bilo kakva mreža - Obavijesti novih streamova pretplaćenih kanala + Obavijesti novih tokova pretplaćenih kanala Prikaži kratku poruku greške - Učitavanje pojedinosti streama … - Pokreni traženje novih streamova + Učitavanje pojedinosti toka … + Pokreni traženje novih tokova Prvjeravanje učestalosti Biblioteka „LeakCanary” nije dostupna - Obavijesti o novim streamovima + Obavijesti o novim tokovima Potrebna mrežna veza Primaj obavijesti Za ovu radnju nije pronađen odgovarajući upravljač datoteka. @@ -702,12 +698,12 @@ Posto Poluton Streamovi koje aplikacija za preuzimanje još ne podržava se ne prikazuju - Eksterni playeri ne podržavaju odabrani stream + Eksterni playeri ne podržavaju odabrani tok Promijenite veličinu intervala učitavanja progresivnog sadržaja (trenutno %s). Niža vrijednost može ubrzati učitavanje - %s novi stream - %s nova streama - %s novih streamova + %s novi tok + %s nova toka + %s novih tokova Veličina intervala učitavanja reprodukcije Nepoznat format @@ -834,4 +830,36 @@ Uvijek koristi ExoPlayer postavku zaobilaženja videa za izlaznu površinu Kartice za dohvaćanje prilikom aktualiziranja feeda. Ova opcija nema učinka ako se kanal aktualizira pomoću brzog modusa. sekundarno + Pretraži %1$s + Pretraži %1$s (%2$s) + Popisi izvođenja + Da biste koristili Popup Player, odaberite %1$s u sljedećem izborniku postavki Androida i omogućite %2$s. + \"Dopusti prikaz preko drugih aplikacija\" + %sK + %sM + %sB + Izbriši datoteku + Obriši unos + Odaberite grupu feedova + Još nije stvorena nijedna grupa feedova + Stranica grupe kanala + Račun ukinut\n\n%1$s navodi ovaj razlog: %2$s + Ovo zaobilazno rješenje oslobađa i ponovno instancira video kodeke kada dođe do promjene površine, umjesto da se površina izravno postavlja na kodek. Već se koristi 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 pogreške reprodukcije prilikom prebacivanja trenutnog video playera ili prebacivanja na cijeli zaslon + Lajkovi + Podijeli kao privremenu playlistu na YouTubeu + Postavke u izvozu koji se uvozi koriste ranjivi format koji je zastario od verzije NewPipe 0.27.0. Provjerite je li izvoz koji se uvozi iz pouzdanog izvora i u budućnosti radije koristite samo izvoze dobivene 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 NewPipea više neće moći uvoziti postavke izvoza iz novih verzija. + Stranica SoundCloud Top 50 uklonjena + SoundCloud je ukinuo originalne Top 50 ljestvice. Odgovarajuća kartica je uklonjena s vaše glavne stranice. + Uklonjeni kombinirani trendovi na YouTubeu + YouTube je ukinuo kombiniranu stranicu s trendovima od 21. srpnja 2025. NewPipe je zamijenio zadanu stranicu s trendovima s trendovima prijenosa uživo.\n\nTakođer možete odabrati različite stranice s trendovima u \"Postavke > Sadržaj > Sadržaj glavne stranice\". + Trendovi u igrama + Trendovi podcasti + Trendovi u filmovima i serijama + Glazba u trendu + Unos izbrisan + HTTP greška 403 primljena od poslužitelja tijekom reprodukcije, vjerojatno uzrokovana istekom URL-a za streaming ili zabranom IP adrese + HTTP greška %1$s primljena od poslužitelja tijekom reprodukcije + HTTP greška 403 primljena od poslužitelja tijekom reprodukcije, vjerojatno uzrokovana zabranom IP adrese ili problemima s deobfuskacijom URL-a za streaming + %1$s je odbio dati podatke, tražeći prijavu kako bi potvrdio da podnositelj zahtjeva nije bot.\n\nVašu IP adresu je možda privremeno zabranio %1$s, možete pričekati neko vrijeme ili se prebaciti na drugu IP adresu (na primjer uključivanjem/isključivanjem VPN-a ili prebacivanjem s WiFi-ja na mobilne podatke). + Ovaj sadržaj nije dostupan za trenutno odabranu zemlju sadržaja.\n\nPromijenite odabir u \"Postavke > Sadržaj > Zadana zemlja sadržaja\". diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index eb60e377f..12778fd32 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -49,7 +49,7 @@ Jelentés Információ: Ez történt: - Saját hozzászólás (angolul): + Saját megjegyzés (angolul): Részletek: Elnézést, valami balul sült el. Elnézést, ennek nem kellett volna megtörténnie. @@ -90,7 +90,7 @@ Néhány felbontásnál eltávolítja a hangot Feliratkozás Feliratkozva - További infó + További információ Felugró ablak alapértelmezett felbontása Magasabb felbontások megjelenítése Csak bizonyos eszközök tudnak 2K/4K-s videókat lejátszani @@ -166,9 +166,6 @@ Nincs letölthető adatfolyam Nincs itt semmi pár tücskön kívül Húzza az átrendezéshez - e - m - M Nincs feliratkozó %s feliratkozó @@ -222,11 +219,11 @@ Legtöbbet lejátszott Főoldal tartalma Üres oldal - Kioszk oldal + Témagyűjtemények Csatornaoldal Válasszon egy csatornát Még nincs csatornafeliratkozás - Válasszon egy kioszkot + Válasszon egy témagyűjteményt Exportálva Importálva Nem érvényes ZIP-fájl @@ -532,15 +529,14 @@ Fiók megnyitása Csatorna részleteinek megjelenítése Tartsa a sorba állításhoz - Alapértelmezett kioszk + Alapértelmezett témagyűjtemény A NewPipe egy copyleft szabad szoftver: tetszése szerint felhasználhatja, tanulmányozhatja, megoszthatja és fejlesztheti. Egész pontosan a Free Software Foundation által kiadott GNU General Public License 3-as, vagy (választható módon) újabb verziójának feltételei szerint módosíthatja vagy adhatja tovább. Megoldás Nyomja meg a „Kész” gombot, ha megoldotta Ujjlenyomat számítása Kapcsolódó elemek - Ellenőrizze, hogy létezik-e már olyan jegy, amely az összeomlásával foglalkozik. Ha duplikált jegyet ad fel, akkor olyan időt vesz el tőlünk, amelyet a hiba javítására tudnánk fordítani. + Ellenőrizze, hogy létezik-e már hibajegy a leírt összeomlással kapcsolatban. Az ismétlődő hibajegyek létrehozása feleslegesen elvonja az erőforrásokat a hiba tényleges javításától. Minimalizálás alkalmazásváltáskor - A(z) %s ezt az okot adta meg: Helyi keresési javaslatok Távoli keresési javaslatok A fő lejátszó teljes képernyős indítása @@ -728,7 +724,7 @@ Eltávolítja az összes ismétlődő közvetítést ebből a lejátszólistáról\? eredeti Kezdőlap pozíciója - A médiacsatornázás alapértelmezés szerint le van tiltva a saját eszközén, mivel a saját eszközmodellje nem támogatja azt. + A médiacsatornázás alapértelmezetten le van tiltva az eszközén, mivel a saját eszközmodellje nem támogatja azt. Kezdőlapválasztó alulra helyezése Nincs élő közvetítés Nincs adatfolyam @@ -805,4 +801,24 @@ Kedvelések SoundCloud Top 50 oldal eltávolítva A SoundCloud megszüntette az eredeti Top 50-es listákat. A megfelelő lap el lett távolítva a főoldalról. + YouTube „felkapott lapok” eltávolítva + A YouTube 2025. július 21-től megszüntette a „felkapott” oldalt. A NewPipe a korábbi alapértelmezett „felkapott” oldalt felkapott élő közvetítésekkel helyettesítette.\n\nA „Beállítások > Tartalom > Főoldal tartalma” menüpontban különböző felkapott lapokat is kiválaszthat. + Felkapott játékok + Felkapott podcastok + Felkapott filmek és sorozatok + Felkapott zenék + %se + %sm + %sM + A felugró ablakos lejátszó használatához válassza ki a(z) %1$s elemet a következő Android beállítások menüben, és engedélyezze a(z) %2$s elemet. + „Megjelenítés a többi alkalmazás fölött” engedélyezése + Fájl törlése + Bejegyzés törlése + Bejegyzés törölve + Fiók megszüntetve\n\n%1$s az alábbi ok miatt: %2$s + A lejátszás közben a kiszolgáló 403-as HTTP-hibát adott vissza, valószínűleg a közvetítési hivatkozás érvényessége lejárt vagy a IP-tiltás miatt + HTTP-hiba (%1$s) érkezett a kiszolgálótól a lejátszás során + HTTP 403-as hiba érkezett a kiszolgálótól a lejátszás közben, valószínűleg IP-tiltás vagy a közvetítési hivatkozás feloldási problémák miatt + %1$s visszautasította az adatok szolgáltatását, és bejelentkezést kér annak megerősítésére, hogy a kérés nem robot által érkezik.\n\nElőfordulhat, hogy az IP-címét ideiglenesen letiltotta %1$s, várhat egy keveset, vagy váltson egy másik IP-címre (például VPN be-/kikapcsolásával, vagy Wi-Fi-ről mobiladat-forgalomra váltva). + Ez a tartalom a jelenleg kiválasztott tartalom országában nem elérhető.\n\nVáltoztassa meg a „Beállítások > Tartalom >Tartalom alapértelmezett országa” menüpontban. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index b640a1232..bb9286dab 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -82,9 +82,6 @@ Meminta kode reCAPTCHA Hitam Semua - r - J - T Buka pada mode sembulan Izin ini dibutuhkan untuk \nmembuka di mode sembul @@ -602,7 +599,6 @@ Aktifkan dapat memilih teks pada deskripsi Anda sekarang dapat memilih teks di dalam deskripsi. Perhatikan bahwa halaman mungkin berkedip dan tautan tidak dapat diklik saat dalam mode pemilihan. Buka situs web - %s menyediakan alasan ini: Akun dinonaktifkan Mode langganan cepat tidak menyediakan lebih banyak info tentang ini. Akun kreator telah dinonaktifkan. @@ -820,4 +816,24 @@ Suka Halaman Top 50 SoundCloud dihapus SoundCloud telah menghentikan dukungan tangga lagu Top 50. Tab terkait telah dihapus dari halaman utama Anda. + Untuk menggunakan Pemutar Sembul, silakan pilih %1$s dalam menu pengaturan Android berikut dan aktifkan %2$s. + \"Izinkan menampilkan di atas aplikasi\" + Hapus berkas + Hapus entri + Akun dihapus\n\n%1$s menyediakan alasan ini: %2$s + Tren terpadu YouTube dihilangkan + YouTube telah mengakhiri halaman tren terpadu pada 21 Juli 2025. NewPipe mengganti halaman tren bawaan dengan tren siaran langsung.\n\nAnda juga dapat memilih halaman tren berbeda dalam \"Pengaturan > Konten > Konten di halaman utama\". + Tren permainan + Tren siniar + Tren film dan acara + Tren musik + Entri dihapus + Kesalahan HTTP 403 diterima dari server saat memutar, dapat disebabkan oleh URL streaming kedaluwarsa atau pemblokiran IP + Kesalahan HTTP %1$s diterima dari server saat memutar + Kesalahan HTTP 403 diterima dari server saat memutar, dapat disebabkan oleh pemblokiran IP atau masalah deobfuskasi URL streaming + %1$s menolak memberikan data, meminta login untuk memastikan peminta bukan bot.\n\nAlamat IP Anda mungkin telah diblokir sementara oleh %1$s, Anda dapat menunggu beberapa saat atau beralih ke alamat IP yang berbeda (misalnya dengan mengaktifkan/menonaktifkan VPN, atau beralih dari WiFi ke data seluler). + Konten ini tidak tersedia untuk negara konten yang saat ini dipilih.\n\nUbah pilihan Anda dari “Pengaturan > Konten > Negara konten bawaan”. + %sK + %sM + %sB diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index bc85f0e06..387bce955 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -97,7 +97,6 @@ Hljóðstillingar Spila í bakgrunni Þegar hlekkur er opnaður — %s - þús. Líkar ekki við Reyna aftur Lýsing @@ -217,7 +216,6 @@ Athugasemd þín (á ensku): Engar niðurstöður Myndskeið - ma. Engin áhorf %s áhorf @@ -352,7 +350,6 @@ Flokkur Merki NewPipe er þróað af sjálfboðaliðum sem eyða frítíma sínum í að færa þér bestu notendaupplifunina. Gefðu til baka til að hjálpa forriturum að gera NewPipe enn betri á meðan þeir njóta kaffibolla. - millj. Slökktu á til að fela lýsingu og viðbótarupplýsingar myndskeiðs Villa kom upp: %1$s Þraut reCAPTCHA @@ -576,7 +573,6 @@ Enginn viðeigandi skráarstjóri fannst fyrir þessa aðgerð. \nVinsamlegast settu upp skráarstjóra sem styður Geymsluaðgangsramma (SAF) Þetta efni er ekki fáanlegt í þínu landi. - %s gefur þessa ástæðu: Þetta efni er aðeins í boði fyrir notendur sem hafa greitt — það er ekki hægt að streyma því eða sækja með NewPipe. Sjálfvirk (þema tækis) Veldu uppáhalds næturþemu þína — %s @@ -813,4 +809,16 @@ Líkar við Topp 50 síða SoundCloud fjarlægð SoundCloud er hætt með Topp 50 vinsældalistann. Viðkomandi flipi hefur verið fjarlægður af aðalsíðunni þinni. + %sK + %sM + %sB + Vinsælir leikir + Vinsæl hlaðvörp + Vinsælar kvikmyndir og þættir + Vinsæl tónlist + \"Leyfa birtingu ofan á öðrum forritum\" + Eyða skrá + Eyða færslu + Aðgangi lokað\n\n%1$s gefur þessa ástæðu: %2$s + Færslu eytt diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d98875630..f73e7437a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -82,9 +82,6 @@ Risoluzione reCAPTCHA Nero Tutto - k - M - Mrd È richiesta la risoluzione del reCAPTCHA Apri in modalità popup Riproduzione in modalità popup @@ -622,7 +619,6 @@ Attiva la selezione del testo nella descrizione È possibile selezionare il testo all\'interno della descrizione. In modalità selezione la pagina potrebbe sfarfallare e i collegamenti potrebbero non essere cliccabili. Visita il sito - %s fornisce questa motivazione: Account chiuso Il recupero veloce dei feed non fornisce ulteriori informazioni al riguardo. L\'account dell\'autore è stato chiuso. @@ -848,4 +844,24 @@ Mi piace Pagina Top 50 di SoundCloud rimossa SoundCloud ha dismesso i grafici Top 50 originali. La scheda relativa è stata rimossa dalla pagina principale. + %sK + %sMilio. + %sMilia. + Rimosse tendenze combinate YouTube + YouTube ha interrotto la pagina di tendenza combinata il 21 luglio 2025. NewPipe ha sostituito la pagina di tendenza predefinita con le dirette in tendenza.\n\nPuoi anche selezionare diverse pagine di tendenza in \"Impostazioni > Contenuto > Contenuto della pagina principale\". + Giochi in tendenza + Podcast in tendenza + Film e spettacoli in tendenza + Musica in tendenza + Per usare il riproduttore popup, seleziona %1$s nel seguente menu delle impostazioni Android e attiva %2$s. + “Consenti la visualizzazione sopra altre app” + Elimina file + Elimina voce + Account eliminato\n\n%1$s fornisce questa motivazione: %2$s + Voce eliminata + Errore HTTP 403 ricevuto dal server durante la riproduzione, probabilmente causato dalla scadenza dell\'URL in streaming o da un divieto dell\'IP + Errore HTTP %1$s ricevuto dal server durante la riproduzione + Errore HTTP 403 ricevuto dal server durante la riproduzione, probabilmente causato da un divieto dell\'IP o problemi di de-offuscamento dell\'URL in streaming + %1$s ha rifiutato di fornire i dati, chiedendo un accesso per confermare che il richiedente non sia un bot.\n\nIl tuo IP potrebbe essere stato temporaneamente vietato da %1$s, puoi aspettare un po\' di tempo o passare ad un IP diverso (ad esempio accendendo/spegnendo una VPN, o passando dal WiFi ai dati mobili). + Questo contenuto non è disponibile per il Paese dei contenuti attualmente selezionato.\n\nModifica la selezione da \"Impostazioni > Contenuti > Paese dei contenuti predefinito\". diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 25c1d9fe9..3274062b5 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -83,9 +83,6 @@ reCAPTCHA を要求しました ブラック すべて - k - M - B ポップアップモードで開く ポップアップモードで開くには \n権限の許可が必要です @@ -615,7 +612,6 @@ オフ オン タブレットモード - %s がこの理由を提示: 表示しない 低品質 (小) 高品質 (大) diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 4fc00c0b3..819517ba4 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -237,9 +237,6 @@ ვიდეო აუდიო ხელახლა სცადეთ - ათასი - მლნ - ბლნ სერვისის გადართვა, ამჟამად არჩეულია: გამოწერები არ არის @@ -548,7 +545,6 @@ ეს ხელმიუწვდომელია თქვენი ქვეყნიდან. ეს მასალა პირადულია, ამიტომაც NewPipe-ს მისი არც მთლიანად და არც თანდათანობით ჩამოწერა არ შეუძლია. ანგარიში შეწყვეტილია - %s იძლევა ამ მიზეზს: ეს მასალა ხელმისაწვდომია მხოლოდ გადამხდელებისთვის, ამიტომაც NewPipe-ს მისი არც მთლიანად და არც თანდათანობით ჩამოწერა არ შეუძლია. გამორჩეული რადიო @@ -741,4 +737,60 @@ არჩიე თავდაპირველი ხმის ჩანაწერი ხმა: %s ხმის ჩანაწერი + დიახ + არა + %1$s-ის ძიება + მოძებნეთ %1$s %2$s + დასაკრავი სიები + ქვემოთ მოცემული თითოეული შეტყობინების მოქმედების რედაქტირებისთვის მასზე შეხებით აირჩიეთ. პირველი სამი მოქმედება (დაკვრა/პაუზა, წინა და შემდეგი) სისტემის მიერ არის დაყენებული და მათი მორგება შეუძლებელია. + აღწერილობითი აუდიოს უპირატესობა + სარეზერვო ასლის შექმნა და აღდგენა + მთავარი ჩანართის ამომრჩევის ქვემოთ გადატანა + მთავარი ჩანართების პოზიცია + ამომხტარი ფანჯრის გამოსაყენებლად, გთხოვთ, აირჩიოთ %1$s შემდეგ Android პარამეტრების მენიუში და ჩართოთ %2$s. + “სხვა აპლიკაციებზე ჩვენების დაშვება“ + %sათასი + %sმლნ + %sმლრდ + ნაკადები არ არის + პირდაპირი ტრანსლაციები არ არის + ფაილის წაშლა + ჩანაწერის წაშლა + აირჩიეთ არხის ჯგუფი + არხის ჯგუფი ჯერ არ შექმნილა + მედიის გვირაბირება თქვენს მოწყობილობაზე ნაგულისხმევად გამორთულია, რადგან თქვენი მოწყობილობის მოდელი, როგორც ცნობილია, მას არ უჭერს მხარს. + NewPipe-ს შეუძლია დროდადრო ავტომატურად შეამოწმოს ახალი ვერსიები და შეგატყობინოთ, როგორც კი ისინი ხელმისაწვდომი გახდება.\nგსურთ ამის ჩართვა? + პარამეტრების გადაყენება + ყველა პარამეტრის ნაგულისხმევ მნიშვნელობებზე დაბრუნება + ყველა პარამეტრის გადატვირთვა გააუქმებს თქვენს მიერ არჩეულ ყველა პარამეტრს და გადატვირთავს აპლიკაციას.\n\nდარწმუნებული ხართ, რომ გსურთ გაგრძელება? + მოწყობილობაზე საკმარისი თავისუფალი ადგილი არ არის + არხის ჯგუფის გვერდი + არხის ჩანართების მოძიება + არხის განახლებისას გამოსატანი ჩანართები. ამ პარამეტრს არანაირი ეფექტი არ აქვს, თუ არხი სწრაფი რეჟიმის გამოყენებით განახლდება. + ანგარიში შეწყვეტილია\n\n%1$s ამ მიზეზს იძლევა: %2$s + მინიატურები + ამტვირთავის ავატარები + ქვეარხის ავატარები + ავატარები + ბანერები + გამომწერები + ამ ნაკადში აუდიო ჩანაწერი უკვე უნდა იყოს წარმოდგენილი + გარე პლეერებისთვის აუდიო ჩანაწერის არჩევა + უცნობი + ExoPlayer-ის პარამეტრები + ExoPlayer-ის ზოგიერთი პარამეტრის მართვა. ამ ცვლილებების ძალაში შესასვლელად მოთამაშის გადატვირთვაა საჭირო. + გამოიყენეთ ExoPlayer-ის დეკოდერის სარეზერვო ფუნქცია + ჩართეთ ეს პარამეტრი, თუ დეკოდერის ინიციალიზაციის პრობლემები გაქვთ, რაც, თუ პირველადი დეკოდერების ინიციალიზაცია ვერ მოხერხდა, დაბალი პრიორიტეტის მქონე დეკოდერებს ეხება. ამან შეიძლება გამოიწვიოს დაკვრის დაბალი შესრულება, ვიდრე პირველადი დეკოდერების გამოყენებისას. + ყოველთვის გამოიყენეთ ExoPlayer-ის ვიდეო გამომავალი ზედაპირის პარამეტრების ალტერნატიული გადაწყვეტა + ეს გამოსავალი ათავისუფლებს და ხელახლა ააქტიურებს ვიდეო კოდეკებს ზედაპირის ცვლილებისას, ზედაპირის პირდაპირ კოდეკზე დაყენების ნაცვლად. ეს პარამეტრი უკვე გამოიყენება ExoPlayer-ის მიერ ზოგიერთ მოწყობილობაზე, რომელსაც ეს პრობლემა აქვს, და მოქმედებს მხოლოდ Android 6-ზე და უფრო მაღალ ვერსიებზე.\n\nამ პარამეტრის ჩართვამ შეიძლება თავიდან აიცილოს დაკვრის შეცდომები მიმდინარე ვიდეო პლეერის გადართვისას ან სრულ ეკრანზე გადართვისას. + თამაშების ტრენდები + ტრენდული პოდკასტები + ტრენდული ფილმები და შოუები + ტრენდული მუსიკა + ჩანაწერი წაშლილია + დაკვრის დროს სერვერიდან მიღებული HTTP შეცდომა 403, სავარაუდოდ, გამოწვეული სტრიმინგის URL-ის ვადის გასვლით ან IP აკრძალვით. + დაკვრის დროს სერვერიდან მიღებული HTTP შეცდომა %1$s + დაკვრის დროს სერვერიდან მიღებული HTTP შეცდომა 403, სავარაუდოდ, გამოწვეულია IP აკრძალვით ან სტრიმინგის URL-ის დებფუსკაციის პრობლემებით. + %1$s-მა უარი თქვა მონაცემების მიწოდებაზე და ითხოვა შესვლა იმის დასადასტურებლად, რომ მომთხოვნი რობოტი არ არის.\n\nშესაძლოა, თქვენი IP მისამართი დროებით აიკრძალა %1$s-ის მიერ, შეგიძლიათ დაელოდოთ ცოტა ხანს ან გადახვიდეთ სხვა IP მისამართზე (მაგალითად, VPN-ის ჩართვით/გამორთვით, ან WiFi-დან მობილურ მონაცემებზე გადართვით). + ეს კონტენტი ამჟამად არჩეული კონტენტის ქვეყნისთვის მიუწვდომელია.\n\nშეცვალეთ თქვენი არჩევანი „პარამეტრები > კონტენტი > ნაგულისხმევი კონტენტის ქვეყანა“-დან. diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 97476fc46..29db74172 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -7,9 +7,9 @@ Sbedd asnas n Kore yexxuṣen\? Sbedd Asider - Isidar - Isidar - Iɣewwaṛen + Isadaren + Isadaren + Iɣewwaren Akter… Tulya n telɣut… Awurman @@ -82,7 +82,6 @@ Sider Asfaylu udhim Ttu - A Kter Ih Amazray @@ -128,7 +127,6 @@ Snifel isem Asider ur yeddi ara Tamwalit - o Aɣawas n deffir Amazray yesteɛfay diff --git a/app/src/main/res/values-kmr/strings.xml b/app/src/main/res/values-kmr/strings.xml index b5e5235d5..24bc5574e 100644 --- a/app/src/main/res/values-kmr/strings.xml +++ b/app/src/main/res/values-kmr/strings.xml @@ -49,9 +49,6 @@ Ne abone Karûbarê veguheztinê, niha hatî hilbijartin: - B - M - k Dîsa biceribîne Deng Vîdyo diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index cfc328a20..39f854d35 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -24,7 +24,7 @@ 다운로드 다음/유사한 비디오 표시 지원하지 않는 URL입니다 - 기본 컨텐츠 언어 + 기본 콘텐츠 언어 비디오 및 오디오 비디오 재생, 구간: 업로더 썸네일 @@ -42,13 +42,13 @@ 백그라운드에서 재생 중 네트워크 오류 돋보기를 탭하여 시작합니다. - 컨텐츠 - 연령 제한 컨텐츠 보여주기 + 콘텐츠 + 연령 제한 콘텐츠 보여주기 라이브 오류 모든 썸네일을 불러올 수 없습니다 웹사이트를 가져올 수 없습니다 - 컨텐츠를 사용할 수 없습니다 + 콘텐츠를 사용할 수 없습니다 다운로드 메뉴를 설정할 수 없습니다 죄송합니다. 오류가 발생했습니다. 이메일을 통해 보고 @@ -116,9 +116,6 @@ 무엇:\\n요청:\\n콘텐츠 언어:\\n콘텐츠 국가:\\n앱 언어:\\n서비스:\\nGMT 시간:\\n패키지:\\n버전:\\nOS 버전: 결과 없음 구독할 항목을 추가하세요 - - 백만 - 십억 구독자 없음 구독자 %s명 @@ -287,7 +284,7 @@ 영상과 소리 분리 (왜곡이 발생할 수 있음) 다운로드 가능한 스트림이 없습니다 선호하는 열기 동작 - 컨텐츠를 열 때 사용할 기본 동작 — %s + 콘텐츠를 열 때 사용할 기본 동작 — %s 자막 플레이어 자막 글자 크기와 배경 스타일을 수정합니다. 적용하려면 앱을 다시 시작해야 합니다 채널 @@ -648,7 +645,6 @@ 챕터 최근 계정이 해지됨 - %s은(는) 다음과 같은 이유를 제공: 이것은 적어도 귀하의 국가에서 SoundCloud Go+ 트랙이므로 NewPipe에서 스트리밍하거나 다운로드할 수 없습니다. 자동 (장치 테마) 고정된 댓글 @@ -809,4 +805,25 @@ 설정 초기화 모든 설정을 기본값으로 초기화 + YouTube 임시 재생목록으로 공유 + SoundCloud Top 50 페이지가 삭제되었습니다 + SoundCloud에서 더 이상 기존 Top 50 차트를 제공하지 않습니다. 해당하는 탭이 메인 페이지에서 제거되었습니다. + %s천 + %s백만 + %s십억 + 파일 삭제 + 피드 그룹 선택 + 피드 그룹을 생성하지 않았습니다 + 채널 그룹 페이지 + 계정 정지됨\n\n%1$s에서 제공한 이유: %2$s + 재생목록 + %1$s 검색 + %1$s 검색 (%2$s) + \"다른 앱 위에 표시 허용\" + YouTube 통합 인기 급상승 동영상이 삭제되었습니다 + YouTube에서 2025년 7월 21일부로 더 이상 통합 인기 급상승 동영상을 제공하지 않습니다. NewPipe에서는 기본 인기 급상승 페이지를 인기 급상승 실시간 페이지로 교체했습니다.\n\n\"설정 > 콘텐츠 > 메인 화면의 내용\"에서 다른 인기 급상승 페이지를 선택할 수 있습니다. + 인기 급상승 게임 + 인기 급상승 팟캐스트 + 인기 급상승 영화 및 쇼 + 인기 급상승 음악 diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 2f3934fff..3dc51fcc8 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -133,9 +133,6 @@ ڤیدیۆ دەنگ هەوڵدانەوە - هەزار - ملیۆن - بلیۆن هیچ بەشداربوویەک نییە %s بەشداربوو diff --git a/app/src/main/res/values-lmo/strings.xml b/app/src/main/res/values-lmo/strings.xml new file mode 100644 index 000000000..80f3dd9c6 --- /dev/null +++ b/app/src/main/res/values-lmo/strings.xml @@ -0,0 +1,5 @@ + + + Pigia la lente per inziaa. + Canai + diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index feb11a01c..20419fef6 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -139,9 +139,6 @@ Atstatoma po grotuvo klaidos Nėra rezultatų Čia nieko nėra išskyrus svirplius - Tūkst. - Mln. - Mlrd. Nėra prenumeratorių Nėra peržiūrų @@ -626,7 +623,6 @@ Įgalinti teksto pasirinkimą apraše Neleisti pasirinkti teksto apraše Dabar apraše galite pasirinkti tekstą aprašyme. Atminkite, kad puslapis gali mirgėti, o nuorodos gali būti nespustelėjamos, kai veikia pasirinkimo režimas. - %s pateikia šią priežastį: Paskyra anuliuota Greito srauto režimas nesuteikia daugiau informacijos apie tai. Autoriaus paskyra anuliuota. @@ -840,4 +836,26 @@ Grojaraščiai Antrinis Dalintis kaip laikinuoju youtube grojaraščiu + Ieškoti %1$s + Ieškoti %1$s (%2$s) + Norėdami įjungti \"Popup Grotuvą\" pasirinkite Android nustatymų meniu pasirinkite %1$s ir įjunkite %2$s. + \"Leisti piešti virš kitų langų\" + %sK + %sM + %sB + Pašalinti failą + Ištrinti įrašą + Pasirinkite kanalo grupę + Dar nėra kanalo grupės + Kanalo grupės puslapis + Paskyra pašalinta\n\n%1$s dėl šios priežasties: %2$s + Mėgsta + SoundCloud Top 50 puslapis pašalintas + SoundCloud nebeteikia Top 50. Šis puslapis pašalintas iš jūsų pagrindinio puslapio. + YouTube sujungti rekomenduojami pašalinti + Žaidimų pasiūlymai + Mėgstami podcasts + Mėgstami filmai ir laidos + Mėgstama muzika + Įrašas pašalintas diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 2d1c60256..0eb6e4201 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -136,9 +136,6 @@ Nav abonamentu Izvēlaties pakalpojumu, šobrīd izvēlēts: - B - M - k Atkārtot Audio Video @@ -530,7 +527,7 @@ Netika atrasts video atskaņotājs. Uzstādīt VLC? Publicēts %1$s Nospiediet uz meklēšanas ikonas, lai sāktu. - Iekrāsot paziņojumu + Pielāgot paziņojumu krāsu Nekas Ielādējas Sajaukt @@ -634,7 +631,6 @@ \nNewPipe turpmāk nevarēs ielādēt šo plūsmu. \nVai vēlaties atteikties no šī kanāla abonēšanas\? Ātrās straumes režīms nesniedz vairāk informācijas par šo. - %s dod šādu pamatojumu: Izslēgt teksta atlasīšanu video aprakstā Iekšeji Autors piekrīt diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index ff3f9b687..3a8fa2f07 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -146,9 +146,6 @@ Видео Звук Пробај повторно - илјади - M - милијарди Нема зачленети %s зачленет @@ -434,7 +431,6 @@ Неуспешно вчитување на новинска лента за „%s“. Прикажи / скриј стримови Оваа содржина е приватна, така што не може да биде емитувана или преземена од страна на NewPipe. - %s ја посочува следната причина: Истакнато Радио Автоматски (режим на уредот) diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 39ce5e57f..5a449025e 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -183,9 +183,6 @@ സബ്ക്രൈബേഴ്സ് ഇല്ല സേവനം മാറ്റുക, ഇപ്പോൾ തിരഞ്ഞെടുത്തത്: - B - k - M വീണ്ടും ശ്രമിക്കുക ഓഡിയോ വീഡിയോ @@ -616,7 +613,6 @@ ടാഗുക്കൾ വിഭാഗം താക്കൾക് ഇപ്പോൾ ഡിസ്ക്രിപ്ഷൻ ബോക്സിലെ ടെക്സ്റ്റ്‌ തിരഞ്ഞെടുക്കാൻ സാധിക്കും. ശ്രെദ്ധിക്കുക സെലെക്ഷൻ മോഡിൽ പേജ് ചിലപ്പോൾ മിന്നുകയും ലിങ്കുകൾ ക്ലിക്ക് ചെയ്യാനാകാതെയും വന്നേക്കാം. - ഇതിന്റെ കാരണം %s നൽകും: അക്കൗണ്ട് ഇല്ലാതായിരിക്കുന്നു ഫാസ്റ്റ് ഫീഡ് മോഡ് കൂടുതൽ വിവരങ്ങൾ നൽകില്ല. സൃഷ്ടാവിന്റെ അക്കൗണ്ട് ഇല്ലാതായിരിക്കുന്നു. diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 304858d84..7a6b2eda4 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -138,11 +138,8 @@ डेबग अपडेट थेट - प्लेलिस्ट - फाईल - के परवाना चेकसम इतिहास diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index bb0527655..e70e61a74 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -171,9 +171,6 @@ Video Audio Cuba semula - K - J - B Tiada pelanggan %s pelanggan diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index c2311585a..d5edc2060 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -89,9 +89,6 @@ Spiller av i oppsprettsmodus Alle Avskrudd - k - M - Mrd. Denne tilgangen trengs for \nåpning i oppsprettsmodus reCAPTCHA-oppgave forespurt @@ -603,7 +600,6 @@ \nØnsker du å oppheve ditt abonnement på denne kanalen\? Skru av merking av tekst i beskrivelsen Skru på merking av tekst i beskrivelsen - %s oppgav denne grunnen: Konto terminert Kunne ikke laste inn informasjonskanal for «%s». Kunne ikke laste inn informasjonskanal diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index dd570b82e..b40145aa6 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -176,9 +176,6 @@ भिडियो अडियो पुन: प्रयास - हजार - करोड - अर्ब कुनै सदस्यहरू छैनन् %s सदस्य diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml index a78b96585..02d772023 100644 --- a/app/src/main/res/values-nl-rBE/strings.xml +++ b/app/src/main/res/values-nl-rBE/strings.xml @@ -147,9 +147,6 @@ Video Geluid Opnieuw proberen - k - M - mld. Geen abonnees %s abonnee @@ -638,4 +635,6 @@ Verkies beschrijvende audio Verkies originele audio Selecteer het oorspronkelijke audiospoor, ongeacht de taal + Zoeken%1$s + Zoeken%1$s(%2$s) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0c16a5c9b..4c8a28745 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -84,9 +84,6 @@ reCAPTCHA-uitdaging gevraagd Openen in pop-upmodus Alles - dznd. - mln. - mld. Deze machtiging is vereist om te \nopenen in pop-upmodus Speelt af in pop-upmodus @@ -615,7 +612,6 @@ Aan Tablet-modus Website openen - %s geeft de volgende reden: Account getermineerd De snelle feed mode levert hierover niet meer informatie. De account van de auteur is getermineerd. @@ -829,4 +825,29 @@ Selecteer een feedgroep Kanaalgroep­pagina Nog geen feedgroep geselecteerd + Zoeken met %1$s + Zoeken met %1$s (%2$s) + Vind-ik-leuks + Trending podcasts + Trending games + Trending films en series + Trending muziek + ‘SoundCloud Top 50’-pagina verwijderd + SoundCloud heeft de originele Top 50-hitlijsten stopgezet. Het bijbehorende tabblad is van uw hoofdpagina verwijderd. + YouTube gecombineerde trending verwijderd + YouTube heeft de gecombineerde trending­pagina per 21 juli 2025 stopgezet. NewPipe heeft de standaard­trendingpagina vervangen door de trending livestreams.\n\nU kunt ook andere trending­pagina\'s selecteren via \'Instellingen > Inhoud > Inhoud van de hoofdpagina\'. + %s dznd. + %s mln. + %s mld. + Bestand verwijderen + Item verwijderen + Item verwijderd + Om de Pop-up-speler te gebruiken, selecteert u %1$s in het volgende Android-instellingenmenu en schakelt u %2$s in. + ‘Weergeven vóór andere apps toestaan’ + Account beëindigd\n\n%1$s geeft de volgende reden: %2$s + HTTP-fout 403 ontvangen van de server tijdens het afspelen, waarschijnlijk veroorzaakt door het verlopen van de streaming-url of een ip-blokkade + HTTP-fout %1$s ontvangen van de server tijdens het afspelen + HTTP-fout 403 ontvangen van de server tijdens het afspelen, waarschijnlijk veroorzaakt door een ip-blokkade of problemen met de deobfuscatie van de streaming-url + %1$s weigerde gegevens te verstrekken en vroeg om een login om te bevestigen dat de aanvrager geen bot is.\n\nUw ip-adres is mogelijk tijdelijk geblokkeerd door %1$s. U kunt even wachten of overschakelen naar een ander ip-adres (bijvoorbeeld door een vpn in of uit te schakelen, of door over te schakelen van wifi naar mobiele data). + Deze inhoud is niet beschikbaar voor het momenteel geselecteerde inhouds­land.\n\nWijzig uw selectie via ‘Instellingen > Inhoud > Standaard­land voor inhoud’. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 016ce1ba2..caf8509e3 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -178,9 +178,6 @@ ߞߐߝߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߡߍ߲ߕߊ ߞߵߊ߬ ߡߊߛߊ߬ߦߌ߬ - ߥߊ߯ - ߞߋ߲߬ - ߥߟߡ ߞߊ߬ ߥߏ߬ߦߏ߫ ߣߊ߬ߕߊ ߝߙߊ߬ ߕߎ߲߰ߠߌ߲ ߠߊ߫ ߞߍ߲ߖߘߍߡߊߓߟߏ ߡߊ߬ ߞߊ߬ ߕߎ߲߰ߠߌ߲ ߘߐߞߊ߬ߙߊ߲ ߓߟߏߕߎ߰ (ߞߊߣߊ߬ ߡߊߛߊ߬ߦߌ߬) ߥߏ߬ߦߏ߫ ߢߐ߲߰ߘߐ ߟߎ߫ ߟߊ߫ ߕߏߟߏ߲ߟߊ߲߫ ߥߊ߲߬ߥߊ߲ ߣߎߡߊ߲߫ ߕߟߊ ߖߍ߰ߙߍ ߛߎߥߊ߲ߘߌ߫ @@ -630,7 +627,6 @@ ߞߐߕߐ߯ ߡߊߡߙߊߟߊ߲߫ ߛߌ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬ ߞߋߥߊߟߌ ߣߌ߲߬ ߞߊ߲ߡߊ߬. \nߘߌ߬ߢߍ߬ ߦߋ߫ ߞߐߕߐ߯ ߡߊߡߙߊߟߊ߲ ߘߏ߫ ߡߊߞߍ߫ ߡߍ߲ ߣߌ߫ ߡߙߊ߬ߘߐ߬ߦߊ ߟߊߛߐ߬ߘߐ߲ ߡߎ߬ߙߊ߲߬ߞߊ߲ߞߋ ߘߌ߫ ߓߍ߲߬ ߦߋߡߍ߲ߕߊ ߘߌ߫ ߡߊߛߐ߬ߘߐ߲߬ YouTube Music Premium ߛߌ߲߬ߝߏ߲ ߠߎ߬ ߟߋ߬ ߘߐߙߐ߲߫ ߓߟߏ߫߸ ߏ߬ ߘߐ߫ ߊ߬ ߕߍ߫ ߛߋ߫ ߘߐߛߊߙߌ߫ ߟߊ߫ ߥߟߊ߫ ߞߵߊ߬ ߟߊߖߌ߰ ߣߌߎߔߌߔ ߓߟߏ. - %s ߦߋ߫ ߞߎ߲߭ ߣߌ߲߬ ߠߋ߬ ߝߐ߫ ߟߊ߫: ߛߊ߲ߞߊߥߟߌ ߥߎߢߊ߲ߓߍ߲ ߖߘߍ߬ߢߍ߫ (ߕߙߏߞߏ߫ ߛߊߛߊ) diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index cf9ebdb97..81184d526 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -438,7 +438,6 @@ ନାପସନ୍ଦ ମନ୍ତବ୍ୟ ଗୁଡିକ ବର୍ଣ୍ଣନା - ନିୟୁତ ସମାଧାନ ପ୍ଲେବେକ୍ ସ୍ପିଡ୍ ନିୟନ୍ତ୍ରଣ ଟେମ୍ପୋ @@ -487,7 +486,6 @@ ସଦସ୍ୟତା ଚୟନ କରନ୍ତୁ କୌଣସି ସଦସ୍ୟତା ଚୟନ ହୋଇନାହିଁ ଦ୍ରୁତ ମୋଡ୍ ସକ୍ଷମ କରନ୍ତୁ - %s ଏହି କାରଣ ପ୍ରଦାନ କରେ: ଚ୍ୟାନେଲର ଅବତାର ଥମ୍ୱନେଲ୍ ବୈଶିଷ୍ଟ୍ୟ ରେଡିଓ @@ -528,7 +526,6 @@ ସମ୍ବନ୍ଧୀୟ ଆଇଟମ୍ ଗୁଡ଼ିକ ପୁନଃ ସଯାଇବାକୁ ଡ୍ରାଗ୍ କରନ୍ତୁ ବିରାମ - ଵୃନ୍ଦ କୌଣସି ଗ୍ରାହକ ନାହାଁନ୍ତି ସୃଷ୍ଟି କରନ୍ତୁ ବିବରଣୀ ପାଇଁ ଟ୍ୟାପ୍ କରନ୍ତୁ @@ -609,7 +606,6 @@ ବହିଃ-ଚାଳକ ପାଇଁ ଗୁଣବତ୍ତା ଚୟନ କରନ୍ତୁ ପିନ୍ ହୋଇଥିବା ମନ୍ତବ୍ୟ ୱେବସାଇଟ୍ ଖୋଲନ୍ତୁ - ହଜାର ସୂଚନା ପାଇବା… %s ଗ୍ରାହକ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 757632994..9dfc8143e 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -29,7 +29,7 @@ ਨਵਾਂ ਕੀ ਹੈ ਬੈਕਗ੍ਰਾਊਂਡ ਪੌਪ-ਅਪ - ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ + ਦੇ ਵਿੱਚ ਜੋੜ੍ਹੋ ਵੀਡੀਓ ਲਈ ਡਾਊਨਲੋਡ ਫ਼ੋਲਡਰ ਡਾਊਨਲੋਡ ਕੀਤੀਆਂ ਵੀਡੀਓ ਫ਼ਾਈਲਾਂ ਇੱਥੇ ਜਮ੍ਹਾਂ ਹੁੰਦੀਆਂ ਹਨ ਵੀਡੀਓ ਫ਼ਾਈਲਾਂ ਲਈ ਡਾਊਨਲੋਡ ਫ਼ੋਲਡਰ ਚੁਣੋ @@ -93,7 +93,7 @@ ਬੰਦ ਕੀਤਾ ਸਾਫ ਕਰੋ ਵਧੀਆ ਰੈਜ਼ੋਲਿਊਸ਼ਨ - ਵਾਪਿਸ + ਅਣ-ਕੀਤਾ ਕਰੋ ਸਾਰੇ ਚਲਾਓ ਹਮੇਸ਼ਾਂ ਸਿਰਫ਼ ਇਸ ਬਾਰ @@ -153,9 +153,6 @@ ਵੀਡੀਓ ਆਡੀਓ ਦੋਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ - ਹਜ਼ਾਰ - ਮਿਲੀਅਨ - ਅਰਬ ਕੋਈ ਸਬਸਕ੍ਰਾਈਬਰ ਨਹੀਂ %s ਸਬਸਕ੍ਰਾਈਬਰ @@ -187,8 +184,7 @@ ਕ੍ਰਿਪਾ ਕਰਕੇ ਉਡੀਕ ਕਰੋ… ਕਲਿਪ-ਬੋਰਡ ਵਿੱਚ ਕਾਪੀ ਹੋ ਗਿਆ ਹੈ ਬਾਅਦ ਵਿੱਚ ਸੈਟਿੰਗਾਂ ਵਿਚੋਂ ਇੱਕ ਡਾਊਨਲੋਡ ਫੋਲਡਰ ਨੂੰ ਚੁਣੋ - ਪੌਪ-ਅਪ ਮੋਡ ਵਿੱਚ ਖੋਲ੍ਹਣ ਵਾਸਤੇ -\nਇਸ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ + ਪੌਪ-ਅਪ ਮੋਡ ਵਿੱਚ ਖੋਲ੍ਹਣ ਵਾਸਤੇ\nਇਸ ਇਜਾਜ਼ਤ ਦੀ ਲੋੜ ਹੈ 1 ਆਈਟਮ ਮਿਟਾਈ ਗਈ। ReCaptcha ਚੁਣੌਤੀ ReCaptcha ਚੁਣੌਤੀ ਲਈ ਬੇਨਤੀ @@ -281,36 +277,19 @@ ਪਿੱਛਲਾ ਐਕਸਪੋਰਟ ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਇੰਪੋਰਟ ਨਹੀਂ ਹੋ ਸਕੀਆਂ ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਐਕਸਪੋਰਟ ਨਹੀਂ ਹੋ ਸਕੀਆਂ - ਗੂਗਲ ਟੇਕਅਊਟ ਤੋਂ ਯੂਟਿਊਬ ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਇੰਪੋਰਟ ਕਰਨ ਲਈ ਐਕਸਪੋਰਟ ਫਾਈਲ ਡਾਊਨਲੋਡ ਕਰੋ: -\n -\n1. ਇਸ URL ਤੇ ਜਾਓ: %1$s -\n2. ਮੰਗਣ ਤੇ ਆਪਣੇ ਖਾਤੇ \'ਚ ਲਾਗ-ਇਨ ਕਰੋ -\n3. ਕਲਿੱਕ ਕਰੋ \" All data incuded\" ਤੇ, ਫੇਰ \"Deselect all\" ਤੇ ਫੇਰ ਸਿਰਫ \"subscriprion\" ਚੁਣੋ ਅਤੇ \"OK\" ਕਰੋ -\n4. \"Next step\" ਤੇ ਕਲਿੱਕ ਕਰੋ ਤੇ ਫੇਰ \"create export\" ਤੇ -\n5. ਡਾਊਨਲੋਡ ਬਟਨ ਦਿਖਾਈ ਦੇਣ ਤੇ ਇਸ ਤੇ ਕਲਿੱਕ ਕਰੋ।ਇੱਕ ਡਾਉਨਲੋਡ ਸ਼ੁਰੂ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ (ਇਹੀ ਐਕਸਪੋਰਟ ਫਾਈਲ ਹੈ) -\n6. ਥੱਲੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰੋ ਤੇ ਡਾਊਨਲੋਡ ਕੀਤੀ .zip ਫਾਈਲ ਚੁਣੋ -\n7. [ਜੇ .zip ਤੋਂ ਐਕਸਪੋਰਟ ਫੇਲ ਹੋ ਜਾਂਦੀ ਹੈ] ਤਾਂ .csv ਫਾਈਲ ਐਕਸਟਰੈਕਟ ਕਰੋ (ਆਮ ਤੌਰ ਤੇ \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), ਥੱਲੇ ਦਿੱਤੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰਕੇ ਐਕਸਟਰੈਕਟ ਕੀਤੀ csv ਫਾਈਲ ਚੁਣੋ - URL ਜਾਂ ਆਪਣੀ ID ਟਾਈਪ ਕਰਕੇ ਸਾਉੰਡ ਕਲਾਉਡ ਪ੍ਰੋਫਾਈਲ ਇੰਪੋਰਟ ਕਰੋ: -\n -\n1. ਇੱਕ ਵੈਬ-ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ \"ਡੈਸਕਟਾਪ ਮੋਡ\" ਨੂੰ ਚਾਲੂ ਕਰੋ (ਸਾਈਟ ਮੋਬਾਈਲ ਉਪਕਰਣਾਂ ਲਈ ਉਪਲਬਧ ਨਹੀਂ ਹੈ) -\n2. ਇਸ URL ਤੇ ਜਾਓ: %1$s -\n3. ਆਪਣੇ ਖਾਤੇ ਚ ਲੌਗ-ਇਨ ਕਰੋ -\n4. ਨਿਰਦੇਸ਼ਤ ਕੀਤੇ ਗਏ ਪ੍ਰੋਫਾਈਲ URL ਨੂੰ ਕਾਪੀ ਕਰੋ. + ਗੂਗਲ ਟੇਕਆਊਟ ਤੋਂ ਯੂਟਿਊਬ ਸਬਸਕ੍ਰਿਪਸ਼ਨਾਂ ਇੰਪੋਰਟ ਕਰਨ ਲਈ ਐਕਸਪੋਰਟ ਫਾਈਲ ਡਾਊਨਲੋਡ ਕਰੋ:\n\n1. ਇਸ URL ਤੇ ਜਾਓ: %1$s\n2. ਮੰਗਣ ਤੇ ਆਪਣੇ ਖਾਤੇ \'ਚ ਲਾਗ-ਇਨ ਕਰੋ\n3. ਕਲਿੱਕ ਕਰੋ \" All data incuded\" ਤੇ, ਫੇਰ \"Deselect all\" ਤੇ ਫੇਰ ਸਿਰਫ \"subscriprion\" ਚੁਣੋ ਅਤੇ \"OK\" ਕਰੋ\n4. \"Next step\" ਤੇ ਕਲਿੱਕ ਕਰੋ ਅਤੇ ਫੇਰ \"create export\" ਤੇ\n5. ਡਾਊਨਲੋਡ ਬਟਨ ਦਿਖਾਈ ਦੇਣ ਤੇ ਇਸ ਤੇ ਕਲਿੱਕ ਕਰੋ। ਇੱਕ ਡਾਉਨਲੋਡ ਸ਼ੁਰੂ ਹੋਣੀ ਚਾਹੀਦੀ ਹੈ (ਇਹੀ ਐਕਸਪੋਰਟ ਫਾਈਲ ਹੈ)\n6. ਥੱਲੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰੋ ਤੇ ਡਾਊਨਲੋਡ ਕੀਤੀ .zip ਫਾਈਲ ਚੁਣੋ\n7. [ਜੇ .zip ਤੋਂ ਐਕਸਪੋਰਟ ਫੇਲ ਹੋ ਜਾਂਦੀ ਹੈ] ਤਾਂ .csv ਫਾਈਲ ਐਕਸਟਰੈਕਟ ਕਰੋ (ਆਮ ਤੌਰ ਤੇ \"YouTube and YouTube Music/subscriptions/subscriptions.csv\"), ਥੱਲੇ ਦਿੱਤੇ ਇੰਪੋਰਟ ਫਾਈਲ ਤੇ ਕਲਿੱਕ ਕਰਕੇ ਐਕਸਟਰੈਕਟ ਕੀਤੀ csv ਫਾਈਲ ਚੁਣੋ + URL ਜਾਂ ਆਪਣੀ ID ਟਾਈਪ ਕਰਕੇ ਸਾਉੰਡ ਕਲਾਉਡ ਪ੍ਰੋਫਾਈਲ ਇੰਪੋਰਟ ਕਰੋ: \n \n1. ਇੱਕ ਵੈਬ-ਬ੍ਰਾਊਜ਼ਰ ਵਿੱਚ \"ਡੈਸਕਟਾਪ ਮੋਡ\" ਨੂੰ ਚਾਲੂ ਕਰੋ (ਸਾਈਟ ਮੋਬਾਈਲ ਉਪਕਰਣਾਂ ਲਈ ਉਪਲਬਧ ਨਹੀਂ ਹੈ) \n2. ਇਸ URL ਤੇ ਜਾਓ: %1$s \n3. ਆਪਣੇ ਖਾਤੇ ਚ ਲੌਗ-ਇਨ ਕਰੋ \n4. ਨਿਰਦੇਸ਼ਤ ਕੀਤੇ ਗਏ ਪ੍ਰੋਫਾਈਲ URL ਨੂੰ ਕਾਪੀ ਕਰੋ। ਤੁਹਾਡੀ ਆਈਡੀ, soundcloud.com/ਤੁਹਾਡੀ ਆਈਡੀ - ਯਾਦ ਰੱਖੋ ਕਿ ਇਸ ਕਾਰਜ ਨਾਲ ਡਾਟਾ ਖਪਤ ਹੋ ਸਕਦਾ ਹੈ। -\n -\nਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ\? + ਯਾਦ ਰੱਖੋ ਕਿ ਇਸ ਕਾਰਜ ਨਾਲ ਡਾਟਾ ਖਪਤ ਹੋ ਸਕਦਾ ਹੈ।\n\nਕੀ ਤੁਸੀਂ ਜਾਰੀ ਰੱਖਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਪਲੇਅਬੈਕ ਸਪੀਡ ਕੰਟਰੋਲ ਤਾਲ ਪਿੱਚ ਅਲਹਿਦਾ ਕਰੋ (ਵਿਗਾੜ ਪੈ ਸਕਦਾ ਹੈ) ਕੀ ਤੁਸੀਂ ਸੈਟਿੰਗਾਂ ਨੂੰ ਵੀ ਇੰਪੋਰਟ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ\? ਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ - ਨਿਊਪਾਈਪ ਪ੍ਰੋਜੈਕਟ ਤੁਹਾਡੀ ਗੋਪਨੀਯਤਾ ਨੂੰ ਬਹੁਤ ਗੰਭੀਰਤਾ ਨਾਲ ਲੈਂਦਾ ਹੈ। ਇਸ ਲਈ ਐਪ ਤੁਹਾਡੀ ਸਹਿਮਤੀ ਤੋਂ ਬਿਨਾਂ ਕੋਈ ਵੀ ਡਾਟਾ ਇੱਕਠਾ ਨਹੀਂ ਕਰਦਾ। -\nਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵਿਸਥਾਰ ਵਿੱਚ ਦੱਸਦੀ ਹੈ ਕਿ ਜਦੋਂ ਤੁਸੀਂ ਕਰੈਸ਼ ਰਿਪੋਰਟ ਭੇਜਦੇ ਹੋ ਤਾਂ ਕਿਹੜਾ ਡਾਟਾ ਭੇਜਿਆ ਜਾਂ ਸਟੋਰ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। + ਨਿਊਪਾਈਪ ਪ੍ਰੋਜੈਕਟ ਤੁਹਾਡੀ ਗੋਪਨੀਯਤਾ ਨੂੰ ਬਹੁਤ ਗੰਭੀਰਤਾ ਨਾਲ ਲੈਂਦਾ ਹੈ। ਇਸ ਲਈ ਐਪ ਤੁਹਾਡੀ ਸਹਿਮਤੀ ਤੋਂ ਬਿਨਾਂ ਕੋਈ ਵੀ ਡਾਟਾ ਇੱਕਠਾ ਨਹੀਂ ਕਰਦਾ।\nਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵਿਸਥਾਰ ਵਿੱਚ ਦੱਸਦੀ ਹੈ ਕਿ ਜਦੋਂ ਤੁਸੀਂ ਕਰੈਸ਼ ਰਿਪੋਰਟ ਭੇਜਦੇ ਹੋ ਤਾਂ ਕਿਹੜਾ ਡਾਟਾ ਭੇਜਿਆ ਜਾਂ ਸਟੋਰ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਪੜ੍ਹੋ - ਯੂਰਪੀਅਨ ਜਨਰਲ ਡੇਟਾ ਪ੍ਰੋਟੈਕਸ਼ਨ ਰੈਗੂਲੇਸ਼ਨ (ਜੀਡੀਪੀਆਰ) ਦੀ ਪਾਲਣਾ ਕਰਨ ਲਈ, ਅਸੀਂ ਤੁਹਾਡਾ ਧਿਆਨ ਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵੱਲ ਖਿੱਚਦੇ ਹਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਧਿਆਨ ਨਾਲ ਪੜ੍ਹੋ। -\nਸਾਨੂੰ ਨੁਕਸ ਰਿਪੋਰਟ ਭੇਜਣ ਲਈ ਤੁਹਾਨੂੰ ਇਸ ਨੂੰ ਸਵੀਕਾਰ ਕਰਨਾ ਹੋਵੇਗਾ। + ਯੂਰਪੀਅਨ ਜਨਰਲ ਡੇਟਾ ਪ੍ਰੋਟੈਕਸ਼ਨ ਰੈਗੂਲੇਸ਼ਨ (ਜੀਡੀਪੀਆਰ) ਦੀ ਪਾਲਣਾ ਕਰਨ ਲਈ, ਅਸੀਂ ਤੁਹਾਡਾ ਧਿਆਨ ਨਿਊਪਾਈਪ ਦੀ ਗੋਪਨੀਯਤਾ ਨੀਤੀ ਵੱਲ ਖਿੱਚਦੇ ਹਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਇਸਨੂੰ ਧਿਆਨ ਨਾਲ ਪੜ੍ਹੋ।\nਸਾਨੂੰ ਨੁਕਸ ਰਿਪੋਰਟ ਭੇਜਣ ਲਈ ਤੁਹਾਨੂੰ ਇਸ ਨੂੰ ਸਵੀਕਾਰ ਕਰਨਾ ਹੋਵੇਗਾ। ਸਵੀਕਾਰ ਕਰੋ ਅਸਵੀਕਾਰ ਕੋਈ ਸੀਮਾ ਨਹੀਂ @@ -458,7 +437,6 @@ ਰੇਡੀਓ ਫੀਚਰਡ ਇਹ ਸਮੱਗਰੀ ਸਿਰਫ਼ ਉਹਨਾਂ ਵਰਤੋਂਕਾਰਾਂ ਲਈ ਉਪਲਬਧ ਹੈ ਜਿੰਨ੍ਹਾਂ ਨੇ ਇਸਦੇ ਲਈ ਕੀਮਤ ਦਿੱਤੀ ਹੈ, ਇਸ ਕਰਕੇ ਨਿਊ-ਪਾਈਪ ਦੁਆਰਾ ਚਲਾਈ ਜਾਂ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। - %s ਇਸਦਾ ਕਾਰਨ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ: ਖਾਤਾ ਬੰਦ ਕੀਤਾ ਗਿਆ ਇਹ ਵੀਡੀਓ ਸਿਰਫ਼ ਯੂਟਿਊਬ ਮਿਊਜ਼ਿਕ ਦੇ ਪ੍ਰੀਮੀਅਮ ਮੈਂਬਰਾਂ ਲਈ ਉਪਲਬਧ ਹੈ, ਇਸ ਕਰਕੇ ਨਿਊ-ਪਾਈਪ ਦੁਆਰਾ ਚਲਾਈ ਜਾਂ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। ਇਹ ਸਮੱਗਰੀ ਨਿੱਜੀ (ਪ੍ਰਾਈਵੇਟ) ਹੈ, ਇਸ ਕਰਕੇ ਨਿਊ-ਪਾਈਪ ਦੁਆਰਾ ਚਲਾਈ ਜਾਂ ਡਾਊਨਲੋਡ ਨਹੀਂ ਕੀਤੀ ਜਾ ਸਕਦੀ। @@ -516,8 +494,7 @@ %d ਸਕਿੰਟ ਹਾਂ, ਅਤੇ ਅੱਧ-ਪਚੱਧੀਆਂ ਵੇਖੀਆਂ ਹੋਈਆਂ ਵੀ - ਪਲੇਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ, ਪਹਿਲਾਂ ਚਾਹੇ ਬਾਅਦ ਵਿੱਚ ਵੇਖੇ ਜਾ ਚੁੱਕੇ ਵੀਡੀਓ ਹਟਾ ਦਿੱਤੇ ਜਾਣਗੇ। -\nਕੀ ਵਾਕਿਆ ਹੀ ਤੁਸੀਂ ਇਹਨਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਸ ਕਾਰਵਾਈ ਨੂੰ ਵਾਪਸ ਨਹੀਂ ਮੋੜਿਆ ਜਾ ਸਕਣਾ! + ਪਲੇਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ ਪਹਿਲਾਂ ਤੇ ਬਾਅਦ ਵਿੱਚ ਵੇਖੇ ਜਾ ਚੁੱਕੇ ਵੀਡੀਓ ਹਟਾ ਦਿੱਤੇ ਜਾਣਗੇ। \nਕੀ ਵਾਕਿਆ ਹੀ ਤੁਸੀਂ ਇਹਨਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ? ਇਸ ਕਾਰਵਾਈ ਨੂੰ ਵਾਪਸ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਣਾ! ਵੇਖੇ ਹੋਏ ਵੀਡੀਓ ਹਟਾ ਦੇਈਏ\? ਵੇਖੇ ਹੋਏ ਨੂੰ ਹਟਾਓ ਸਿਸਟਮ ਡਿਫ਼ਾਲਟ @@ -558,7 +535,7 @@ ਕੋਈ ਸਰੋਤਾ ਨਹੀਂ ਸੁਣ ਰਿਹਾ ਕੋਈ ਦਰਸ਼ਕ ਨਹੀਂ ਵੇਖ ਰਿਹਾ ਵੇਰਵਾ - ਸਬੰਧਤ ਨਗ + ਸਬੰਧਤ ਆਈਟਮਾਂ ਟਿੱਪਣੀਆਂ ਗਿਟਹੱਬ \'ਤੇ ਜਾ ਕੇ ਇਤਲਾਹ ਦਿਓ ਦੂਜੀਆਂ ਐਪਾਂ ਦੇ ਉੱਤੇ ਵਿਖਾਉਣ ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ @@ -826,10 +803,32 @@ ਸੈਕੰਡਰੀ ਅਸਥਾਈ ਯੂਟਿਊਬ ਪਲੇਲਿਸਟ ਵਜੋਂ ਸਾਂਝਾ ਕਰੋ ਪਲੇਲਿਸਟਾਂ - %1$s ਦੀ ਖੋਜ ਕਰੋ - %1$s (%2$s) ٪1$s ਦੀ ਖੋਜ ਕਰੋ + %1$s ਖੋਜੋ + %1$s (%2$s) ਖੋਜੋ ਫੀਡ ਗਰੁੱਪ ਚੁਣੋ ਅਜੇ ਤੱਕ ਕੋਈ ਫੀਡ ਗਰੁੱਪ ਨਹੀਂ ਬਣਾਇਆ ਗਿਆ ਚੈਨਲ ਗਰੁੱਪ ਪੰਨਾ ਪਸੰਦ + ਫ਼ਾਈਲ ਮਿਟਾਓ + ਐਂਟਰੀ ਮਿਟਾਓ + ਖ਼ਾਤਾ ਬੰਦ ਕੀਤਾ ਗਿਆ\n\n%1$s ਇਹ ਕਾਰਨ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ: %2$s + ਐਂਟਰੀ ਮਿਟਾ ਦਿੱਤੀ ਗਈ + ਪੌਪਅੱਪ ਪਲੇਅਰ ਦੀ ਵਰਤੋਂ ਕਰਨ ਲਈ, ਕਿਰਪਾ ਕਰਕੇ ਹੇਠਾਂ ਦਿੱਤੇ Android ਸੈਟਿੰਗ ਮੀਨੂ ਵਿੱਚ %1$s ਚੁਣੋ ਅਤੇ %2$s ਨੂੰ ਇਨੇਬਲ ਕਰੋ। + \"ਹੋਰ ਐਪਾਂ ਉੱਤੇ ਡਿਸਪਲੇ ਦੀ ਆਗਿਆ ਦਿਓ\" + %sਹਜ਼ਾਰ + %sਮਿਲੀਅਨ + %sਅਰਬ + SoundCloud ਟੌਪ 50 ਪੰਨਾ ਹਟਾ ਦਿੱਤਾ ਗਿਆ + SoundCloud ਨੇ ਮੂਲ ਟੌਪ 50 ਚਾਰਟਾਂ ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਹੈ। ਸੰਬੰਧਿਤ ਟੈਬ ਨੂੰ ਤੁਹਾਡੇ ਮੁੱਖ ਪੰਨੇ ਤੋਂ ਹਟਾ ਦਿੱਤਾ ਗਿਆ ਹੈ। + YouTube ਸੰਯੁਕਤ ਰੁਝਾਨ ਹਟਾਇਆ ਗਿਆ + YouTube ਨੇ 21 ਜੁਲਾਈ 2025 ਤੋਂ ਸੰਯੁਕਤ \"ਰੁਝਾਨ ਵਿੱਚ\" ਪੰਨੇ ਨੂੰ ਬੰਦ ਕਰ ਦਿੱਤਾ ਹੈ। NewPipe ਨੇ ਡਿਫ਼ਾਲਟ \"ਰੁਝਾਨ ਵਿੱਚ\" ਪੰਨੇ ਨੂੰ ਟ੍ਰੈਂਡਿੰਗ ਲਾਈਵਸਟ੍ਰੀਮਾਂ ਨਾਲ ਬਦਲ ਦਿੱਤਾ ਹੈ।\n\nਤੁਸੀਂ \"ਸੈਟਿੰਗਾਂ > ਸਮੱਗਰੀ > ਮੁੱਖ ਪੰਨੇ ਦੀ ਸਮੱਗਰੀ\" ਵਿੱਚ ਵੱਖ-ਵੱਖ ਟ੍ਰੈਂਡਿੰਗ ਪੰਨਿਆਂ ਨੂੰ ਵੀ ਚੁਣ ਸਕਦੇ ਹੋ। + ਗੇਮਿੰਗ ਟ੍ਰੈਂਡਸ + ਟ੍ਰੈਂਡਿੰਗ ਪੌਡਕਾਸਟ + ਟਰੈਂਡਿੰਗ ਫ਼ਿਲਮਾਂ ਅਤੇ ਸ਼ੋਅ + ਟਰੈਂਡਿੰਗ ਸੰਗੀਤ + ਪਲੇਅ ਕਰਦੇ ਸਮੇਂ ਸਰਵਰ ਤੋਂ HTTP error 403 ਪ੍ਰਾਪਤ ਹੋਇਆ, ਜੋ ਸ਼ਾਇਦ ਸਟ੍ਰੀਮਿੰਗ URL ਦੀ ਮਿਆਦ ਪੁੱਗਣ ਜਾਂ IP ਦੀ ਪਾਬੰਦੀ ਕਾਰਨ ਹੋਈ ਹੈ + ਚਲਾਉਣ ਦੌਰਾਨ ਸਰਵਰ ਤੋਂ HTTP error %1$s ਪ੍ਰਾਪਤ ਹੋਇਆ + ਪਲੇਅ ਕਰਦੇ ਸਮੇਂ ਸਰਵਰ ਤੋਂ HTTP error 403 ਪ੍ਰਾਪਤ ਹੋਇਆ, ਜੋ ਸ਼ਾਇਦ IP ਬੈਨ ਜਾਂ ਸਟ੍ਰੀਮਿੰਗ URL ਡੀਔਬਫਸਕੇਸ਼ਨ ਸਮੱਸਿਆਵਾਂ ਕਾਰਨ ਹੋਈ ਹੈ + %1$s ਨੇ ਡੇਟਾ ਪ੍ਰਦਾਨ ਕਰਨ ਤੋਂ ਇਨਕਾਰ ਕਰ ਦਿੱਤਾ, ਅਤੇ ਇਹ ਪੁਸ਼ਟੀ ਕਰਨ ਲਈ ਲੌਗਇਨ ਕਰਨ ਲਈ ਕਿਹਾ ਕਿ ਬੇਨਤੀਕਰਤਾ ਬੋਟ ਨਹੀਂ ਹੈ।\n\nਹੋ ਸਕਦਾ ਹੈ ਕਿ %1$s ਨੇ ਤੁਹਾਡੇ IP ਨੂੰ ਅਸਥਾਈ ਤੌਰ \'ਤੇ ਪਾਬੰਦੀ ਲਗਾਈ ਹੋਵੇ, ਤੁਸੀਂ ਕੁਝ ਸਮਾਂ ਉਡੀਕ ਕਰ ਸਕਦੇ ਹੋ ਜਾਂ ਕਿਸੇ ਵੱਖਰੇ IP \'ਤੇ ਸਵਿੱਚ ਕਰ ਸਕਦੇ ਹੋ (ਉਦਾਹਰਣ ਵਜੋਂ VPN ਨੂੰ ਚਾਲੂ/ਬੰਦ ਕਰਕੇ, ਜਾਂ WiFi ਤੋਂ ਮੋਬਾਈਲ ਡੇਟਾ \'ਤੇ ਸਵਿੱਚ ਕਰਕੇ)। + ਇਹ ਸਮੱਗਰੀ ਵਰਤਮਾਨ ਵਿੱਚ ਚੁਣੇ ਗਏ ਦੇਸ਼ ਦੀ ਸਮੱਗਰੀ ਲਈ ਉਪਲੱਬਧ ਨਹੀਂ ਹੈ।\n\n\"ਸੈਟਿੰਗਾਂ > ਸਮੱਗਰੀ > ਡਿਫ਼ਾਲਟ ਸਮੱਗਰੀ ਦੇਸ਼\" ਤੋਂ ਆਪਣੀ ਚੋਣ ਬਦਲੋ। diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a0c68f942..3eee59c46 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -96,10 +96,7 @@ Wszystkie Wyłączone Wyczyść - tys. - mln - mld - To pozwolenie jest wymagane, aby + To pozwolenie jest wymagane, aby \notworzyć w trybie okienkowym Otwórz w trybie okienkowym Tryb okienkowy @@ -595,8 +592,8 @@ Zepsuj aplikację Ta treść dostępna jest tylko dla użytkowników, którzy za nią zapłacili. Nie może być strumieniowana ani pobierana przez NewPipe. To wideo dostępne jest tylko dla subskrybentów usługi YouTube Music Premium. Nie może być strumieniowane ani pobierane przez NewPipe. - Ta treść jest prywatna, więc nie może być strumieniowana ani pobierana przez NewPipe - Ta treść nie jest dostępna w Twoim kraju + Ta treść jest prywatna, więc nie może być strumieniowana ani pobierana przez NewPipe. + Ta treść nie jest dostępna w Twoim kraju. To wideo jest objęte ograniczeniem wiekowym. \nZe względu na nowe zasady YouTube dotyczące wideo z ograniczeniami wiekowymi NewPipe nie może uzyskać dostępu do żadnego z jego strumieni wideo i dlatego nie jest w stanie go odtworzyć. To jest utwór SoundCloud Go+ (przynajmniej w Twoim kraju). Nie może być strumieniowany ani pobierany przez NewPipe. @@ -626,8 +623,7 @@ Kategoria Otwórz stronę Teraz możesz zaznaczyć tekst wewnątrz opisu. Pamiętaj, że w trybie zaznaczania strona może migotać i linki nie będą klikalne. - %s podaje ten powód: - Konto zamknięte + Konto zamknięte. Tryb szybki dla ładowania kanału nie dostarcza więcej informacji na ten temat. Konto autora zostało zawieszone. \nNewPipe nie będzie w stanie załadować tego kanału w przyszłości. @@ -857,4 +853,24 @@ Polubienia Usunięto stronę SoundCloud 50 najlepszych SoundCloud wycofał oryginalną listę 50 najlepszych. Odpowiadająca karta została usunięta ze strony głównej. + Usunięto połączone Na czasie YouTube + Od 21 lipca 2025 r. YouTube zaprzestał korzystania z połączonego Na czasie. NewPipe zastąpił domyślną stronę Na czasie popularnymi transmisjami na żywo.\n\nMożesz także wybrać różne strony Na czasie w „Ustawienia > Zawartość > Zawartość strony głównej”. + Gry na czasie + Podcasty na czasie + Filmy i programy na czasie + Muzyka na czasie + %stys. + %smln + %smld + Usuń plik + Usuń wpis + Usunięto wpis + Aby korzystać z odtwarzacza w trybie okienkowym, wybierz %1$s w następującym menu ustawień Androida i włącz %2$s. + „Zezwól na wyświetlanie nad innymi aplikacjami” + Konto zamknięte.\n\n%1$s podaje następujący powód: %2$s + Podczas odtwarzania otrzymano od serwera błąd HTTP 403, prawdopodobnie spowodowany wygaśnięciem adresu URL strumienia lub blokadą adresu IP. + Podczas odtwarzania otrzymano od serwera błąd HTTP %1$s. + Podczas odtwarzania otrzymano od serwera błąd HTTP 403, prawdopodobnie spowodowany blokadą adresu IP lub problemami z odszyfrowaniem adresu URL strumienia. + %1$s odmówił dostarczenia danych, prosząc o zalogowanie się w celu potwierdzenia, że nie jest się botem.\n\nTwoje IP mogło zostać tymczasowo zablokowane przez %1$s. Możesz chwilę poczekać lub zmienić adres IP (na przykład włączając/wyłączając VPN lub przełączając się z sieci Wi-Fi na dane komórkowe). + Ta treść nie jest dostępna dla aktualnie wybranego kraju treści.\n\nZmień swój wybór w „Ustawienia > Zawartość > Domyślny kraj treści”. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 56731cb06..a0ec8127e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -90,9 +90,6 @@ Reproduzindo em modo Popup Todos Desativado - mil - mi - bi Essa permissão é necessária \npara abrir em modo Popup Limpar @@ -622,7 +619,6 @@ Ativar seleção de texto na descrição Agora você pode selecionar o texto dentro da descrição. Note que a página pode piscar e os URL podem não ser clicáveis no modo de seleção. Abrir site - %s fornece este motivo: Conta encerrada O modo feed rápido não fornece mais informações sobre isso. A conta do autor foi encerrada. @@ -843,9 +839,29 @@ Selecione um grupo de feeds Nenhum grupo de feeds criado ainda Página do grupo do canal - Pesquisar %1$s - Pesquisar %1$s (%2$s) + Buscar %1$s + Buscar %1$s (%2$s) Curtidas Página Top 50 do SoundCloud removida O SoundCloud descontinuou as paradas originais do Top 50. A aba correspondente foi removida da sua página principal. + Para usar o Popup Player, selecione %1$s no seguinte menu de configurações do Android e ative %2$s. + “Permitir exibição sobre outros aplicativos” + %sK + %sM + %sB + Excluir arquivo + Excluir entrada + Tendências combinadas do YouTube removidas + O YouTube descontinuou a página de tendências combinadas em 21 de julho de 2025. O NewPipe substituiu a página de tendências padrão pelas transmissões ao vivo em alta.\n\nVocê também pode selecionar páginas de tendências diferentes em \"Configurações > Conteúdo > Conteúdo da página principal\". + Jogos em alta + Podcasts em alta + Filmes e programas em alta + Músicas em alta + Entrada excluída + Conta encerrada\n\n%1$s informa este motivo: %2$s + Erro HTTP 403 recebido do servidor durante a reprodução, provavelmente causado por URL de streaming expirado ou IP banido + Erro HTTP %1$s recebido do servidor durante reprodução + Erro HTTP 403 recebido do servidor durante a reprodução, provavelmente causado por um banimento de IP ou problemas de desofuscação de URL de streaming + %1$s se recusou a fornecer dados, solicitando um login para confirmar que o solicitante não é um bot.\n\nSeu IP pode ter sido temporariamente banido por %1$s. Você pode esperar um pouco ou mudar para um IP diferente (por exemplo, ativando/desativando uma VPN ou alternando de Wi-Fi para dados móveis). + Este conteúdo não está disponível para o país selecionado atualmente.\n\nAltere sua seleção acessando “Configurações > Conteúdo > País padrão do conteúdo”. diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index f670aa52e..1ec01b0d7 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -229,7 +229,6 @@ OK Não foi possível atualizar a subscrição Sim e também os vídeos parcialmente vistos - M Ainda não há listas de reprodução favoritas %s ouvinte @@ -423,8 +422,6 @@ Importado Automático Substitui o seu histórico, subscrições, listas de reprodução e (opcionalmente) definições - k - MM Remover marcador Útil ao trocar para dados móveis, mas algumas transferências não podem ser suspensas Toque longo para colocar na fila @@ -626,7 +623,6 @@ Desativar seleção de texto na descrição Ativar seleção de texto na descrição Agora pode selecionar o texto na descrição. Note que a página pode cintilar e as ligações podem não ser clicáveis enquanto estiver no modo de seleção. - %s fornece este motivo: Conta encerrada O modo de feed rápido não fornece mais informações sobre isto. A conta do autor foi encerrada. @@ -845,4 +841,27 @@ Página do grupo do canal Pesquisar %1$s Pesquisar %1$s (%2$s) + Gostos + Página Top 50 do SoundCloud removida + O SoundCloud descontinuou os gráficos originais do Top 50. A guia correspondente foi removida da sua página principal. + Tendência combinada do YouTube removida + O YouTube descontinuou a página de tendência combinada a partir de 21 de julho de 2025. O NewPipe substituiu a página de tendência predefinida com as streams ao vivo de tendência.\n\nTambém pode escolher páginas de tendência diferentes em \"Definições > Conteúdo > Conteúdo da página principal\". + Tendências de jogos + Tendências de podcasts + Tendências de filmes e shows + Tendências de música + %sK + %sM + %sB + Para usar o reprodutor pop-up, escolhe %1$s no menu seguinte de configurações do Android e ative %2$s. + “Permitir exibição sobre outras apps” + Apagar ficheiro + Apagar entrada + Entrada apagada + Conta terminada\n\n%1$s fornece esta razão: %2$s + Erro HTTP %1$s recebido do servidor ao reproduzir + %1$s recusou fornecer dados, pedindo por um login para confirmar que o solicitante não é um bot.\n\nO seu IP pode ter sido temporariamente banido por %1$s, pode esperar algum tempo ou mudar para um IP diferente (por exemplo, a ligar / desligar uma VPN, ou a alternar de Wi-Fi para dados móveis). + Este conteúdo não está disponível para o país de conteúdo atualmente selecionado.\n\nAltere a sua seleção de \"Configurações > Conteúdo > País predefinido de conteúdo\". + Erro HTTP 403 recebido do servidor durante a reprodução, provavelmente causado pela URL de streaming expirado ou IP banido + Erro HTTP 403 recebido do servidor durante a reprodução, provavelmente causado por um bloqueio de IP ou problemas de desofuscação da URL de streaming diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 72c6b62da..da54067e7 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -82,9 +82,6 @@ Abrir no modo popup Preto Tudo - K - M - MM Esta permissão é necessária \npara o modo popup Desafio reCAPTCHA @@ -608,7 +605,6 @@ Desativar túnel multimédia Sempre que descarregar um ficheiro, terá que indicar o local para o guardar Ainda não definiu uma pasta para as descargas. Escolha agora a pasta a utilizar - %s fornece este motivo: Conta encerrada O modo de fonte rápida não fornece mais informações sobre isto. A conta do autor foi encerrada. @@ -845,4 +841,27 @@ Página do grupo do canal Pesquisar %1$s Pesquisar %1$s (%2$s) + Gostos + Página Top 50 do SoundCloud removida + O SoundCloud descontinuou os gráficos originais do Top 50. A guia correspondente foi removida da sua página principal. + Tendência combinada do YouTube removida + O YouTube descontinuou a página de tendência combinada a partir de 21 de julho de 2025. O NewPipe substituiu a página de tendência predefinida com as streams ao vivo de tendência.\n\nTambém pode escolher páginas de tendência diferentes em \"Definições > Conteúdo > Conteúdo da página principal\". + Tendências de jogos + Tendências de podcasts + Tendências de filmes e shows + Tendências de música + %sK + %sM + %sB + Para usar o reprodutor pop-up, escolhe %1$s no menu seguinte de configurações do Android e ative %2$s. + “Permitir exibição sobre outras apps” + Apagar ficheiro + Apagar entrada + Entrada apagada + Conta terminada\n\n%1$s fornece esta razão: %2$s + Erro HTTP %1$s recebido do servidor ao reproduzir + %1$s recusou fornecer dados, pedindo por um login para confirmar que o solicitante não é um bot.\n\nO seu IP pode ter sido temporariamente banido por %1$s, pode esperar algum tempo ou mudar para um IP diferente (por exemplo, a ligar / desligar uma VPN, ou a alternar de Wi-Fi para dados móveis). + Este conteúdo não está disponível para o país de conteúdo atualmente selecionado.\n\nAltere a sua seleção de \"Configurações > Conteúdo > País predefinido de conteúdo\". + Erro HTTP 403 recebido do servidor durante a reprodução, provavelmente causado pela URL de streaming expirado ou IP banido + Erro HTTP 403 recebido do servidor durante a reprodução, provavelmente causado por um bloqueio de IP ou problemas de desofuscação da URL de streaming diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index cda82bd25..1b7210e58 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -92,9 +92,6 @@ Dezactivat Aplicația/UI s-a oprit Ce:\\nSolicitare:\\nLimba conținutului:\\nȚara conținutului:\\nLimba aplicației:\\nServiciu:\\nOra GMT:\\nPachet:\\nVersiune:\\nVersiune SO: - k - mil. - mld. Elimină sunetul audio la anumite rezoluții Fundal Pop-up @@ -627,7 +624,6 @@ Dezactivați selectarea textului în descriere Activați selectarea textului în descriere Acum puteți selecta text în interiorul descrierii. Rețineți că este posibil ca pagina să pâlpâie, iar linkurile să nu poată fi accesate în modul de selecție. - %s oferă acest motiv: Contul a fost închis Modul rapid nu furnizează mai multe informații în acest sens. Contul autorului a fost închis. @@ -848,4 +844,8 @@ Aprecieri Pagina SoundCloud Top 50 a fost eliminată SoundCloud a eliminat Top 50. Fila corespunzătoare a fost eliminată din pagina principală. + „Permite afișarea deasupra altor aplicații” + %s mii + %s mil. + %s mld. diff --git a/app/src/main/res/values-rom/strings.xml b/app/src/main/res/values-rom/strings.xml new file mode 100644 index 000000000..55344e519 --- /dev/null +++ b/app/src/main/res/values-rom/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8b54adaad..e3ca674a2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -94,9 +94,6 @@ Выберите отображаемые предложения поиска Отключено Убирает звук в некоторых разрешениях - млн - млрд - тыс. Разрешение всплывающего окна Помнить последние размер и позицию всплывающего окна Предложения поиска @@ -636,7 +633,6 @@ Не удалось загрузить подписку \'%s\'. Ошибка загрузки подписки Открыть веб-сайт - %s указывает следующую причину: Аккаунт отключён Начиная с Android 10 поддерживается только «Storage Access Framework» Спрашивать, куда сохранять каждую загрузку @@ -853,4 +849,24 @@ Лайки Страница SoundCloud Top 50 удалена SoundCloud прекратил поддерживать оригинальные чарты Top 50. Соответствующая вкладка была удалена с вашей главной страницы. + %sК + %sмлн + %sмлрд + Игровые тренды + Удалены объединённые тренды YouTube + YouTube прекратил поддержку объединённой страницы трендов 21 июля 2025 года. NewPipe заменил страницу трендов по умолчанию на тренды в прямых трансляциях.\n\nВы также можете выбрать другие страницы трендов в \"Настройки > Контент > Главная страница\". + Тренды в подкастах + Тренды в фильмах и шоу + Тренды в музыке + Чтобы использовать Popup Player, выберите %1$s в следующем меню настроек Android и включите %2$s. + «Разрешить отображение поверх других приложений» + Удалить файл + Удалить запись + Учётная запись закрыта\n\n%1$s указал причину: %2$s + Запись удалена + Во время воспроизведения получена ошибка HTTP 403 от сервера, вероятно, вызванная истечением срока действия URL-адреса потоковой передачи или блокировкой IP-адреса + Ошибка HTTP %1$s получена от сервера во время воспроизведения + Во время воспроизведения получена ошибка HTTP 403 от сервера, вероятно, вызванная блокировкой IP-адреса или проблемами деобфускации URL-адреса потоковой передачи + %1$s отказался предоставить данные, запросив логин для подтверждения, что запросчик не бот.\n\nВозможно, ваш IP-адрес временно заблокирован %1$s. Вы можете подождать некоторое время или переключиться на другой IP-адрес (например, включив/выключив VPN или переключившись с Wi-Fi на мобильный интернет). + Этот контент недоступен для выбранной страны контента.\n\nИзмените свой выбор в разделе «Настройки > Контент > Страна контента по умолчанию». diff --git a/app/src/main/res/values-ryu/strings.xml b/app/src/main/res/values-ryu/strings.xml index 1bc89420c..e9baa11da 100644 --- a/app/src/main/res/values-ryu/strings.xml +++ b/app/src/main/res/values-ryu/strings.xml @@ -83,9 +83,6 @@ reCAPTCHAようきゅうさびたん ブラック まじり - k - M - B ポップアップモードっしふぃらちゅん ポップアップモードっしふぃらちゅんがー \nきんぎんぬきょかがふぃちようでぃす @@ -625,7 +622,6 @@ オフ オン タブレットモード - %sやしがくぬりゆうていじ: ひょうじさん ていふぃんしち(しょう) かんふぃんしち(だい) diff --git a/app/src/main/res/values-sat/strings.xml b/app/src/main/res/values-sat/strings.xml index a3ed7c405..717283e83 100644 --- a/app/src/main/res/values-sat/strings.xml +++ b/app/src/main/res/values-sat/strings.xml @@ -216,8 +216,6 @@ ᱡᱟᱦᱟᱱ ᱡᱤᱱᱤᱥ ᱱᱚᱣᱟ ᱨᱮᱫᱚ ᱡᱟᱹᱥᱛᱤ ᱡᱟᱹᱥᱛᱤ ᱠᱨᱤᱠᱮᱴ ᱢᱮᱱᱟᱜᱼᱟ ᱾ ᱵᱷᱤᱰᱤᱭᱳ - k - M ᱥᱮᱞᱮᱫᱤᱭᱟᱹ ᱠᱚᱣᱟᱜ ᱞᱮᱠᱷᱟ ᱵᱟᱭ ᱦᱟᱹᱴᱤᱧ ᱟᱠᱟᱱᱟ ᱵᱟᱱᱩᱜ ᱧᱮᱞ ᱵᱷᱤᱰᱤᱭᱳ ᱵᱟᱹᱱᱩᱜᱼᱟ @@ -333,7 +331,6 @@ ᱪᱮᱯᱴᱟᱨᱥ ᱞᱟᱹᱠᱛᱤ ᱠᱟᱱᱟ ᱟᱢ ᱢᱤᱫ ᱯᱷᱤᱞ ᱢᱟᱱᱮᱡᱚᱨ ᱤᱱᱥᱴᱚᱞ ᱢᱮ ᱟᱨᱵᱟᱝ ᱰᱟᱩᱱᱞᱚᱰ ᱥᱤᱴᱤᱝ ᱨᱮ ᱵᱚᱫᱚᱞ ᱦᱚᱪᱚ ᱞᱟᱹᱜᱤᱫ ᱯᱨᱚᱵᱷᱟᱣ ᱢᱮ\" ᱱᱚᱶᱟ ᱵᱷᱤᱰᱤᱭᱳ ᱫᱚ ᱭᱩᱴᱭᱩᱵᱽ ᱢᱤᱣᱡᱤᱠ ᱯᱨᱤᱢᱤᱭᱟᱢ ᱥᱮᱞᱮᱫᱤᱭᱟᱹ ᱠᱚ ᱞᱟᱹᱜᱤᱫ ᱜᱮ ᱧᱟᱢᱚᱜᱼᱟ, ᱚᱱᱟᱛᱮ ᱱᱚᱶᱟ ᱫᱚ ᱱᱤᱭᱩ ᱯᱟᱭᱤᱯ ᱦᱚᱛᱮᱛᱮ ᱵᱟᱝ ᱥᱴᱨᱤᱢ ᱟᱨ ᱵᱟᱝ ᱰᱟᱩᱱᱞᱳᱰ ᱦᱩᱭ ᱫᱟᱲᱮᱭᱟᱜᱼᱟ ᱾ - %s ᱫᱚ ᱱᱚᱶᱟ ᱞᱟᱹᱠᱛᱤ ᱠᱟᱱᱟ: ᱚᱴᱚᱢᱟᱴᱤᱠ (ᱰᱤᱵᱟᱤᱥ ᱛᱷᱮᱢ) ᱟᱢᱟᱜ ᱯᱩᱭᱞᱩ ᱧᱤᱫᱟᱹ ᱛᱷᱤᱢ ᱵᱟᱪᱷᱟᱣ ᱢᱮ ⁇ %s ᱟᱢ ᱞᱟᱛᱟᱨ ᱨᱮ ᱟᱢᱟᱜ ᱧᱤᱫᱟᱹ ᱪᱮᱛᱟᱱ ᱵᱟᱪᱷᱟᱣ ᱫᱟᱲᱮᱭᱟᱜ ᱟ @@ -478,7 +475,6 @@ ᱱᱟᱣᱟ ᱟᱹᱨᱡᱤ ᱞᱟᱹᱜᱤᱫ ᱟᱹᱪᱩᱨ ᱢᱮ ᱚᱰᱤᱭᱳ ᱟᱨᱦᱚᱸ ᱯᱟᱲᱦᱟᱣ ᱢᱮ - ᱵᱤ ᱱᱤᱛᱚᱜ ᱵᱟᱪᱷᱟᱣ ᱟᱠᱟᱱ ᱴᱳᱜᱞ ᱥᱮᱵᱟ: ᱚᱵᱷᱤᱱᱮᱛᱟᱨ ᱵᱟᱹᱱᱩᱜᱼᱟ ᱚᱠᱚᱭ ᱦᱚᱸ ᱵᱟᱝ ᱧᱮᱞᱚᱜ ᱠᱟᱱᱟ diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 0c42f1ef0..091001ae6 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -165,9 +165,6 @@ Perunu iscritu Allughe/istuda su servìtziu. Ischertadu como: - Mrd - Mlln - mìg Torra a proare Àudio Vìdeu @@ -613,7 +610,6 @@ Como podes ischertare su testu in intro de sa descritzione. Ammenta·ti chi sa pàgina diat pòdere trèmere e sos ligàmenes si diant pòdere no abèrrere cando ses in modalidade de ischerta. Incumintzende dae Android 10 petzi sa \'Storage Access Framework\' (Istrutura de Atzessu a s\'Archiviatzione) est suportada Aberi su situ web - %s frunit custa resone: Contu serradu Su recùperu lestru de sos flussos non frunit àteras informatziones in subra de custu. Su contu de s\'autore l\'ant serradu. @@ -826,4 +822,18 @@ Iscalitas Cumpartzi comente un\'iscalita temporànea de YouTube segundàriu + Chirca in %1$s + Chirca %1$s (%2$s) + Seletziona unu grupu de flussos + Galu perunu grupu de flussos creadu + Pàgina de grupu de canales + Agradessimentos + Pàgina Top 50 de SoundCloud bogada + SoundCloud at abbandonadu sos gràficos Top 50 originales. S\'ischeda currispondente est istada bogada dae sa pàgina printzipale tua. + Tendèntzias cumbinadas de YouTube bogadas + YouTube at abbandonadu sa pàgina de sas tendèntzias cumbinadas in su 21 de trìulas de su 2025. NewPipe at sostituidu sa pàgina de sas tendèntzias printzipales cun sas diretas de tendèntzia.\n\nPodes fintzas seletzionare pàginas de sas tendèntzias diferentes in \"Impostatziones > Cuntenutos > Cuntenutu de sa pàgina printzipale\". + Giogos de tendèntzia + Podcasts de tendèntzia + Films e programmas de tendèntzia + Mùsica de tendèntzia diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 3bae642c7..601623cb1 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -45,7 +45,7 @@ Náhľad avataru uploadera Lajky Dislajky - Začnite klepnutím na lupu. + Začnite ťuknutím na lupu. Obsah Zobraziť vekovo obmedzený obsah Naživo @@ -74,7 +74,7 @@ Čakajte prosím… Skopírované do schránky Priečinok na sťahovanie zadefinujte prosím neskôr v nastaveniach - Sťahované súbory + Stiahnuté súbory Stiahnuté Hlásenie o chybe Aplikácia/UP zlyhalo @@ -82,9 +82,6 @@ Výzva reCAPTCHA Čierna Všetko - k - M - B Požiadavka reCAPTCHA Otvoriť vo vyskakovacom okne Tieto práva sú potrebné pre @@ -211,7 +208,7 @@ Neplatný ZIP súbor Upozornenie: Nemožno importovať všetky súbory. Toto prepíše vaše aktuálne nastavenie. - Trendy + Populárne Top 50 Nové a horúce Odstrániť @@ -476,7 +473,7 @@ %d dní Skupiny kanálov - Zdroj naposledy aktualizovaný: %s + Zdroj aktualizovaný: %s Nenačítané: %d Načítavanie zdroja… Spracovávanie zdroja… @@ -489,7 +486,7 @@ Bez názvu skupiny Chcete zmazať vybranú skupinu\? - Nová + Nový Zdroj Interval obnovy zdroja Čas od poslednej aktualizácie, kedy sa odber považuje za neaktuálny - %s @@ -621,7 +618,6 @@ Povolenie výberu textu v popise Teraz môžete vybrať text vo vnútri popisu. Upozorňujeme, že stránka môže blikať a odkazy nemusia byť klikateľné, keď je v režime výberu. Otvoriť webstránku - %s uvádza tento dôvod: Účet bol zrušený Tento rýchly režim neposkytuje viac informácií. Účet autora bol zrušený. @@ -728,7 +724,7 @@ Používate najnovšiu verziu NewPipe Táto možnosť je dostupná len pre motív %s Rýchly režim - Import alebo export odberov z 3-bodkovej ponuky + Importujte alebo exportujte odbery v menu s 3-mi bodkami Akcia gesta vľavo Vyberte gesto pre pravú polovicu obrazovky prehrávača Akcia gesta vpravo @@ -847,4 +843,24 @@ Páči sa SoundCloud Top 50 stránka odstránená SoundCloud prestal používať pôvodnú Top 50. Daná stránka bola odstránená z hlavnej stránky. + Odstránené kombinované trendy na YouTube + YouTube ukončil prevádzku kombinovanej stránky s trendmi k 21. júlu 2025. NewPipe nahradil predvolenú stránku s trendmi stránkou s trendovými živými prenosmi.\n\nV nastaveniach „Nastavenia > Obsah > Obsah hlavnej stránky“ môžete vybrať aj iné stránky s trendmi. + Populárne hry + Populárne podcasty + Populárne filmy a seriály + Populárna hudba + %stis. + %smil. + %smld. + Položka vymazaná + Vymazať položku + \"Povoliť zobrazenie cez iné aplikácie\" + Vymazať súbor + Ak chcete používať Popup Player, vyberte %1$s v nasledujúcej ponuke nastavení Androidu a povoľte %2$s. + Účet bol zrušený\n\n%1$s uvádza tento dôvod: %2$s + Počas prehrávania bola zo servera prijatá chyba HTTP 403, pravdepodobne spôsobená vypršaním platnosti streamingovej adresy URL alebo zákazom IP adresy + Chyba HTTP %1$s prijatá zo servera počas prehrávania + Chyba HTTP 403 prijatá zo servera počas prehrávania, pravdepodobne spôsobená zákazom IP adresy alebo problémami s deobfuskáciou streamingovej URL adresy + %1$s odmietol poskytnúť údaje, žiada o prihlásenie na potvrdenie, že žiadateľ nie je bot.\n\nVaša IP adresa mohla byť dočasne zakázaná %1$s, môžete nejaký čas počkať alebo prejsť na inú IP adresu (napríklad zapnutím/vypnutím VPN alebo prepnutím z WiFi na mobilné dáta). + Tento obsah nie je dostupný pre aktuálne zvolenú krajinu obsahu.\n\nZmeňte výber v ponuke \"Nastavenia > Obsah > Predvolená krajina obsahu\". diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 68875fe23..8b4ba8f7b 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -83,9 +83,6 @@ Predmet:\\nZahteva:\\nJezik vsebine:\\nDržava vsebine:\\nJezik aplikacije:\\nStoritev:\\nČas v GMT:\\nPaket:\\nRazličica:\\nRazličica OS: Črna Vse - k - mio - mrd Odpri v pojavnem načinu To dovoljenje je potrebno za odpiranje \nv pojavnem načinu @@ -264,7 +261,7 @@ NewPipe-ovi pravilnik zasebnosti Obiščite spletno mesto od NewPipe za več informacij in novic. Končano - Ni komantarjev + Ni komentarjev Nobeden ne posluša Nobeden ne gleda Zgodila se je napaka: %1$s diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml index 6b4de7353..860a607f5 100644 --- a/app/src/main/res/values-so/strings.xml +++ b/app/src/main/res/values-so/strings.xml @@ -242,9 +242,6 @@ Dad rukuntay ma jiraan Furo adeega, hada waxaa dooran: - B - K - M ku celi Dhagaysi Muuqaal @@ -611,7 +608,6 @@ Xidh caalamadinta qoraalka Fur caalamadinta qoraalka Hadda waad dooran kartaa qoraalka ku dhexjira faahfaahinta. Ogow markaad caalamdinayso qoraalka boggu wuu boodboodi karaa tixraacyadana waxay noqon karaan kuwo aan lagu dhufan karin. - %s wuxuu sheegayaa sababtan: Akoonka waa lajoojiyay Nidaamka dagdaga ah faahfaahin dheeraad ah uma hayo shaygan. Akoonka soosaaraha waa la joojiyay. diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 162ba5ada..4b9c2ac36 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -295,9 +295,6 @@ Nuk ka abonues Aktivizoje shërbimin, momentalisht e zgjedhur: - B - M - k Riprovo Audio Video @@ -601,7 +598,6 @@ Zgjidhni temën tuaj të preferuar të natës - %s Automatike (tema e pajisjes) Radio - %s e jep këtë arsye: Llogaria është mbyllur Kjo përmbajtje është private, kështu që nuk mund të luhet apo shkarkohet nga NewPipe. Kjo përmbajtje nuk është e disponueshme në shtetin tuaj. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 99b9e971f..02711dac2 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -78,14 +78,11 @@ Преузимања Извештај о грешци Програм је отказао - Шта:\\nЗахтев:\\nЈезик садржаја:\\nЗемља садржаја:\\nЈезик апликације:\\nУслуга:\\nGMT време:\\nПакет:\\nВерзија:\\nВерзија ОС-а: + Шта:\\nЗахтев:\\nЈезик садржаја:\\nДржава садржаја:\\nЈезик апликације:\\nУслуга:\\nGMT време:\\nПакет:\\nВерзија:\\nВерзија ОС-а: „reCAPTCHA“ задатак Решите „reCAPTCHA“ задатак Црна Све - хиљ. - мил. - млрд. Отвори у искачућем облику Ова дозвола је потребна за \nотварање у искачућем режиму @@ -117,7 +114,7 @@ О NewPipe О нама и ЧПП Лиценце - Слободно и лагано стримовање на Android-у. + Слободно и лагано токовање на Android-у. Погледај на GitHub-у Прочитај лиценцу Допринос @@ -164,7 +161,7 @@ Без обзира имате ли идеје; превод, промене дизајна, чишћење кода или праве, озбиљне, промене кода—помоћ је увек добродошла. Што се више уради, то је боље! Прикажите савет када притиснете позадину или искачуће дугме у видео снимку „Детаљи:“ Пусти све - Није могуће пустити овај стрим + Није могуће пустити овај ток Дошло је до непоправљиве грешке плејера Опорављање од грешке плејера Желите ли да избришете ову ставку из историје претраге\? @@ -182,11 +179,11 @@ Детаљи Подешавања аудио снимка Није пронађен ниједан извођач довода (можете уградити ВЛЦ ради извођења садржаја). - Преузимање фајла стрима + Преузимање фајла тока Прикажи информације Обележене плејлисте Додај на - Подразумевана држава за садржај + Подразумевана држава садржаја Исправљање грешака Увек Само једном @@ -322,7 +319,7 @@ Увоз из Увоз Срушите апликацију - Изворни текстови са услуга биће видљиви у ставкама стрима + Изворни текстови са услуга биће видљиви у ставкама тока Прикажи изворно време ставки Присилно извештавање о „Rx“ изузецима који се не могу испоручити ван фрагмента или животног циклуса активности након одлагања Пријави грешке ван животног циклуса @@ -421,15 +418,15 @@ Врати подразумеване вредности Желите ли да вратите подразумеване вредности\? Није могуће прочитати сачуване картице, тако да се користе подразумеване - Нема стримова доступних за преузимање + Нема токова доступних за преузимање Дошло је до грешке: %1$s Фајл не постоји или нема дозволе за читање или писање Назив фајла не може бити празан Нема таквог фајла/извора садржаја Нема таквог фолдера Фајл је премештен или избрисан - Нису пронађени видео стримови - Нису пронађени аудио стримови + Нису пронађени видео токови + Нису пронађени аудио токови Спољни плејери не подржавају ове врсте линкова Преузимање на спољну, SD (меморијску), картицу није могуће. Ресетовати локацију фолдера за преузимање\? Спољна меморија није доступна @@ -443,7 +440,7 @@ Избриши позиције репродукције Историја гледања је избрисана Избрисати целу историју гледања\? - Брише историју пуштаних стримова и позиције репродукције + Брише историју пуштаних токова и позиције репродукције Очисти историју гледања Чисти колачиће које NewPipe чува када решите „reCAPTCHA“ Извоз историје, праћења, плејлисти и подешавања @@ -495,10 +492,10 @@ Вратите последњу позицију репродукције Настави репродукцију Аутоматско стављање у редослед - Наставите да завршавате (непонављајући) редослед репродукције додавањем сродног стрима - Аутоматски стави у редослед следећи стрим + Наставите да завршавате (непонављајући) редослед репродукције додавањем сродног тока + Аутоматски стави у редослед следећи ток Кеш метаподатака обрисан - Искључите да бисте сакрили поља за метаподатке са додатним информацијама о креатору стрима, садржају стрима или захтеву за претрагу + Искључите да бисте сакрили поља за метаподатке са додатним информацијама о креатору тока, садржају тока или захтеву за претрагу Прикажи метаподатке Искључите да бисте сакрили опис видео снимка и додатне информације Прикажи опис @@ -530,10 +527,10 @@ Аутоматски (тема уређаја) Радио Истакнуто - Овај садржај је доступан само корисницима који су платили, тако да га NewPipe не може стримовати или преузимати. - Овај видео снимак је доступан само премијум YouTube Music члановима, тако да га NewPipe не може стримовати или преузимати. - Овај садржај је приватан, тако да га NewPipe не може стримовати или преузимати. - Ово је SoundCloud Go+ нумера, барем у вашој земљи, тако да је NewPipe не може стримовати или преузимати. + Овај садржај је доступан само корисницима који су платили, тако да га NewPipe не може токовати или преузимати. + Овај видео снимак је доступан само премијум YouTube Music члановима, тако да га NewPipe не може токовати или преузимати. + Овај садржај је приватан, тако да га NewPipe не може токовати или преузимати. + Ово је SoundCloud Go+ нумера, барем у вашој земљи, тако да је NewPipe не може токовати или преузимати. Овај садржај није доступан у вашој земљи. Ниједна апликација на вашем уређају не може да отвори ово Поглавља @@ -622,7 +619,6 @@ Омогући бирање текста унутар описа Онемогући бирање текста унутар описа Сада можете изабрати текст унутар описа. Имајте на уму да страница може треперети и да се на линкове можда неће моћи кликнути док сте у режиму избора. - %s даје овај разлог: Налог укинут Режим брзог фида не пружа више информација о овоме. Налог аутора је укинут. @@ -653,7 +649,7 @@ %s преузимања је завршено %1$s %2$s - Обавештења о новим стримовима + Обавештења о новим токовима Обавештења Погледај на веб-сајту Ако имате проблема са коришћењем апликације, обавезно погледајте ове одговоре на честа питања! @@ -666,7 +662,7 @@ Поништи трајну сличицу Било која мрежа Ручно проверите постоје ли нове верзије - Спољни плејери не подржавају изабрани стрим + Спољни плејери не подржавају изабрани ток Померите главни бирач картице на дно Положај главних картица Дошло је до грешке, погледајте обавештење @@ -675,34 +671,34 @@ Учесталост провере Картица Уклонити дупликате\? - Желите ли да уклоните све дупликате стримова на овој плејлисти\? + Желите ли да уклоните све дупликате токова на овој плејлисти? Предстојеће Подешавања ExoPlayer-а Увек користи заобилазно решење ExoPlayer-а за подешавање површине излаза видео снимка Изабери квалитет за спољне плејере - Нема стримова - Нема стримова уживо + Нема токова + Нема токова уживо - %s нови стрим - %s нова стрима - %s нових стримова + %s нови ток + %s нова тока + %s нових токова - Учитавање детаља стрима… + Учитавање детаља тока… Прикажи „Сруши плејер“ - Покрени проверу нових стримова + Покрени проверу нових токова Тунеловање медија је подразумевано онемогућено на вашем уређају, јер је познато да ваш уређај то не подржава. Полутон - Обавештавање о новим стримовима из праћења + Обавештавање о новим токовима из праћења Провера ажурирања… Избрисати све преузете фајлове са диска\? Уклони дупликате Прикачен коментар ExoPlayer подразумевано Изабери све - Не приказују се стримови које програм за преузимање још увек не подржава - Аудио снимак би већ требало да буде присутан у овом стриму - Нема доступних аудио стримова за спољне плејере - Нема доступних видео стримова за спољне плејере + Не приказују се токови које програм за преузимање још увек не подржава + Аудио снимак би већ требало да буде присутан у овом току + Нема доступних аудио токова за спољне плејере + Нема доступних видео токова за спољне плејере Непознати формат Непознати квалитет Непознато @@ -716,8 +712,8 @@ Додирните да бисте преузели %s Провера ажурирања Преглед сличице траке за претрагу - Приказ следећих стримова - Прикажи/сакриј стримове + Приказ следећих токова + Прикажи/сакриј токове Користи резервну функцију декодера ExoPlayer-а оригинални Промените величину интервала учитавања на прогресивним садржајима (тренутно %s). Нижа вредност може убрзати њихово почетно учитавање @@ -726,8 +722,8 @@ Радња покретом улево Изаберите покрет за десну половину екрана плејера Обавештење плејера - Обавештења о новим стримовима за праћења - Конфигуришите обавештење о тренутно репродукованом стриму + Обавештења о новим токовима за праћења + Конфигуришите обавештење о тренутно репродукованом току Изаберите оригинални аудио снимак, без обзира на језик Изаберите аудио снимак са описима за особе са оштећеним видом, ако је доступан Преферирај описни аудио снимак @@ -735,7 +731,7 @@ Осветљеност Јачина звука Ниједно - Нови стримови + Нови токови Обавештења за пријаву грешака Увезите или извезите праћења из менија са 3 тачке Аудио снимак @@ -794,7 +790,7 @@ Низак квалитет Укључи цео екран Аватари - Следећи стрим + Следећи ток Аватари потканала Отвори редослед пуштања Не учитавај слике @@ -810,7 +806,7 @@ Више опција Сличице Трајање - Претходни стрим + Претходни ток Дели листу URL адреса Дели са насловима %1$s @@ -844,4 +840,28 @@ Изаберите групу фидова Страница групе канала Ликовања + Претрага %1$s + Претрага %1$s (%2$s) + Да бисте користили искачући плејер, изаберите %1$s у следећем менију подешавања Android-а и омогућите %2$s. + „Дозволи приказ преко других апликација“ + %sк + %sмлн. + %sмлрд. + Избриши фајл + Избриши унос + Налог је укинут\n\n%1$s наводи овај разлог: %2$s + Страница SoundCloud Top 50 је уклоњена + SoundCloud је укинуо оригиналне топ 50 листе. Одговарајућа картица је уклоњена са ваше главне странице. + Уклоњен је комбиновану страницу у тренду на YouTube-у + YouTube је укинуо комбиновану страницу у тренду од 21. јула 2025. NewPipe је заменио подразумевану страницу у тренду са страницом уживо у тренду.\n\nТакође можете одабрати различите странице у тренду у „Подешавања > Садржај > Садржај главне странице“. + Игре у тренду + Подкасти у тренду + Филмови и серије у тренду + Музика у тренду + Унос избрисан + HTTP грешка 403 примљена са сервера током репродукције, вероватно узрокована истеком URL-а за токовање или забраном IP адресе + HTTP грешка %1$s примљена са сервера током репродукције + HTTP грешка 403 примљена са сервера током репродукције, вероватно узрокована забраном IP адресе или проблемима са деобфускацијом URL-а за токовање + %1$s је одбио да пружи податке, тражећи пријаву како би се потврдило да подносилац захтева није бот.\n\nВаша IP адреса је можда привремено забрањена од стране %1$s, можете сачекати неко време или прећи на другу IP адресу (на пример, укључивањем/искључивањем VPN-а или преласком са WiFi-ја на мобилне податке). + Овај садржај није доступан за тренутно изабрану земљу садржаја.\n\nПромените свој избор у „Подешавања > Садржај > Подразумевана држава садржаја“. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 3beab4181..7893eae19 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -112,9 +112,6 @@ Video Ljud Försök igen - t - mn - md Inga prenumeranter %s prenumerant @@ -147,7 +144,7 @@ \nöppna i popup-läge reCAPTCHA utmaning reCAPTCHA utmaning begärd - Hämta + Hämtning Tillåtna tecken i filnamn Ogiltiga tecken ersätts med detta värde Ersättningstecken @@ -169,8 +166,8 @@ Vill du ta bort det här objektet från sökhistoriken? Huvudsidans innehåll Tom sida - Kiosk sida - Kanal-sida + Kiosksida + Kanalsida Välj en kanal Inga kanal prenumerationer ännu Välj en kiosk @@ -589,7 +586,6 @@ Hämtningen har startat Radio Detta innehåll är endast tillgängligt för användare som har betalat för det, så det kan inte strömmas eller hämtas av NewPipe. - %s anger detta skäl: Kontot avslutat Denna video är endast tillgänglig för YouTube Music Premium-medlemmar, så den kan inte strömmas eller hämtas av NewPipe. Detta innehåll är privat, så det kan inte strömmas eller hämtas av NewPipe. @@ -829,4 +825,29 @@ Välj en flödesgrupp Ingen flödesgrupp har skapats ännu Kanalgruppsida + Sök %1$s + Sök %1$s (%2$s) + %sK + %sM + %sB + Gillar + SoundCloud Topp 50-sida borttagen + SoundCloud har lagt ner de ursprungliga topp 50-listorna. Motsvarande flik har tagits bort från din startsida. + YouTubes kombinerade trender har tagits bort + YouTube har upphört med den kombinerade trendsidan från och med den 21 juli 2025. NewPipe ersatte standardtrendsidan med trendiga livestreams.\n\nDu kan också välja olika trendsidor i \"Inställningar > Innehåll > Innehåll på huvudsidan\". + Speltrender + Trendiga poddar + Trendiga filmer och serier + Trendig musik + För att använda Popup Player, välj %1$s i följande Android-inställningsmeny och aktivera %2$s. + \"Tillåt visning över andra appar\" + Ta bort fil + Ta bort post + Konto avslutat\n\n%1$s anger denna anledning: %2$s + Posten borttagen + HTTP-fel 403 mottogs från servern under uppspelning, troligen orsakat av att streaming-URL:en har löpt ut eller att en IP-adress har blockerats + HTTP-fel %1$s mottogs från servern under spelning + HTTP-fel 403 mottogs från servern under spelning, troligen orsakat av en IP-avstängning eller problem med deobfuskering av streaming-URL:er + %1$s vägrade att tillhandahålla data och bad om en inloggning för att bekräfta att den som begärde detta inte är en bot.\n\nDin IP-adress kan ha blivit tillfälligt avstängd av %1$s. Du kan vänta en stund eller byta till en annan IP-adress (till exempel genom att slå på/av ett VPN eller genom att byta från WiFi till mobildata). + Detta innehåll är inte tillgängligt för det valda innehållslandet.\n\nÄndra ditt val från \"Inställningar > Innehåll > Standardinnehållsland\". diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 906249376..f1d388393 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -245,7 +245,6 @@ உம் அபிமான பியர்டியூப் நிகழ்வுகளைத் தேர்ந்தெடு உள்ளடக்க இயல்பிருப்பு மொழி இயக்குதலைத் மறுதொடர் - நி நிகழ்வு ஏற்கனவே உள்ளது யூடியூபின் \"கட்டுப்பாடு பயன்முறை\"ஐ இயக்கு பாடல்கள் @@ -260,8 +259,6 @@ என்ன:\\nகோரிக்கை:\\nஉள்ளடக்க மொழி:\\nஉள்ளடக்க நாடு:\\nசெயலி மொழி:\\nசேவை:\\nGMT நேரம்:\\nசிப்பம்:\\nபதிப்பு:\\nOS பதிப்பு: காணொளியை இயக்கு, காலவளவு: கருத்தளிப்புகள் - - ப.ல இயக்கியைச் சிதை பட்டியல்களில் இயக்கக குறியட நிலைகாட்டிகளைக் காட்டு துணையியக்கியில் காணொளிகளை துவக்காதே, ஆனால் தானாக சுழற்றல் பூட்டப்பட்டிருந்தால் நேரடியாக முழுதிரைக்குத் திரும்பு. முழுதிரையை வெளியேறி நீங்கள் இன்னும் துணையியக்கியை அணுகலாம் @@ -431,7 +428,6 @@ பதிவிறக்க வரிசையை கட்டுப்படுத்துங்கள் ஒரு பதிவிறக்கம் ஒரே நேரத்தில் இயங்கும் உங்கள் சாதனத்தில் எந்த பயன்பாடும் இதைத் திறக்க முடியாது - %s இந்த காரணத்தை வழங்குகிறது: துணை சேனல் அவதாரங்கள் அவதாரங்கள் எக்சோப்ளேயர் இயல்புநிலை diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index c537495ba..b95e04c0c 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -77,9 +77,6 @@ వీడియో ఆడియో మళ్ళీ ప్రయత్నించు - కి - ఎం - బిలియన్ సభ్యులు లేరు %s సభ్యుడు diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index bcbbca0a4..01be8d1d2 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -165,9 +165,6 @@ วิดีโอ เสียง ลองอีกครั้ง - พัน - ล้าน - พันล้าน ไม่มีสมาชิกที่สมัครรับ %s บอกรับ @@ -412,4 +409,23 @@ ไม่มี ไม่ ใช่/ตกลง + วิดีโอ + ปิดเสียง + เลิกปิดเสียง + ใหม่ + ฟีด + เพลง + อัลบั้ม + ไม่ต้อง + ล่าสุด + ความคิดเห็น + คำอธิบาย + แนะนำ + หมวดหมู่ + แท็ก + ลิขสิทธิ์ + ภาษา + จัดเรียง + การ์ด + กำลังจะมา diff --git a/app/src/main/res/values-ti/strings.xml b/app/src/main/res/values-ti/strings.xml index f25b86cc1..ccce73524 100644 --- a/app/src/main/res/values-ti/strings.xml +++ b/app/src/main/res/values-ti/strings.xml @@ -52,7 +52,7 @@ ቀዳማይ ወሰን ተጠዋቃ ደገመ ድምጺ ምውራድ ማህደር - ቦኦክማርከድ ዝርዝር-ጸወታ + ዝርዝር-ጸወታት ናይ ድምጺ ፋይል ኣራግፍ ምረጽ ዝጎደለ ኮረ ኣፕፕ ኣውራድ፧ ነባሪ ቅርጺ ድምጺ @@ -94,4 +94,95 @@ ደርፍታት ኣልበማት ስነ-ጥበባውያን + %1$s ድለ + ድለ %1$sን %2$s + ዝርዝር-ጸወታት + ራብዓይ ስጉምቲ መጠወቒ + ሕንፍሽፍሽ + ታሪኽ + ዞባዊ + ኣኼባ + ኣወግድ + ዝርዝር ሓበሬታ + መስርዕ + ተሰሪዑ + ስም ቀያር + ስም + ዓባስ + ኣብ ዝርዝር-ጸወታ ኮነ + ግጡም + ሙሉእ + ስጉሚ + ተቐበል + ንጸግ + ዋላ-ሓደ + ብፍጹም + ዝርዝር + ብዘይ መግለጺ ጽሑፍ + + %s ተራእዩ + %s ተራእዩ + + + %s ተኸታሊ + %s ተኸታተልቲ + + ሓድሽ ዝርዝር ጸወታ + ፍጠር + + %d ሰዓት + %d ሰዓታት + + + %d መዓልቲ + %d መዓልታት + + + %d ካልኢት + %d ካልኢት + + + %d ደቒቕ + %d ደቓይቕ + + እንታይ ሓድሽ ኣሎ + ሓድሽ + ጉጅለታት መስመር + ኩሉ ተጻወቶ + ኣብ ዝመጺእ + ብኸፊል ዝተራእየ + ብምሉእ ዝተራእየ + እዞም ዝስዕቡ ውሕጅታት ኣርእዩ + ዝተሓደሰ ዕለት: %s + ቅልጡፍ ኣገባብ ኣንቅሕ + ዝርዝራት: + ፈተውቲ + ጸላእቲ + ርእይቶታት + መብርሂ + ብድምጺ + እንደገና ፈትን + %sሽ + %sሚ + %sቢ + ጀምር + ደምስስ + ሰሩዞ + ስም ቀያር + ስሕተት + መፍለዪ + ህዝባዊ + ብሕትነት + ፍቓድታት + ፍቓድ ኣንብብ + ምድብ + ፍቓድ + እዋናዊታት + ኣቫታራት + ባነራት + ዘይተዘርዘረ + ብሕታዊ + ውሽጣዊ + ተኸታተልቲ + መርበብ-ቦታ ክፈት diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e2774ad39..947d0bcf9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -88,9 +88,6 @@ Devre dışı Yorumunuz (İngilizce): Ayrıntılar: - b - M - M Bu izin, açılır pencere kipinde \naçmak için gereklidir reCAPTCHA formu @@ -611,7 +608,6 @@ Açıklamadaki metni seçmeyi etkinleştir Artık, açıklamadaki metni seçebilirsiniz. Seçim kipindeyken sayfanın titreyebileceğini ve bağlantıların tıklanamayacağını unutmayın. Web sitesini aç - %s şu nedeni sağlıyor: Hesap sonlandırıldı Hızlı besleme kipi bununla ilgili daha çok bilgi sağlamıyor. Yazarın hesabı sonlandırılmış. @@ -834,4 +830,24 @@ %1$s İle Ara (%2$s) SoundCloud Top 50 sayfası kaldırıldı SoundCloud, özgün Top 50 listesini artık yayınlamıyor. İlgili sekme ana sayfanızdan kaldırıldı. + %sK + %sM + %sB + YouTube birleşik trendler kaldırıldı + YouTube, 21 Temmuz 2025\'ten sonra birleşik trendler sayfasını kaldırdı. NewPipe, öntanımlı trendler sayfasını trend canlı akışlarla değiştirdi.\n\n\"Ayarlar > İçerik > Ana sayfanın içeriği\"nden başka trend sayfalar seçebilirsiniz. + Oyun trendleri + Trend podcastler + Trend film ve gösteriler + Trend müzik + Açılır Oynatıcıyı kullanmak için Android ayarlar menüsünden %1$s seçilmeli ve %2$s etkinleştirilmeli. + “Diğer uygulamaların üstünde göstermeye izin ver” + Dosyayı sil + Girdiyi sil + Girdi silindi + Hesap sonlandırıldı\n\n%1$s şu nedeni sağladı: %2$s + Oynatırken sunucudan HTTP 403 hatası alındı, akış URL’si bitmiş ya da IP engellenmiş olabilir + Oynatırken sunucudan HTTP %1$s hatası alındı + Oynatırken sunucudan HTTP 403 hatası alındı, IP engeli ya da akış URL’si çözme sorunları olabilir + %1$s veri sağlamayı geri çevirdi, istekçinin robot olmadığını doğrulaması için oturum açmasını istiyor.\n\n%1$s, IP adresinizi geçici olarak engellemiş olabilir, bir süre bekleyebilir ya da başka IP\'ye geçebilirsiniz (örneğin VPN\'i açıp/kapatarak ya da WiFi\'den mobil veriye geçerek). + Bu içerik şu anda seçili içerik ülkesinde kullanılamıyor.\n\nSeçiminizi \"Ayarlar > İçerik > Öntanımlı içerik ülkesi\"nden değiştirin. diff --git a/app/src/main/res/values-tzm/strings.xml b/app/src/main/res/values-tzm/strings.xml index 8ef077a9f..a62b37fa3 100644 --- a/app/src/main/res/values-tzm/strings.xml +++ b/app/src/main/res/values-tzm/strings.xml @@ -113,7 +113,6 @@ Kkes Senulfu Senti - ifḍ Als-arem Imesli Avidyu diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d548dac3c..58e62c0d8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -65,9 +65,6 @@ Відео Аудіо Повторити спробу - тис - млн - млрд Почати Пауза Видалити @@ -627,7 +624,6 @@ Заборонити виділення тексту в описі Дозволити виділяти текст в описі Тепер можна виділяти текст в описі. Зауважте, що сторінка може мигати і посилання можуть не працювати в режимі виділення. - %s подає таку причину: Неможливо завантажити стрічку для «%s». Помилка завантаження стрічки Обліковий запис автора припинено. @@ -853,4 +849,24 @@ Пошук %1$s (%2$s) Сторінку SoundCloud Top 50 видалено SoundCloud припинив підтримку оригінальних чартів Топ-50. Відповідну вкладку видалено з вашої головної сторінки. + Видалено об’єднаний тренд YouTube + YouTube припинив підтримку об’єднаної сторінки трендів з 21 липня 2025 року. NewPipe замінив стандартну сторінку трендів на сторінку трендових прямих трансляцій.\n\nВи також можете вибрати різні сторінки трендів у розділі «Налаштування» > «Контент» > «Контент головної сторінки». + Ігрові тренди + Популярні подкасти + Популярні фільми та шоу + Популярна музика + %sтис + %sмлн + %sмлрд + Щоб використовувати спливаючий програвач, виберіть %1$s у наступному меню налаштувань Android та увімкніть %2$s. + “Дозволити показ поверх інших програм” + Видалити файл + Видалити запис + Запис видалено + Обліковий запис заблоковано\n\n%1$s вказує таку причину: %2$s + Під час відтворення від сервера отримано помилку HTTP 403, ймовірно, через закінчення терміну дії URL-адреси потокової передачі або заборону IP-адреси + Помилка HTTP %1$s отримана від сервера під час відтворення + Під час відтворення від сервера отримано помилку HTTP 403, ймовірно, через заборону IP-адреси або проблеми з деобфускацією URL-адреси потокової передачі + %1$s відмовився надати дані, запитуючи логін для підтвердження того, що запитувач не є ботом.\n\nВаша IP-адреса могла бути тимчасово заблокована %1$s. Ви можете почекати деякий час або перейти на іншу IP-адресу (наприклад, увімкнувши/вимкнувши VPN або переключившись з Wi-Fi на мобільні дані). + Цей контент недоступний для вибраної країни контенту.\n\nЗмініть свій вибір у розділі \"Налаштування > Контент > Країна контенту за замовчуванням\". diff --git a/app/src/main/res/values-und/strings.xml b/app/src/main/res/values-und/strings.xml index 94e4f9dd6..0bd3bf442 100644 --- a/app/src/main/res/values-und/strings.xml +++ b/app/src/main/res/values-und/strings.xml @@ -70,7 +70,6 @@ ویڈیو آڈیو فیر کرو - ہزار بݨاؤ بارے لائیسنس diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 1a434ab63..bb95e2811 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -146,9 +146,6 @@ ویڈیو آڈیو دوبارہ کوشش کریں - ہزار - دہ لاکھ - ارب کوئی صارفین نہیں %s صارف diff --git a/app/src/main/res/values-v35/styles.xml b/app/src/main/res/values-v35/styles.xml new file mode 100644 index 000000000..beb16bcdf --- /dev/null +++ b/app/src/main/res/values-v35/styles.xml @@ -0,0 +1,27 @@ + + + + + + + + +