1
mirror of https://github.com/TeamNewPipe/NewPipe synced 2025-10-30 19:20:54 +01:00

Compare commits

..

1 Commits

Author SHA1 Message Date
TobiGr
27b2d5de70 [AndroidTV] Fix selecting PeerTube instance in navigation drawer
Fixes #10020
2023-07-15 04:38:36 +02:00
1300 changed files with 8474 additions and 32883 deletions

View File

@@ -6,7 +6,7 @@ NewPipe contribution guidelines
## Crash reporting ## Crash reporting
Report crashes through the **automated crash report system** of NewPipe. Report crashes through the **automated crash report system** of NewPipe.
This way all the data needed for debugging is included in your bug report for GitHub. This way all the data needed for debugging is included in your bugreport for GitHub.
You'll see *exactly* what is sent, be able to add **your comments**, and then send it. You'll see *exactly* what is sent, be able to add **your comments**, and then send it.
## Issue reporting/feature requests ## Issue reporting/feature requests
@@ -42,6 +42,10 @@ You'll see *exactly* what is sent, be able to add **your comments**, and then se
* Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions. * Create PRs that cover only **one specific issue/solution/bug**. Do not create PRs that are huge monoliths and could have been split into multiple independent contributions.
* NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out. * NewPipe uses [NewPipeExtractor](https://github.com/TeamNewPipe/NewPipeExtractor) to fetch data from services. If you need to change something there, you must test your changes in NewPipe. Telling NewPipe to use your extractor version can be accomplished by editing the `app/build.gradle` file: the comments under the "NewPipe libraries" section of `dependencies` will help you out.
### Kotlin in NewPipe
* NewPipe will remain mostly Java for time being
* Contributions containing a simple conversion from Java to Kotlin should be avoided. Conversions to Kotlin should only be done if Kotlin actually brings improvements like bug fixes or better performance which are not, or only with much more effort, implementable in Java. The core team sees Java as an easier to learn and generally well adopted programming language.
### Creating a Pull Request (PR) ### Creating a Pull Request (PR)
* Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub. * Make changes on a **separate branch** with a meaningful name, not on the _master_ branch or the _dev_ branch. This is commonly known as *feature branch workflow*. You may then send your changes as a pull request (PR) on GitHub.
@@ -79,6 +83,6 @@ The [ktlint](https://github.com/pinterest/ktlint) plugin does the same job as ch
## Communication ## Communication
* You can use a Matrix account to join the NewPipe channel at [#newpipe:matrix.newpipe-ev.de](https://matrix.to/#/#newpipe:matrix.newpipe-ev.de). Some convenient clients, available both for phone and desktop, are listed at that link. * The #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) has the core team and other developers in it. [Click here for webchat](https://web.libera.chat/#newpipe)!
* Alternatively, the #newpipe channel on Libera Chat (`ircs://irc.libera.chat:6697/newpipe`) can also be joined, as it is bridged to the Matrix room. [Click here for webchat](https://web.libera.chat/#newpipe)! * You can also use a Matrix account to join the NewPipe channel at [#newpipe:libera.chat](https://matrix.to/#/#newpipe:libera.chat). Some convenient clients, available both for phone and desktop, are listed at that link.
* You can post your suggestions, changes, ideas etc. on either GitHub or Matrix (including via IRC). * You can post your suggestions, changes, ideas etc. on either GitHub or IRC.

View File

@@ -1,3 +1,6 @@
name: Question
description: Ask about anything NewPipe-related
labels: [question]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@@ -14,7 +14,7 @@ body:
attributes: attributes:
label: "Checklist" label: "Checklist"
options: options:
- label: "I am able to reproduce the bug with the latest version given here: [CLICK THIS LINK](https://github.com/TeamNewPipe/NewPipe/releases/latest)." - label: "I am able to reproduce the bug with the [latest version](https://github.com/TeamNewPipe/NewPipe/releases/latest)."
required: true required: true
- label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to." - label: "I made sure that there are *no existing issues* - [open](https://github.com/TeamNewPipe/NewPipe/issues) or [closed](https://github.com/TeamNewPipe/NewPipe/issues?q=is%3Aissue+is%3Aclosed) - which I could contribute my information to."
required: true required: true

View File

@@ -3,9 +3,9 @@ contact_links:
- name: ❓ Question - name: ❓ Question
url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions url: https://github.com/TeamNewPipe/NewPipe/discussions/new?category=questions
about: Ask about anything NewPipe-related about: Ask about anything NewPipe-related
- name: 💬 Matrix
url: https://matrix.to/#/#newpipe:matrix.newpipe-ev.de
about: Chat with us via Matrix for quick Q/A
- name: 💬 IRC - name: 💬 IRC
url: https://web.libera.chat/#newpipe url: https://web.libera.chat/#newpipe
about: Chat with us via IRC for quick Q/A about: Chat with us via IRC for quick Q/A
- name: 💬 Matrix
url: https://matrix.to/#/#newpipe:libera.chat
about: Chat with us via Matrix for quick Q/A

View File

@@ -1,17 +0,0 @@
# Add 'size/small' label to any changes with less than 50 lines
size/small:
max: 49
# Add 'size/medium' label to any changes between 50 and 249 lines
size/medium:
min: 50
max: 249
# Add 'size/large' label to any changes between 250 and 749 lines
size/large:
min: 250
max: 749
# Add 'size/giant' label to any changes for more than 749 lines
size/giant:
min: 750

View File

@@ -1,38 +0,0 @@
name: "Build unsigned release APK on master"
on:
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: 'master'
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
cache: 'gradle'
- name: "Build release APK"
run: ./gradlew assembleRelease --stacktrace
- name: "Rename APK"
run: |
VERSION_NAME="$(jq -r ".elements[0].versionName" "app/build/outputs/apk/release/output-metadata.json")"
echo "Version name: $VERSION_NAME" >> "$GITHUB_STEP_SUMMARY"
echo '```json' >> "$GITHUB_STEP_SUMMARY"
cat "app/build/outputs/apk/release/output-metadata.json" >> "$GITHUB_STEP_SUMMARY"
echo >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
# assume there is only one APK in that folder
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
with:
name: app
path: app/build/outputs/apk/release/*.apk

View File

@@ -6,7 +6,6 @@ on:
branches: branches:
- dev - dev
- master - master
- refactor
- release** - release**
paths-ignore: paths-ignore:
- 'README.md' - 'README.md'
@@ -37,8 +36,8 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v2 - uses: gradle/wrapper-validation-action@v1
- name: create and checkout branch - name: create and checkout branch
# push events already checked out the branch # push events already checked out the branch
@@ -47,10 +46,10 @@ jobs:
BRANCH: ${{ github.head_ref }} BRANCH: ${{ github.head_ref }}
run: git checkout -B "$BRANCH" run: git checkout -B "$BRANCH"
- name: set up JDK - name: set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: 21 java-version: 17
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
@@ -58,13 +57,14 @@ jobs:
run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint run: ./gradlew assembleDebug lintDebug testDebugUnitTest --stacktrace -DskipFormatKtlint
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: app name: app
path: app/build/outputs/apk/debug/*.apk path: app/build/outputs/apk/debug/*.apk
test-android: test-android:
runs-on: ubuntu-latest # macos has hardware acceleration. See android-emulator-runner action
runs-on: macos-latest
timeout-minutes: 20 timeout-minutes: 20
strategy: strategy:
matrix: matrix:
@@ -80,18 +80,12 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Enable KVM - name: set up JDK 17
run: | uses: actions/setup-java@v3
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: set up JDK
uses: actions/setup-java@v4
with: with:
java-version: 21 java-version: 17
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
@@ -104,7 +98,7 @@ jobs:
script: ./gradlew connectedCheck --stacktrace script: ./gradlew connectedCheck --stacktrace
- name: Upload test report when tests fail # because the printed out stacktrace (console) is too short, see also #7553 - 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@v3
if: failure() if: failure()
with: with:
name: android-test-report-api${{ matrix.api-level }} name: android-test-report-api${{ matrix.api-level }}
@@ -117,19 +111,19 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
- name: Set up JDK - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: 21 java-version: 17
distribution: "temurin" distribution: "temurin"
cache: 'gradle' cache: 'gradle'
- name: Cache SonarCloud packages - name: Cache SonarCloud packages
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: ~/.sonar/cache path: ~/.sonar/cache
key: ${{ runner.os }}-sonar key: ${{ runner.os }}-sonar

View File

@@ -17,8 +17,6 @@ module.exports = async ({github, context}) => {
initialBody = context.payload.comment.body; initialBody = context.payload.comment.body;
} else if (context.eventName == 'issues') { } else if (context.eventName == 'issues') {
initialBody = context.payload.issue.body; initialBody = context.payload.issue.body;
} else if (context.eventName == 'pull_request') {
initialBody = context.payload.pull_request.body;
} else { } else {
console.log('Aborting: No body found'); console.log('Aborting: No body found');
return; return;
@@ -32,8 +30,8 @@ module.exports = async ({github, context}) => {
} }
// Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>) // Regex for finding images (simple variant) ![ALT_TEXT](https://*.githubusercontent.com/<number>/<variousHexStringsAnd->.<fileExtension>)
const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[([^\]]*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm; const REGEX_USER_CONTENT_IMAGE_LOOKUP = /\!\[(.*)\]\((https:\/\/[-a-z0-9]+\.githubusercontent\.com\/\d+\/[-0-9a-f]{32,512}\.(jpg|gif|png))\)/gm;
const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[([^\]]*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm; const REGEX_ASSETS_IMAGE_LOCKUP = /\!\[(.*)\]\((https:\/\/github\.com\/[-\w\d]+\/[-\w\d]+\/assets\/\d+\/[\-0-9a-f]{32,512})\)/gm;
// Check if we found something // Check if we found something
let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody) let foundSimpleImages = REGEX_USER_CONTENT_IMAGE_LOOKUP.test(initialBody)
@@ -76,17 +74,9 @@ module.exports = async ({github, context}) => {
repo: context.repo.repo, repo: context.repo.repo,
body: newBody body: newBody
}); });
} else if (context.eventName == 'pull_request') {
console.log('Updating pull request', context.payload.pull_request.number);
await github.rest.pulls.update({
pull_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: newBody
});
} }
// Async replace function from https://stackoverflow.com/a/48032528 // Asnyc replace function from https://stackoverflow.com/a/48032528
async function replaceAsync(str, regex, asyncFn) { async function replaceAsync(str, regex, asyncFn) {
const promises = []; const promises = [];
str.replace(regex, (match, ...args) => { str.replace(regex, (match, ...args) => {
@@ -138,7 +128,7 @@ module.exports = async ({github, context}) => {
if (shouldModify) { if (shouldModify) {
wasMatchModified = true; wasMatchModified = true;
console.log(`Modifying match '${match}'`); console.log(`Modifying match '${match}'`);
return `<img alt="${g1}" src="${g2}" width=${Math.min(600, Math.floor(IMG_MAX_HEIGHT_PX * probeAspectRatio))} />`; return `<img alt="${g1}" src="${g2}" width=${Math.min(600, (IMG_MAX_HEIGHT_PX * probeAspectRatio).toFixed(0))} />`;
} }
console.log(`Match '${match}' is ok/will not be modified`); console.log(`Match '${match}' is ok/will not be modified`);

View File

@@ -5,8 +5,6 @@ on:
types: [created, edited] types: [created, edited]
issues: issues:
types: [opened, edited] types: [opened, edited]
pull_request:
types: [opened, edited]
permissions: permissions:
issues: write issues: write
@@ -17,9 +15,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
@@ -27,7 +25,7 @@ jobs:
run: npm i probe-image-size@7.2.3 --ignore-scripts run: npm i probe-image-size@7.2.3 --ignore-scripts
- name: Minimize simple images - name: Minimize simple images
uses: actions/github-script@v7 uses: actions/github-script@v6
timeout-minutes: 3 timeout-minutes: 3
with: with:
script: | script: |

View File

@@ -1,18 +0,0 @@
name: "PR size labeler"
on: [pull_request_target]
permissions:
contents: read
pull-requests: write
jobs:
changed-lines-count-labeler:
runs-on: ubuntu-latest
name: Automatically labelling pull requests based on the changed lines count
permissions:
pull-requests: write
steps:
- name: Set a label
uses: TeamNewPipe/changed-lines-count-labeler@main
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/changed-lines-count-labeler.yml

21
.idea/icon.svg generated
View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px"
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
<style type="text/css">
.st0{fill:#CD201F;}
.st1{fill:#FFFFFF;}
</style>
<g id="Alapkör">
<circle id="XMLID_23_" class="st0" cx="50" cy="50" r="50"/>
</g>
<g id="Elemek">
<path id="XMLID_19_" class="st1" d="M47,28.2c-9-5.3-15.3-9-15.3-9v61.7c0,0,30.4-18,52.3-30.9C72.1,43,57.7,34.5,47,28.2z"/>
</g>
<g id="Fedő">
<path id="XMLID_5_" class="st0" d="M48.4,40.1c-4.1-2.4-7-4.1-7-4.1V64c0,0,13.9-8.2,23.8-14C59.8,46.8,53.3,42.9,48.4,40.1z"/>
<rect id="XMLID_4_" x="41.4" y="55.6" class="st0" width="6.2" height="21"/>
</g>
<g id="Vonalak">
</g>
</svg>

Before

Width:  |  Height:  |  Size: 850 B

File diff suppressed because one or more lines are too long

View File

@@ -12,7 +12,7 @@ plugins {
} }
android { android {
compileSdk 34 compileSdk 33
namespace 'org.schabi.newpipe' namespace 'org.schabi.newpipe'
defaultConfig { defaultConfig {
@@ -20,15 +20,8 @@ android {
resValue "string", "app_name", "NewPipe" resValue "string", "app_name", "NewPipe"
minSdk 21 minSdk 21
targetSdk 33 targetSdk 33
if (System.properties.containsKey('versionCodeOverride')) { versionCode 993
versionCode System.getProperty('versionCodeOverride') as Integer versionName "0.25.1"
} else {
versionCode 1004
}
versionName "0.27.7"
if (System.properties.containsKey('versionNameSuffix')) {
versionNameSuffix System.getProperty('versionNameSuffix')
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -57,6 +50,9 @@ android {
} }
} }
// Keep the release build type at the end of the list to override 'archivesBaseName' of
// debug build. This seems to be a Gradle bug, therefore
// TODO: update Gradle version
release { release {
if (System.properties.containsKey('packageSuffix')) { if (System.properties.containsKey('packageSuffix')) {
applicationIdSuffix System.getProperty('packageSuffix') applicationIdSuffix System.getProperty('packageSuffix')
@@ -99,16 +95,13 @@ android {
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
packagingOptions { packagingOptions {
resources { resources {
// remove two files which belong to jsoup // remove two files which belong to jsoup
// no idea how they ended up in the META-INF dir... // no idea how they ended up in the META-INF dir...
excludes += ['META-INF/README.md', 'META-INF/CHANGES', excludes += ['META-INF/README.md', 'META-INF/CHANGES']
// 'COPYRIGHT' belongs to RxJava...
'META-INF/COPYRIGHT']
} }
} }
} }
@@ -116,18 +109,19 @@ android {
ext { ext {
checkstyleVersion = '10.12.1' checkstyleVersion = '10.12.1'
androidxLifecycleVersion = '2.6.2' androidxLifecycleVersion = '2.5.1'
androidxRoomVersion = '2.6.1' androidxRoomVersion = '2.4.3'
androidxWorkVersion = '2.8.1' androidxWorkVersion = '2.7.1'
stateSaverVersion = '1.4.1' icepickVersion = '3.2.0'
exoPlayerVersion = '2.18.7' exoPlayerVersion = '2.18.7'
googleAutoServiceVersion = '1.1.1' googleAutoServiceVersion = '1.0.1'
groupieVersion = '2.10.1' groupieVersion = '2.10.1'
markwonVersion = '4.6.2' markwonVersion = '4.6.2'
leakCanaryVersion = '2.12' leakCanaryVersion = '2.9.1'
stethoVersion = '1.6.0' stethoVersion = '1.6.0'
mockitoVersion = '4.0.0'
} }
configurations { configurations {
@@ -142,7 +136,7 @@ checkstyle {
toolVersion = checkstyleVersion toolVersion = checkstyleVersion
} }
tasks.register('runCheckstyle', Checkstyle) { task runCheckstyle(type: Checkstyle) {
source 'src' source 'src'
include '**/*.java' include '**/*.java'
exclude '**/gen/**' exclude '**/gen/**'
@@ -163,7 +157,7 @@ tasks.register('runCheckstyle', Checkstyle) {
def outputDir = "${project.buildDir}/reports/ktlint/" def outputDir = "${project.buildDir}/reports/ktlint/"
def inputFiles = project.fileTree(dir: "src", include: "**/*.kt") def inputFiles = project.fileTree(dir: "src", include: "**/*.kt")
tasks.register('runKtlint', JavaExec) { task runKtlint(type: JavaExec) {
inputs.files(inputFiles) inputs.files(inputFiles)
outputs.dir(outputDir) outputs.dir(outputDir)
getMainClass().set("com.pinterest.ktlint.Main") getMainClass().set("com.pinterest.ktlint.Main")
@@ -172,7 +166,7 @@ tasks.register('runKtlint', JavaExec) {
jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED")
} }
tasks.register('formatKtlint', JavaExec) { task formatKtlint(type: JavaExec) {
inputs.files(inputFiles) inputs.files(inputFiles)
outputs.dir(outputDir) outputs.dir(outputDir)
getMainClass().set("com.pinterest.ktlint.Main") getMainClass().set("com.pinterest.ktlint.Main")
@@ -198,7 +192,7 @@ sonar {
dependencies { dependencies {
/** Desugaring **/ /** Desugaring **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.0.4' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
/** NewPipe libraries **/ /** NewPipe libraries **/
// You can use a local version by uncommenting a few lines in settings.gradle // You can use a local version by uncommenting a few lines in settings.gradle
@@ -206,9 +200,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test // 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/ // This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751' implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
// WORKAROUND: if you get errors with the NewPipeExtractor dependency, replace `v0.24.3` with implementation 'com.github.TeamNewPipe:NewPipeExtractor:8495ad619e'
// the corresponding commit hash, since JitPack is sometimes buggy
implementation 'com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:v0.24.6'
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0' implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
/** Checkstyle **/ /** Checkstyle **/
@@ -216,44 +208,42 @@ dependencies {
ktlint 'com.pinterest:ktlint:0.45.2' ktlint 'com.pinterest:ktlint:0.45.2'
/** Kotlin **/ /** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}"
/** AndroidX **/ /** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-livedata-ktx:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.6.0'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference:1.2.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}" implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}" implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}" kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// Newer version specified to prevent accessibility regressions with RecyclerView, see: // Newer version specified to prevent accessibility regressions with RecyclerView, see:
// https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01 // https://developer.android.com/jetpack/androidx/releases/viewpager2#1.1.0-alpha01
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta01'
implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}" implementation "androidx.work:work-runtime-ktx:${androidxWorkVersion}"
implementation "androidx.work:work-rxjava3:${androidxWorkVersion}" implementation "androidx.work:work-rxjava3:${androidxWorkVersion}"
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.6.1'
implementation "androidx.webkit:webkit:1.9.0"
/** Third-party libraries **/ /** Third-party libraries **/
// Instance state boilerplate elimination // Instance state boilerplate elimination
implementation 'com.github.livefront:bridge:v2.0.2' implementation "frankiesardo:icepick:${icepickVersion}"
implementation "com.evernote:android-state:$stateSaverVersion" kapt "frankiesardo:icepick-processor:${icepickVersion}"
kapt "com.evernote:android-state-processor:$stateSaverVersion"
// HTML parser // HTML parser
implementation "org.jsoup:jsoup:1.17.2" implementation "org.jsoup:jsoup:1.16.1"
// HTTP client // HTTP client
implementation "com.squareup.okhttp3:okhttp:4.12.0" implementation "com.squareup.okhttp3:okhttp:4.11.0"
// Media player // Media player
implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}" implementation "com.google.android.exoplayer:exoplayer-core:${exoPlayerVersion}"
@@ -282,37 +272,38 @@ dependencies {
implementation "io.noties.markwon:linkify:${markwonVersion}" implementation "io.noties.markwon:linkify:${markwonVersion}"
// Crash reporting // Crash reporting
implementation "ch.acra:acra-core:5.11.3" implementation "ch.acra:acra-core:5.10.1"
// Properly restarting // Properly restarting
implementation 'com.jakewharton:process-phoenix:2.1.2' implementation 'com.jakewharton:process-phoenix:2.1.2'
// Reactive extensions for Java VM // Reactive extensions for Java VM
implementation "io.reactivex.rxjava3:rxjava:3.1.8" implementation "io.reactivex.rxjava3:rxjava:3.1.6"
implementation "io.reactivex.rxjava3:rxandroid:3.0.2" implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
// RxJava binding APIs for Android UI widgets // RxJava binding APIs for Android UI widgets
implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0" implementation "com.jakewharton.rxbinding4:rxbinding:4.0.0"
// Date and time formatting // Date and time formatting
implementation "org.ocpsoft.prettytime:prettytime:5.0.8.Final" implementation "org.ocpsoft.prettytime:prettytime:5.0.6.Final"
/** Debugging **/ /** Debugging **/
// Memory leak detection // Memory leak detection
debugImplementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}" implementation "com.squareup.leakcanary:leakcanary-object-watcher-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}" implementation "com.squareup.leakcanary:plumber-android:${leakCanaryVersion}"
debugImplementation "com.squareup.leakcanary:leakcanary-android-core:${leakCanaryVersion}" debugImplementation "com.squareup.leakcanary:leakcanary-android:${leakCanaryVersion}"
// Debug bridge for Android // Debug bridge for Android
debugImplementation "com.facebook.stetho:stetho:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho:${stethoVersion}"
debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}" debugImplementation "com.facebook.stetho:stetho-okhttp3:${stethoVersion}"
/** Testing **/ /** Testing **/
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.6.0' testImplementation "org.mockito:mockito-core:${mockitoVersion}"
testImplementation "org.mockito:mockito-inline:${mockitoVersion}"
androidTestImplementation "androidx.test.ext:junit:1.1.5" androidTestImplementation "androidx.test.ext:junit:1.1.5"
androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}" androidTestImplementation "androidx.room:room-testing:${androidxRoomVersion}"
androidTestImplementation "org.assertj:assertj-core:3.24.2" androidTestImplementation "org.assertj:assertj-core:3.23.1"
} }
static String getGitWorkingBranch() { static String getGitWorkingBranch() {

View File

@@ -5,21 +5,22 @@
## Rules for NewPipeExtractor ## Rules for NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } -keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
## Rules for Rhino and Rhino Engine
-keep class org.mozilla.javascript.* { *; }
-keep class org.mozilla.javascript.** { *; } -keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.javascript.engine.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter -keep class org.mozilla.classfile.ClassFileWriter
-dontwarn org.mozilla.javascript.JavaToJSONConverters
-dontwarn org.mozilla.javascript.tools.** -dontwarn org.mozilla.javascript.tools.**
-keep class javax.script.** { *; }
-dontwarn javax.script.**
-keep class jdk.dynalink.** { *; }
-dontwarn jdk.dynalink.**
## Rules for ExoPlayer ## Rules for ExoPlayer
-keep class com.google.android.exoplayer2.** { *; } -keep class com.google.android.exoplayer2.** { *; }
## Rules for Icepick. Copy pasted from https://github.com/frankiesardo/icepick
-dontwarn icepick.**
-keep class icepick.** { *; }
-keep class **$$Icepick { *; }
-keepclasseswithmembernames class * {
@icepick.* <fields>;
}
-keepnames class * { @icepick.State *;}
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp ## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
-dontwarn okhttp3.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,15 @@ import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import androidx.room.Room import androidx.room.Room
import androidx.room.testing.MigrationTestHelper import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.stream.StreamType import org.schabi.newpipe.extractor.stream.StreamType
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@@ -24,23 +21,20 @@ class DatabaseMigrationTest {
private const val DEFAULT_SERVICE_ID = 0 private const val DEFAULT_SERVICE_ID = 0
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4" private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
private const val DEFAULT_TITLE = "Test Title" private const val DEFAULT_TITLE = "Test Title"
private const val DEFAULT_NAME = "Test Name"
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
private const val DEFAULT_DURATION = 480L private const val DEFAULT_DURATION = 480L
private const val DEFAULT_UPLOADER_NAME = "Uploader Test" private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg" private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
private const val DEFAULT_SECOND_SERVICE_ID = 1 private const val DEFAULT_SECOND_SERVICE_ID = 0
private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc" private const val DEFAULT_SECOND_URL = "https://www.youtube.com/watch?v=ncQU6iBn5Fc"
private const val DEFAULT_THIRD_SERVICE_ID = 2
private const val DEFAULT_THIRD_URL = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
} }
@get:Rule @get:Rule
val testHelper = MigrationTestHelper( val testHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
) )
@Test @Test
@@ -114,20 +108,6 @@ class DatabaseMigrationTest {
Migrations.MIGRATION_6_7 Migrations.MIGRATION_6_7
) )
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_8,
true,
Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV3 = getMigratedDatabase() val migratedDatabaseV3 = getMigratedDatabase()
val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst() val listFromDB = migratedDatabaseV3.streamDAO().all.blockingFirst()
@@ -162,157 +142,6 @@ class DatabaseMigrationTest {
assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation) assertNull(secondStreamFromMigratedDatabase.isUploadDateApproximation)
} }
@Test
fun migrateDatabaseFrom7to8() {
val databaseInV7 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_7)
val defaultSearch1 = " abc "
val defaultSearch2 = " abc"
val serviceId = DEFAULT_SERVICE_ID // YouTube
// Use id different to YouTube because two searches with the same query
// but different service are considered not equal.
val otherServiceId = ServiceList.SoundCloud.serviceId
databaseInV7.run {
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", serviceId)
put("search", defaultSearch2)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch1)
}
)
insert(
"search_history", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", otherServiceId)
put("search", defaultSearch2)
}
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_8,
true, Migrations.MIGRATION_7_8
)
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME, Migrations.DB_VER_9,
true, Migrations.MIGRATION_8_9
)
val migratedDatabaseV8 = getMigratedDatabase()
val listFromDB = migratedDatabaseV8.searchHistoryDAO().all.blockingFirst()
assertEquals(2, listFromDB.size)
assertEquals("abc", listFromDB[0].search)
assertEquals("abc", listFromDB[1].search)
assertNotEquals(listFromDB[0].serviceId, listFromDB[1].serviceId)
}
@Test
fun migrateDatabaseFrom8to9() {
val databaseInV8 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_8)
val localUid1: Long
val localUid2: Long
val remoteUid1: Long
val remoteUid2: Long
databaseInV8.run {
localUid1 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "1")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
localUid2 = insert(
"playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("name", DEFAULT_NAME + "2")
put("is_thumbnail_permanent", false)
put("thumbnail_stream_id", -1)
}
)
delete(
"playlists", "uid = ?",
Array(1) { localUid1 }
)
remoteUid1 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SERVICE_ID)
put("url", DEFAULT_URL)
}
)
remoteUid2 = insert(
"remote_playlists", SQLiteDatabase.CONFLICT_FAIL,
ContentValues().apply {
put("service_id", DEFAULT_SECOND_SERVICE_ID)
put("url", DEFAULT_SECOND_URL)
}
)
delete(
"remote_playlists", "uid = ?",
Array(1) { remoteUid2 }
)
close()
}
testHelper.runMigrationsAndValidate(
AppDatabase.DATABASE_NAME,
Migrations.DB_VER_9,
true,
Migrations.MIGRATION_8_9
)
val migratedDatabaseV9 = getMigratedDatabase()
var localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(1, localListFromDB.size)
assertEquals(localUid2, localListFromDB[0].uid)
assertEquals(-1, localListFromDB[0].displayIndex)
assertEquals(1, remoteListFromDB.size)
assertEquals(remoteUid1, remoteListFromDB[0].uid)
assertEquals(-1, remoteListFromDB[0].displayIndex)
val localUid3 = migratedDatabaseV9.playlistDAO().insert(
PlaylistEntity(DEFAULT_NAME + "3", false, -1, -1)
)
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
PlaylistRemoteEntity(
DEFAULT_THIRD_SERVICE_ID, DEFAULT_NAME, DEFAULT_THIRD_URL,
DEFAULT_THUMBNAIL, DEFAULT_UPLOADER_NAME, -1, 10
)
)
localListFromDB = migratedDatabaseV9.playlistDAO().all.blockingFirst()
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().all.blockingFirst()
assertEquals(2, localListFromDB.size)
assertEquals(localUid3, localListFromDB[1].uid)
assertEquals(-1, localListFromDB[1].displayIndex)
assertEquals(2, remoteListFromDB.size)
assertEquals(remoteUid3, remoteListFromDB[1].uid)
assertEquals(-1, remoteListFromDB[1].displayIndex)
}
private fun getMigratedDatabase(): AppDatabase { private fun getMigratedDatabase(): AppDatabase {
val database: AppDatabase = Room.databaseBuilder( val database: AppDatabase = Room.databaseBuilder(
ApplicationProvider.getApplicationContext(), ApplicationProvider.getApplicationContext(),

View File

@@ -1,130 +0,0 @@
package org.schabi.newpipe.database
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import io.reactivex.rxjava3.core.Single
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Before
import org.junit.Test
import org.schabi.newpipe.database.feed.dao.FeedDAO
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.database.stream.dao.StreamDAO
import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.subscription.SubscriptionDAO
import org.schabi.newpipe.database.subscription.SubscriptionEntity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.stream.StreamType
import java.io.IOException
import java.time.OffsetDateTime
import kotlin.streams.toList
class FeedDAOTest {
private lateinit var db: AppDatabase
private lateinit var feedDAO: FeedDAO
private lateinit var streamDAO: StreamDAO
private lateinit var subscriptionDAO: SubscriptionDAO
private val serviceId = ServiceList.YouTube.serviceId
private val stream1 = StreamEntity(1, serviceId, "https://youtube.com/watch?v=1", "stream 1", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-01", OffsetDateTime.parse("2023-01-01T00:00:00Z"))
private val stream2 = StreamEntity(2, serviceId, "https://youtube.com/watch?v=2", "stream 2", StreamType.VIDEO_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-02", OffsetDateTime.parse("2023-01-02T00:00:00Z"))
private val stream3 = StreamEntity(3, serviceId, "https://youtube.com/watch?v=3", "stream 3", StreamType.LIVE_STREAM, 1000, "channel-1", "https://youtube.com/channel/1", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-01-03", OffsetDateTime.parse("2023-01-03T00:00:00Z"))
private val stream4 = StreamEntity(4, serviceId, "https://youtube.com/watch?v=4", "stream 4", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val stream5 = StreamEntity(5, serviceId, "https://youtube.com/watch?v=5", "stream 5", StreamType.VIDEO_STREAM, 1000, "channel-2", "https://youtube.com/channel/2", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-20", OffsetDateTime.parse("2023-08-20T00:00:00Z"))
private val stream6 = StreamEntity(6, serviceId, "https://youtube.com/watch?v=6", "stream 6", StreamType.VIDEO_STREAM, 1000, "channel-3", "https://youtube.com/channel/3", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-09-01", OffsetDateTime.parse("2023-09-01T00:00:00Z"))
private val stream7 = StreamEntity(7, serviceId, "https://youtube.com/watch?v=7", "stream 7", StreamType.VIDEO_STREAM, 1000, "channel-4", "https://youtube.com/channel/4", "https://i.ytimg.com/vi/1/hqdefault.jpg", 100, "2023-08-10", OffsetDateTime.parse("2023-08-10T00:00:00Z"))
private val allStreams = listOf(
stream1, stream2, stream3, stream4, stream5, stream6, stream7
)
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java
).build()
feedDAO = db.feedDAO()
streamDAO = db.streamDAO()
subscriptionDAO = db.subscriptionDAO()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun testUnlinkStreamsOlderThan_KeepOne() {
setupUnlinkDelete("2023-08-15T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
@Test
fun testUnlinkStreamsOlderThan_KeepMultiple() {
setupUnlinkDelete("2023-08-01T00:00:00Z")
val streams = feedDAO.getStreams(
FeedGroupEntity.GROUP_ALL_ID, includePlayed = true, includePartiallyPlayed = true, null
)
.blockingGet()
val allowedStreams = listOf(stream3, stream4, stream5, stream6, stream7)
assertEqual(streams, allowedStreams)
}
private fun assertEqual(streams: List<StreamWithState>?, allowedStreams: List<StreamEntity>) {
assertNotNull(streams)
assertEquals(
allowedStreams,
streams!!
.map { it.stream }
.sortedBy { it.uid }
.toList()
)
}
private fun setupUnlinkDelete(time: String) {
clearAndFillTables()
Single.fromCallable {
feedDAO.unlinkStreamsOlderThan(OffsetDateTime.parse(time))
}.blockingSubscribe()
Single.fromCallable {
streamDAO.deleteOrphans()
}.blockingSubscribe()
}
private fun clearAndFillTables() {
db.clearAllTables()
streamDAO.insertAll(allStreams)
subscriptionDAO.insertAll(
listOf(
SubscriptionEntity.from(ChannelInfo(serviceId, "1", "https://youtube.com/channel/1", "https://youtube.com/channel/1", "channel-1")),
SubscriptionEntity.from(ChannelInfo(serviceId, "2", "https://youtube.com/channel/2", "https://youtube.com/channel/2", "channel-2")),
SubscriptionEntity.from(ChannelInfo(serviceId, "3", "https://youtube.com/channel/3", "https://youtube.com/channel/3", "channel-3")),
SubscriptionEntity.from(ChannelInfo(serviceId, "4", "https://youtube.com/channel/4", "https://youtube.com/channel/4", "channel-4")),
)
)
feedDAO.insertAll(
listOf(
FeedEntity(1, 1),
FeedEntity(2, 1),
FeedEntity(3, 1),
FeedEntity(4, 2),
FeedEntity(5, 2),
FeedEntity(6, 3),
FeedEntity(7, 4),
)
)
}
}

View File

@@ -1,82 +0,0 @@
package org.schabi.newpipe.local.subscription;
import static org.junit.Assert.assertEquals;
import androidx.test.core.app.ApplicationProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.feed.model.FeedGroupEntity;
import org.schabi.newpipe.database.subscription.SubscriptionEntity;
import org.schabi.newpipe.extractor.channel.ChannelInfo;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
import org.schabi.newpipe.testUtil.TestDatabase;
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule;
import java.io.IOException;
import java.util.List;
public class SubscriptionManagerTest {
private AppDatabase database;
private SubscriptionManager manager;
@Rule
public TrampolineSchedulerRule trampolineScheduler = new TrampolineSchedulerRule();
private SubscriptionEntity getAssertOneSubscriptionEntity() {
final List<SubscriptionEntity> entities = manager
.getSubscriptions(FeedGroupEntity.GROUP_ALL_ID, "", false)
.blockingFirst();
assertEquals(1, entities.size());
return entities.get(0);
}
@Before
public void setup() {
database = TestDatabase.Companion.createReplacingNewPipeDatabase();
manager = new SubscriptionManager(ApplicationProvider.getApplicationContext());
}
@After
public void cleanUp() {
database.close();
}
@Test
public void testInsert() throws ExtractionException, IOException {
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/3blue1brown");
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
manager.insertSubscription(subscription);
final SubscriptionEntity readSubscription = getAssertOneSubscriptionEntity();
// the uid has changed, since the uid is chosen upon inserting, but the rest should match
assertEquals(subscription.getServiceId(), readSubscription.getServiceId());
assertEquals(subscription.getUrl(), readSubscription.getUrl());
assertEquals(subscription.getName(), readSubscription.getName());
assertEquals(subscription.getAvatarUrl(), readSubscription.getAvatarUrl());
assertEquals(subscription.getSubscriberCount(), readSubscription.getSubscriberCount());
assertEquals(subscription.getDescription(), readSubscription.getDescription());
}
@Test
public void testUpdateNotificationMode() throws ExtractionException, IOException {
final ChannelInfo info = ChannelInfo.getInfo("https://www.youtube.com/c/veritasium");
final SubscriptionEntity subscription = SubscriptionEntity.from(info);
subscription.setNotificationMode(0);
manager.insertSubscription(subscription);
manager.updateNotificationMode(subscription.getServiceId(), subscription.getUrl(), 1)
.blockingAwait();
final SubscriptionEntity anotherSubscription = getAssertOneSubscriptionEntity();
assertEquals(0, subscription.getNotificationMode());
assertEquals(subscription.getUrl(), anotherSubscription.getUrl());
assertEquals(1, anotherSubscription.getNotificationMode());
}
}

View File

@@ -12,21 +12,15 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest import androidx.test.filters.MediumTest
import androidx.test.internal.runner.junit4.statement.UiThreadStatement import androidx.test.internal.runner.junit4.statement.UiThreadStatement
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.schabi.newpipe.R import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.MediaFormat import org.schabi.newpipe.extractor.MediaFormat
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.stream.AudioStream import org.schabi.newpipe.extractor.stream.AudioStream
import org.schabi.newpipe.extractor.stream.Stream import org.schabi.newpipe.extractor.stream.Stream
import org.schabi.newpipe.extractor.stream.SubtitlesStream import org.schabi.newpipe.extractor.stream.SubtitlesStream
import org.schabi.newpipe.extractor.stream.VideoStream import org.schabi.newpipe.extractor.stream.VideoStream
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
@MediumTest @MediumTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@@ -90,7 +84,7 @@ class StreamItemAdapterTest {
@Test @Test
fun subtitleStreams_noIcon() { fun subtitleStreams_noIcon() {
val adapter = StreamItemAdapter<SubtitlesStream, Stream>( val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
StreamItemAdapter.StreamInfoWrapper( StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map { (0 until 5).map {
SubtitlesStream.Builder() SubtitlesStream.Builder()
.setContent("https://example.com", true) .setContent("https://example.com", true)
@@ -111,7 +105,7 @@ class StreamItemAdapterTest {
@Test @Test
fun audioStreams_noIcon() { fun audioStreams_noIcon() {
val adapter = StreamItemAdapter<AudioStream, Stream>( val adapter = StreamItemAdapter<AudioStream, Stream>(
StreamItemAdapter.StreamInfoWrapper( StreamItemAdapter.StreamSizeWrapper(
(0 until 5).map { (0 until 5).map {
AudioStream.Builder() AudioStream.Builder()
.setId(Stream.ID_UNKNOWN) .setId(Stream.ID_UNKNOWN)
@@ -129,109 +123,12 @@ class StreamItemAdapterTest {
} }
} }
@Test
fun retrieveMediaFormatFromFileTypeHeaders() {
val streams = getIncompleteAudioStreams(5)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromFileTypeHeaders(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(getResponse(mapOf(Pair("file-type", "mp0"))), 1)
helper.assertValidResponse(getResponse(mapOf(Pair("x-amz-meta-file-type", "aiff"))), 2, MediaFormat.AIFF)
helper.assertValidResponse(getResponse(mapOf(Pair("file-type", "mp3"))), 3, MediaFormat.MP3)
}
@Test
fun retrieveMediaFormatFromContentDispositionHeader() {
val streams = getIncompleteAudioStreams(11)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromContentDispositionHeader(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "mp3"))), 0)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.png\""))), 1
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"data.csv\""))), 2
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; filename=\"data.csv\""))), 3
)
helper.assertInvalidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"fieldName\"; filename*=\"filename.jpg\""))), 4
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "filename=\"train.ogg\""))),
5, MediaFormat.OGG
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "some-form-data; filename=\"audio.flac\""))),
6, MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.aiff\"; filename=\"audio.aiff\""))),
7, MediaFormat.AIFF
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"alien?\"; filename*=UTF-8''%CE%B1%CE%BB%CE%B9%CF%B5%CE%BD.m4a"))),
8, MediaFormat.M4A
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=UTF-8''alien.opus"))),
9, MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Disposition", "form-data; name=\"audio.mp3\"; filename=\"audio.opus\"; filename*=\"UTF-8''alien.opus\""))),
10, MediaFormat.OPUS
)
}
@Test
fun retrieveMediaFormatFromContentTypeHeader() {
val streams = getIncompleteAudioStreams(12)
val wrapper = StreamInfoWrapper(streams, context)
val retrieveMediaFormat = { stream: AudioStream, response: Response ->
StreamInfoWrapper.retrieveMediaFormatFromContentTypeHeader(stream, wrapper, response)
}
val helper = AssertionHelper(streams, wrapper, retrieveMediaFormat)
helper.assertInvalidResponse(getResponse(mapOf(Pair("content-length", "984501"))), 0)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/xyz"))), 1)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 2)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "mp3"))), 3)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/mpeg"))), 4)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "audio/aif"))), 5)
helper.assertInvalidResponse(getResponse(mapOf(Pair("Content-Type", "whatever"))), 6)
helper.assertInvalidResponse(getResponse(mapOf()), 7)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/flac"))), 8, MediaFormat.FLAC
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/wav"))), 9, MediaFormat.WAV
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/opus"))), 10, MediaFormat.OPUS
)
helper.assertValidResponse(
getResponse(mapOf(Pair("Content-Type", "audio/aiff"))), 11, MediaFormat.AIFF
)
}
/** /**
* @return a list of video streams, in which their video only property mirrors the provided * @return a list of video streams, in which their video only property mirrors the provided
* [videoOnly] vararg. * [videoOnly] vararg.
*/ */
private fun getVideoStreams(vararg videoOnly: Boolean) = private fun getVideoStreams(vararg videoOnly: Boolean) =
StreamItemAdapter.StreamInfoWrapper( StreamItemAdapter.StreamSizeWrapper(
videoOnly.map { videoOnly.map {
VideoStream.Builder() VideoStream.Builder()
.setId(Stream.ID_UNKNOWN) .setId(Stream.ID_UNKNOWN)
@@ -264,19 +161,6 @@ class StreamItemAdapterTest {
} }
) )
private fun getIncompleteAudioStreams(size: Int): List<AudioStream> {
val list = ArrayList<AudioStream>(size)
for (i in 1..size) {
list.add(
AudioStream.Builder()
.setId(Stream.ID_UNKNOWN)
.setContent("https://example.com/$i", true)
.build()
)
}
return list
}
/** /**
* Checks whether the item at [position] in the [spinner] has the correct icon visibility when * Checks whether the item at [position] in the [spinner] has the correct icon visibility when
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list). * it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
@@ -312,56 +196,11 @@ class StreamItemAdapterTest {
streams.forEachIndexed { index, stream -> streams.forEachIndexed { index, stream ->
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let { val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
SecondaryStreamHelper( SecondaryStreamHelper(
StreamItemAdapter.StreamInfoWrapper(streams, context), StreamItemAdapter.StreamSizeWrapper(streams, context),
it it
) )
} }
put(index, secondaryStreamHelper) put(index, secondaryStreamHelper)
} }
} }
private fun getResponse(headers: Map<String, String>): Response {
val listHeaders = HashMap<String, List<String>>()
headers.forEach { entry ->
listHeaders[entry.key] = listOf(entry.value)
}
return Response(200, null, listHeaders, "", "")
}
/**
* Helper class for assertion related to extractions of [MediaFormat]s.
*/
class AssertionHelper<T : Stream>(
private val streams: List<T>,
private val wrapper: StreamInfoWrapper<T>,
private val retrieveMediaFormat: (stream: T, response: Response) -> Boolean
) {
/**
* Assert that an invalid response does not result in wrongly extracted [MediaFormat].
*/
fun assertInvalidResponse(
response: Response,
index: Int
) {
assertFalse(
"invalid header returns valid value", retrieveMediaFormat(streams[index], response)
)
assertNull("Media format extracted although stated otherwise", wrapper.getFormat(index))
}
/**
* Assert that a valid response results in correctly extracted and handled [MediaFormat].
*/
fun assertValidResponse(
response: Response,
index: Int,
format: MediaFormat
) {
assertTrue(
"header was not recognized", retrieveMediaFormat(streams[index], response)
)
assertEquals("Wrong media format extracted", format, wrapper.getFormat(index))
}
}
} }

View File

@@ -3,6 +3,7 @@ package org.schabi.newpipe
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.facebook.stetho.Stetho import com.facebook.stetho.Stetho
import com.facebook.stetho.okhttp3.StethoInterceptor import com.facebook.stetho.okhttp3.StethoInterceptor
import leakcanary.AppWatcher
import leakcanary.LeakCanary import leakcanary.LeakCanary
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.schabi.newpipe.extractor.downloader.Downloader import org.schabi.newpipe.extractor.downloader.Downloader
@@ -12,6 +13,8 @@ class DebugApp : App() {
super.onCreate() super.onCreate()
initStetho() initStetho()
// Give each object 10 seconds to be GC'ed, before LeakCanary gets nosy on it
AppWatcher.config = AppWatcher.config.copy(watchDurationMillis = 10000)
LeakCanary.config = LeakCanary.config.copy( LeakCanary.config = LeakCanary.config.copy(
dumpHeap = PreferenceManager dumpHeap = PreferenceManager
.getDefaultSharedPreferences(this).getBoolean( .getDefaultSharedPreferences(this).getBoolean(

View File

@@ -367,7 +367,6 @@
<data android:host="tilvids.com" /> <data android:host="tilvids.com" />
<data android:host="video.lqdn.fr" /> <data android:host="video.lqdn.fr" />
<data android:host="video.ploud.fr" /> <data android:host="video.ploud.fr" />
<data android:host="subscribeto.me" />
<data android:pathPrefix="/videos/" /> <!-- it contains playlists --> <data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
<data android:pathPrefix="/w/" /> <!-- short video URLs --> <data android:pathPrefix="/w/" /> <!-- short video URLs -->

View File

@@ -1,127 +0,0 @@
<!DOCTYPE html>
<html lang="en"><head><title></title><script>
/**
* Factory method to create and load a BotGuardClient instance.
* @param options - Configuration options for the BotGuardClient.
* @returns A promise that resolves to a loaded BotGuardClient instance.
*/
function loadBotGuard(challengeData) {
this.vm = this[challengeData.globalName];
this.program = challengeData.program;
this.vmFunctions = {};
this.syncSnapshotFunction = null;
if (!this.vm)
throw new Error('[BotGuardClient]: VM not found in the global object');
if (!this.vm.a)
throw new Error('[BotGuardClient]: Could not load program');
const vmFunctionsCallback = function (
asyncSnapshotFunction,
shutdownFunction,
passEventFunction,
checkCameraFunction
) {
this.vmFunctions = {
asyncSnapshotFunction: asyncSnapshotFunction,
shutdownFunction: shutdownFunction,
passEventFunction: passEventFunction,
checkCameraFunction: checkCameraFunction
};
};
this.syncSnapshotFunction = this.vm.a(this.program, vmFunctionsCallback, true, this.userInteractionElement, function () {/** no-op */ }, [ [], [] ])[0]
// an asynchronous function runs in the background and it will eventually call
// `vmFunctionsCallback`, however we need to manually tell JavaScript to pass
// control to the things running in the background by interrupting this async
// function in any way, e.g. with a delay of 1ms. The loop is most probably not
// needed but is there just because.
return new Promise(function (resolve, reject) {
i = 0
refreshIntervalId = setInterval(function () {
if (!!this.vmFunctions.asyncSnapshotFunction) {
resolve(this)
clearInterval(refreshIntervalId);
}
if (i >= 10000) {
reject("asyncSnapshotFunction is null even after 10 seconds")
clearInterval(refreshIntervalId);
}
i += 1;
}, 1);
})
}
/**
* Takes a snapshot asynchronously.
* @returns The snapshot result.
* @example
* ```ts
* const result = await botguard.snapshot({
* contentBinding: {
* c: "a=6&a2=10&b=SZWDwKVIuixOp7Y4euGTgwckbJA&c=1729143849&d=1&t=7200&c1a=1&c6a=1&c6b=1&hh=HrMb5mRWTyxGJphDr0nW2Oxonh0_wl2BDqWuLHyeKLo",
* e: "ENGAGEMENT_TYPE_VIDEO_LIKE",
* encryptedVideoId: "P-vC09ZJcnM"
* }
* });
*
* console.log(result);
* ```
*/
function snapshot(args) {
return new Promise(function (resolve, reject) {
if (!this.vmFunctions.asyncSnapshotFunction)
return reject(new Error('[BotGuardClient]: Async snapshot function not found'));
this.vmFunctions.asyncSnapshotFunction(function (response) { resolve(response) }, [
args.contentBinding,
args.signedTimestamp,
args.webPoSignalOutput,
args.skipPrivacyBuffer
]);
});
}
function runBotGuard(challengeData) {
const interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;
if (interpreterJavascript) {
new Function(interpreterJavascript)();
} else throw new Error('Could not load VM');
const webPoSignalOutput = [];
return loadBotGuard({
globalName: challengeData.globalName,
globalObj: this,
program: challengeData.program
}).then(function (botguard) {
return botguard.snapshot({ webPoSignalOutput: webPoSignalOutput })
}).then(function (botguardResponse) {
return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse }
})
}
function obtainPoToken(webPoSignalOutput, integrityToken, identifier) {
const getMinter = webPoSignalOutput[0];
if (!getMinter)
throw new Error('PMD:Undefined');
const mintCallback = getMinter(integrityToken);
if (!(mintCallback instanceof Function))
throw new Error('APF:Failed');
const result = mintCallback(identifier);
if (!result)
throw new Error('YNJ:Undefined');
if (!(result instanceof Uint8Array))
throw new Error('ODM:Invalid');
return result;
}
</script></head><body></body></html>

View File

@@ -25,7 +25,6 @@ import android.view.ViewGroup;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.os.BundleCompat;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.PagerAdapter;
@@ -285,7 +284,7 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
Bundle state = null; Bundle state = null;
if (!mSavedState.isEmpty()) { if (!mSavedState.isEmpty()) {
state = new Bundle(); state = new Bundle();
state.putParcelableArrayList("states", mSavedState); state.putParcelableArray("states", mSavedState.toArray(new Fragment.SavedState[0]));
} }
for (int i = 0; i < mFragments.size(); i++) { for (int i = 0; i < mFragments.size(); i++) {
final Fragment f = mFragments.get(i); final Fragment f = mFragments.get(i);
@@ -312,12 +311,13 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
if (state != null) { if (state != null) {
final Bundle bundle = (Bundle) state; final Bundle bundle = (Bundle) state;
bundle.setClassLoader(loader); bundle.setClassLoader(loader);
final var states = BundleCompat.getParcelableArrayList(bundle, "states", final Parcelable[] fss = bundle.getParcelableArray("states");
Fragment.SavedState.class);
mSavedState.clear(); mSavedState.clear();
mFragments.clear(); mFragments.clear();
if (states != null) { if (fss != null) {
mSavedState.addAll(states); for (final Parcelable parcelable : fss) {
mSavedState.add((Fragment.SavedState) parcelable);
}
} }
final Iterable<String> keys = bundle.keySet(); final Iterable<String> keys = bundle.keySet();
for (final String key : keys) { for (final String key : keys) {

View File

@@ -17,17 +17,12 @@ import org.acra.config.CoreConfigurationBuilder;
import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.ReCaptchaActivity;
import org.schabi.newpipe.extractor.NewPipe; import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.downloader.Downloader; import org.schabi.newpipe.extractor.downloader.Downloader;
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
import org.schabi.newpipe.settings.NewPipeSettings; import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.util.BridgeStateSaverInitializer;
import org.schabi.newpipe.util.Localization; import org.schabi.newpipe.util.Localization;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.ServiceHelper; import org.schabi.newpipe.util.ServiceHelper;
import org.schabi.newpipe.util.StateSaver; import org.schabi.newpipe.util.StateSaver;
import org.schabi.newpipe.util.image.ImageStrategy;
import org.schabi.newpipe.util.image.PicassoHelper;
import org.schabi.newpipe.util.image.PreferredImageQuality;
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl;
import java.io.IOException; import java.io.IOException;
import java.io.InterruptedIOException; import java.io.InterruptedIOException;
@@ -63,8 +58,6 @@ import io.reactivex.rxjava3.plugins.RxJavaPlugins;
public class App extends Application { public class App extends Application {
public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID; public static final String PACKAGE_NAME = BuildConfig.APPLICATION_ID;
private static final String TAG = App.class.toString(); private static final String TAG = App.class.toString();
private boolean isFirstRun = false;
private static App app; private static App app;
@NonNull @NonNull
@@ -90,13 +83,7 @@ public class App extends Application {
return; return;
} }
// check if the last used preference version is set // Initialize settings first because others inits can use its values
// to determine whether this is the first app run
final int lastUsedPrefVersion = PreferenceManager.getDefaultSharedPreferences(this)
.getInt(getString(R.string.last_used_preferences_version), -1);
isFirstRun = lastUsedPrefVersion == -1;
// Initialize settings first because other initializations can use its values
NewPipeSettings.initSettings(this); NewPipeSettings.initSettings(this);
NewPipe.init(getDownloader(), NewPipe.init(getDownloader(),
@@ -104,7 +91,6 @@ public class App extends Application {
Localization.getPreferredContentCountry(this)); Localization.getPreferredContentCountry(this));
Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext())); Localization.initPrettyTime(Localization.resolvePrettyTime(getApplicationContext()));
BridgeStateSaverInitializer.init(this);
StateSaver.init(this); StateSaver.init(this);
initNotificationChannels(); initNotificationChannels();
@@ -113,15 +99,12 @@ public class App extends Application {
// Initialize image loader // Initialize image loader
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
PicassoHelper.init(this); PicassoHelper.init(this);
ImageStrategy.setPreferredImageQuality(PreferredImageQuality.fromPreferenceKey(this, PicassoHelper.setShouldLoadImages(
prefs.getString(getString(R.string.image_quality_key), prefs.getBoolean(getString(R.string.download_thumbnail_key), true));
getString(R.string.image_quality_default))));
PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG PicassoHelper.setIndicatorsEnabled(MainActivity.DEBUG
&& prefs.getBoolean(getString(R.string.show_image_indicators_key), false)); && prefs.getBoolean(getString(R.string.show_image_indicators_key), false));
configureRxJavaErrorHandler(); configureRxJavaErrorHandler();
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl.INSTANCE);
} }
@Override @Override
@@ -269,7 +252,4 @@ public class App extends Application {
return false; return false;
} }
public boolean isFirstRun() {
return isFirstRun;
}
} }

View File

@@ -10,9 +10,9 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import com.evernote.android.state.State; import icepick.Icepick;
import com.livefront.bridge.Bridge; import icepick.State;
import leakcanary.AppWatcher;
public abstract class BaseFragment extends Fragment { public abstract class BaseFragment extends Fragment {
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()); protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
@@ -49,7 +49,7 @@ public abstract class BaseFragment extends Fragment {
+ "savedInstanceState = [" + savedInstanceState + "]"); + "savedInstanceState = [" + savedInstanceState + "]");
} }
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bridge.restoreInstanceState(this, savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
onRestoreInstanceState(savedInstanceState); onRestoreInstanceState(savedInstanceState);
} }
@@ -71,39 +71,26 @@ public abstract class BaseFragment extends Fragment {
@Override @Override
public void onSaveInstanceState(@NonNull final Bundle outState) { public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Bridge.saveInstanceState(this, outState); Icepick.saveInstanceState(this, outState);
} }
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) { protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
} }
@Override
public void onDestroy() {
super.onDestroy();
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
}
/*////////////////////////////////////////////////////////////////////////// /*//////////////////////////////////////////////////////////////////////////
// Init // Init
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/
/**
* This method is called in {@link #onViewCreated(View, Bundle)} to initialize the views.
*
* <p>
* {@link #initListeners()} is called after this method to initialize the corresponding
* listeners.
* </p>
* @param rootView The inflated view for this fragment
* (provided by {@link #onViewCreated(View, Bundle)})
* @param savedInstanceState The saved state of this fragment
* (provided by {@link #onViewCreated(View, Bundle)})
*/
protected void initViews(final View rootView, final Bundle savedInstanceState) { protected void initViews(final View rootView, final Bundle savedInstanceState) {
} }
/**
* Initialize the listeners for this fragment.
*
* <p>
* This method is called after {@link #initViews(View, Bundle)}
* in {@link #onViewCreated(View, Bundle)}.
* </p>
*/
protected void initListeners() { protected void initListeners() {
} }
@@ -121,20 +108,9 @@ public abstract class BaseFragment extends Fragment {
} }
} }
/**
* Finds the root fragment by looping through all of the parent fragments. The root fragment
* is supposed to be {@link org.schabi.newpipe.fragments.MainFragment}, and is the fragment that
* handles keeping the backstack of opened fragments in NewPipe, and also the player bottom
* sheet. This function therefore returns the fragment manager of said fragment.
*
* @return the fragment manager of the root fragment, i.e.
* {@link org.schabi.newpipe.fragments.MainFragment}
*/
protected FragmentManager getFM() { protected FragmentManager getFM() {
Fragment current = this; return getParentFragment() == null
while (current.getParentFragment() != null) { ? getFragmentManager()
current = current.getParentFragment(); : getParentFragment().getFragmentManager();
}
return current.getFragmentManager();
} }
} }

View File

@@ -29,7 +29,7 @@ import okhttp3.ResponseBody;
public final class DownloaderImpl extends Downloader { public final class DownloaderImpl extends Downloader {
public static final String USER_AGENT = public static final String USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0"; "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY = public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
"youtube_restricted_mode_key"; "youtube_restricted_mode_key";
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000"; public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
@@ -137,8 +137,7 @@ public final class DownloaderImpl extends Downloader {
} }
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder() final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
.method(httpMethod, requestBody) .method(httpMethod, requestBody).url(url)
.url(url)
.addHeader("User-Agent", USER_AGENT); .addHeader("User-Agent", USER_AGENT);
final String cookies = getCookies(url); final String cookies = getCookies(url);
@@ -146,33 +145,38 @@ public final class DownloaderImpl extends Downloader {
requestBuilder.addHeader("Cookie", cookies); requestBuilder.addHeader("Cookie", cookies);
} }
headers.forEach((headerName, headerValueList) -> { for (final Map.Entry<String, List<String>> pair : headers.entrySet()) {
requestBuilder.removeHeader(headerName); final String headerName = pair.getKey();
headerValueList.forEach(headerValue -> final List<String> headerValueList = pair.getValue();
requestBuilder.addHeader(headerName, headerValue));
});
try ( if (headerValueList.size() > 1) {
okhttp3.Response response = client.newCall(requestBuilder.build()).execute() requestBuilder.removeHeader(headerName);
) { for (final String headerValue : headerValueList) {
if (response.code() == 429) { requestBuilder.addHeader(headerName, headerValue);
throw new ReCaptchaException("reCaptcha Challenge requested", url);
}
String responseBodyToReturn = null;
try (ResponseBody body = response.body()) {
if (body != null) {
responseBodyToReturn = body.string();
} }
} else if (headerValueList.size() == 1) {
requestBuilder.header(headerName, headerValueList.get(0));
} }
final String latestUrl = response.request().url().toString();
return new Response(
response.code(),
response.message(),
response.headers().toMultimap(),
responseBodyToReturn,
latestUrl);
} }
final okhttp3.Response response = client.newCall(requestBuilder.build()).execute();
if (response.code() == 429) {
response.close();
throw new ReCaptchaException("reCaptcha Challenge requested", url);
}
final ResponseBody body = response.body();
String responseBodyToReturn = null;
if (body != null) {
responseBodyToReturn = body.string();
}
final String latestUrl = response.request().url().toString();
return new Response(response.code(), response.message(), response.headers().toMultimap(),
responseBodyToReturn, latestUrl);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,6 @@ 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_4_5;
import static org.schabi.newpipe.database.Migrations.MIGRATION_5_6; 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_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.content.Context;
import android.database.Cursor; import android.database.Cursor;
@@ -29,7 +27,7 @@ public final class NewPipeDatabase {
return Room return Room
.databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME) .databaseBuilder(context.getApplicationContext(), AppDatabase.class, DATABASE_NAME)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, .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) MIGRATION_5_6, MIGRATION_6_7)
.build(); .build();
} }

View File

@@ -20,7 +20,9 @@ import com.grack.nanojson.JsonParser
import com.grack.nanojson.JsonParserException import com.grack.nanojson.JsonParserException
import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.util.ReleaseVersionUtil import org.schabi.newpipe.util.ReleaseVersionUtil.coerceUpdateCheckExpiry
import org.schabi.newpipe.util.ReleaseVersionUtil.isLastUpdateCheckExpired
import org.schabi.newpipe.util.ReleaseVersionUtil.isReleaseApk
import java.io.IOException import java.io.IOException
class NewVersionWorker( class NewVersionWorker(
@@ -82,7 +84,7 @@ class NewVersionWorker(
@Throws(IOException::class, ReCaptchaException::class) @Throws(IOException::class, ReCaptchaException::class)
private fun checkNewVersion() { private fun checkNewVersion() {
// Check if the current apk is a github one or not. // Check if the current apk is a github one or not.
if (!ReleaseVersionUtil.isReleaseApk) { if (!isReleaseApk()) {
return return
} }
@@ -91,7 +93,7 @@ class NewVersionWorker(
// Check if the last request has happened a certain time ago // Check if the last request has happened a certain time ago
// to reduce the number of API requests. // to reduce the number of API requests.
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0) val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) { if (!isLastUpdateCheckExpired(expiry)) {
return return
} }
} }
@@ -106,7 +108,7 @@ class NewVersionWorker(
try { try {
// Store a timestamp which needs to be exceeded, // Store a timestamp which needs to be exceeded,
// before a new request to the API is made. // before a new request to the API is made.
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires")) val newExpiry = coerceUpdateCheckExpiry(response.getHeader("expires"))
prefs.edit { prefs.edit {
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry) putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
} }
@@ -118,13 +120,13 @@ class NewVersionWorker(
// Parse the json from the response. // Parse the json from the response.
try { try {
val newpipeVersionInfo = JsonParser.`object`() val githubStableObject = JsonParser.`object`()
.from(response.responseBody()).getObject("flavors") .from(response.responseBody()).getObject("flavors")
.getObject("newpipe") .getObject("github").getObject("stable")
val versionName = newpipeVersionInfo.getString("version") val versionName = githubStableObject.getString("version")
val versionCode = newpipeVersionInfo.getInt("version_code") val versionCode = githubStableObject.getInt("version_code")
val apkLocationUrl = newpipeVersionInfo.getString("apk") val apkLocationUrl = githubStableObject.getString("apk")
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode) compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
} catch (e: JsonParserException) { } catch (e: JsonParserException) {
// Most likely something is wrong in data received from NEWPIPE_API_URL. // Most likely something is wrong in data received from NEWPIPE_API_URL.

View File

@@ -75,7 +75,7 @@ public final class QueueItemMenuUtil {
return true; return true;
case R.id.menu_item_share: case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(), shareText(context, item.getTitle(), item.getUrl(),
item.getThumbnails()); item.getThumbnailUrl());
return true; return true;
case R.id.menu_item_download: case R.id.menu_item_download:
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(), fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),

View File

@@ -41,14 +41,10 @@ import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding; import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding; import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog; import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.download.LoadingDialog;
import org.schabi.newpipe.error.ErrorInfo; import org.schabi.newpipe.error.ErrorInfo;
import org.schabi.newpipe.error.ErrorUtil; import org.schabi.newpipe.error.ErrorUtil;
import org.schabi.newpipe.error.ReCaptchaActivity; import org.schabi.newpipe.error.ReCaptchaActivity;
@@ -68,7 +64,6 @@ import org.schabi.newpipe.extractor.exceptions.PrivateContentException;
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException; import org.schabi.newpipe.extractor.exceptions.SoundCloudGoPlusContentException;
import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentException; 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.playlist.PlaylistInfo;
import org.schabi.newpipe.extractor.stream.StreamInfo; import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.ktx.ExceptionUtils; import org.schabi.newpipe.ktx.ExceptionUtils;
@@ -76,11 +71,10 @@ import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.PlayerType; import org.schabi.newpipe.player.PlayerType;
import org.schabi.newpipe.player.helper.PlayerHelper; import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder; import org.schabi.newpipe.player.helper.PlayerHolder;
import org.schabi.newpipe.player.playqueue.ChannelTabPlayQueue; import org.schabi.newpipe.player.playqueue.ChannelPlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueue; import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue; import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue;
import org.schabi.newpipe.player.playqueue.SinglePlayQueue; import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ChannelTabHelper;
import org.schabi.newpipe.util.Constants; import org.schabi.newpipe.util.Constants;
import org.schabi.newpipe.util.DeviceUtils; import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ExtractorHelper; import org.schabi.newpipe.util.ExtractorHelper;
@@ -101,6 +95,8 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import icepick.Icepick;
import icepick.State;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable; import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.core.Single;
@@ -153,7 +149,7 @@ public class RouterActivity extends AppCompatActivity {
getWindow().setAttributes(params); getWindow().setAttributes(params);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Bridge.restoreInstanceState(this, savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState);
// FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates // FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
// We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments // We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
@@ -198,7 +194,7 @@ public class RouterActivity extends AppCompatActivity {
@Override @Override
protected void onSaveInstanceState(@NonNull final Bundle outState) { protected void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
Bridge.saveInstanceState(this, outState); Icepick.saveInstanceState(this, outState);
} }
@Override @Override
@@ -793,10 +789,10 @@ public class RouterActivity extends AppCompatActivity {
} }
} }
}, () -> }, () -> {
// this branch is executed if there is no activity context // this branch is executed if there is no activity context
inFlight(false) inFlight(false);
); });
} }
<T> Single<T> pleaseWait(final Single<T> single) { <T> Single<T> pleaseWait(final Single<T> single) {
@@ -816,24 +812,19 @@ public class RouterActivity extends AppCompatActivity {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
private void openDownloadDialog(final int currentServiceId, final String currentUrl) { private void openDownloadDialog(final int currentServiceId, final String currentUrl) {
inFlight(true); inFlight(true);
final LoadingDialog loadingDialog = new LoadingDialog(R.string.loading_metadata_title);
loadingDialog.show(getParentFragmentManager(), "loadingDialog");
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.compose(this::pleaseWait) .compose(this::pleaseWait)
.subscribe(result -> .subscribe(result ->
runOnVisible(ctx -> { runOnVisible(ctx -> {
loadingDialog.dismiss();
final FragmentManager fm = ctx.getSupportFragmentManager(); final FragmentManager fm = ctx.getSupportFragmentManager();
final DownloadDialog downloadDialog = new DownloadDialog(ctx, result); final DownloadDialog downloadDialog = new DownloadDialog(ctx, result);
// dismiss listener to be handled by FragmentManager // dismiss listener to be handled by FragmentManager
downloadDialog.show(fm, "downloadDialog"); downloadDialog.show(fm, "downloadDialog");
} }
), throwable -> runOnVisible(ctx -> { ), throwable -> runOnVisible(ctx ->
loadingDialog.dismiss(); ((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl))));
((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl);
})));
} }
private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) { private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) {
@@ -1025,16 +1016,7 @@ public class RouterActivity extends AppCompatActivity {
} }
playQueue = new SinglePlayQueue((StreamInfo) info); playQueue = new SinglePlayQueue((StreamInfo) info);
} else if (info instanceof ChannelInfo) { } else if (info instanceof ChannelInfo) {
final Optional<ListLinkHandler> playableTab = ((ChannelInfo) info).getTabs() playQueue = new ChannelPlayQueue((ChannelInfo) info);
.stream()
.filter(ChannelTabHelper::isStreamsTab)
.findFirst();
if (playableTab.isPresent()) {
playQueue = new ChannelTabPlayQueue(info.getServiceId(), playableTab.get());
} else {
return; // there is no playable tab
}
} else if (info instanceof PlaylistInfo) { } else if (info instanceof PlaylistInfo) {
playQueue = new PlaylistPlayQueue((PlaylistInfo) info); playQueue = new PlaylistPlayQueue((PlaylistInfo) info);
} else { } else {

Some files were not shown because too many files have changed in this diff Show More