Compare commits
No commits in common. "main" and "rollback/vc18-back-to-NPE" have entirely different histories.
main
...
rollback/v
1081 changed files with 170975 additions and 12963 deletions
|
|
@ -1,129 +0,0 @@
|
||||||
# Straw APK — build on Forgejo Actions, and (gated on a green build) publish to
|
|
||||||
# fdroid.sulkta.com so the in-app updater picks it up.
|
|
||||||
#
|
|
||||||
# Runs in the dedicated git.sulkta.com/sulkta-infra/straw-build image (Android
|
|
||||||
# SDK/NDK + Rust + cargo-ndk, see ci/Dockerfile). The signing key comes from
|
|
||||||
# the STRAW_SIGNING_KEYSTORE_B64 secret (the same androiddebugkey the whole
|
|
||||||
# series is signed with — vaulted as "Sulkta — Straw fdroid signing keystore").
|
|
||||||
#
|
|
||||||
# Publish path mirrors scripts/publish-fdroid.sh: drop the APK into the Lucy
|
|
||||||
# fdroid repo + re-sign the index (both via the host docker socket, keystore
|
|
||||||
# never leaves Lucy), then stream the signed repo to Rackham web168 over a
|
|
||||||
# scoped, forced-command deploy key (STRAW_FDROID_RACKHAM_KEY).
|
|
||||||
name: build-apk
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'strawApp/**'
|
|
||||||
- 'rust/**'
|
|
||||||
- 'buildSrc/**'
|
|
||||||
- 'gradle/**'
|
|
||||||
- 'gradlew'
|
|
||||||
- '*.gradle.kts'
|
|
||||||
- 'gradle.properties'
|
|
||||||
- 'ci/Dockerfile'
|
|
||||||
- '.forgejo/workflows/build.yml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-publish:
|
|
||||||
runs-on: lucy-rust
|
|
||||||
# Run every step with bash, not the runner's default dash — the steps use
|
|
||||||
# `set -euo pipefail` (pipefail is a bashism; dash errors "Illegal option").
|
|
||||||
# The straw-build image ships /usr/bin/bash.
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
container:
|
|
||||||
image: git.sulkta.com/sulkta-infra/straw-build:latest
|
|
||||||
steps:
|
|
||||||
# We clone with plain git instead of actions/checkout@v4: that action is
|
|
||||||
# a Node action, and the straw-build job container ships the Android +
|
|
||||||
# Rust toolchain but NOT node — so checkout@v4 dies with
|
|
||||||
# `exec: "node": not found`. git is in the image, both repos are public,
|
|
||||||
# and a shell clone also sidesteps the runner's flaky data.forgejo.org
|
|
||||||
# action fetch. strawcore must be a SIBLING of straw because
|
|
||||||
# rust/strawcore depends on it via `path = "../../../strawcore"`.
|
|
||||||
- name: Checkout straw + strawcore (sibling, no JS actions)
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
git clone https://git.sulkta.com/Sulkta-OSS/straw.git straw
|
|
||||||
git -C straw checkout --detach "${{ github.sha }}"
|
|
||||||
git clone --depth 1 https://git.sulkta.com/Sulkta-OSS/strawcore.git strawcore
|
|
||||||
echo "straw: $(git -C straw rev-parse --short HEAD)"
|
|
||||||
echo "strawcore: $(git -C strawcore rev-parse --short HEAD)"
|
|
||||||
|
|
||||||
- name: Decode signing keystore
|
|
||||||
env:
|
|
||||||
KS_B64: ${{ secrets.STRAW_SIGNING_KEYSTORE_B64 }}
|
|
||||||
run: echo "$KS_B64" | base64 -d > "$GITHUB_WORKSPACE/straw.keystore"
|
|
||||||
|
|
||||||
- name: Assemble debug APK
|
|
||||||
working-directory: straw
|
|
||||||
env:
|
|
||||||
STRAW_KEYSTORE_FILE: ${{ github.workspace }}/straw.keystore
|
|
||||||
STRAW_KEYSTORE_PASS: android
|
|
||||||
STRAW_KEY_ALIAS: androiddebugkey
|
|
||||||
STRAW_KEY_PASS: android
|
|
||||||
# Keep the 4-ABI cross-compile off the container rootfs.
|
|
||||||
CARGO_TARGET_DIR: ${{ github.workspace }}/cargo-target
|
|
||||||
# ionice idle class + low nice so the build yields disk/CPU to the
|
|
||||||
# live DBs on Lucy — a 5GB build I/O-starved the MAS Postgres
|
|
||||||
# checkpoint and dropped Matrix for ~2min on 2026-06-19.
|
|
||||||
run: ionice -c3 nice -n 19 ./gradlew :strawApp:assembleDebug --no-daemon --stacktrace
|
|
||||||
|
|
||||||
- name: Verify signer + stage APK
|
|
||||||
working-directory: straw
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
VC=$(grep STRAW_VERSION_CODE buildSrc/src/main/kotlin/ProjectConfig.kt | grep -o '[0-9]\+')
|
|
||||||
APK=$(ls strawApp/build/outputs/apk/debug/*.apk | head -1)
|
|
||||||
NAME="com.sulkta.straw.debug_${VC}.apk"
|
|
||||||
cp "$APK" "$GITHUB_WORKSPACE/$NAME"
|
|
||||||
echo "Built vc=$VC -> $NAME"
|
|
||||||
# The whole series is signed with SHA-1 bb9ca96b...; fail loudly if a
|
|
||||||
# build ever produces a different signer (would break in-place updates).
|
|
||||||
# Pick whatever build-tools the image actually ships (36 today, not 34).
|
|
||||||
# apksigner is a shell wrapper that needs `java` on PATH; the image
|
|
||||||
# sets JAVA_HOME but doesn't put its bin on PATH for run steps (gradle
|
|
||||||
# uses JAVA_HOME directly, so the build itself is fine).
|
|
||||||
export PATH="$JAVA_HOME/bin:$PATH"
|
|
||||||
APKSIGNER=$(ls "$ANDROID_HOME"/build-tools/*/apksigner | sort -V | tail -1)
|
|
||||||
FP=$("$APKSIGNER" verify --print-certs "$APK" | grep -i 'SHA-1' | grep -o '[0-9a-f]\{40\}')
|
|
||||||
echo "signer SHA-1: $FP"
|
|
||||||
if [ "$FP" != "bb9ca96b10ebbc1ac48e037a21f350415d18915f" ]; then
|
|
||||||
echo "::error::APK signer $FP != canonical key — refusing to publish"; exit 1
|
|
||||||
fi
|
|
||||||
echo "STRAW_VC=$VC" >> "$GITHUB_ENV"
|
|
||||||
echo "STRAW_APK=$NAME" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
# ---- Publish: only on a real push to main, AFTER the build above passed ----
|
|
||||||
- name: Publish to fdroid (Lucy repo + index re-sign, via host docker socket)
|
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
FDROID=git.sulkta.com/sulkta-infra/fdroid-sulkta:latest
|
|
||||||
# Stream the APK into the host-mounted fdroid repo (helper runs as the
|
|
||||||
# fdroid uid so ownership matches).
|
|
||||||
docker run --rm -i -u 1000:1000 -v /mnt/cache/appdata/fdroid-sulkta/repo:/repo \
|
|
||||||
--entrypoint sh "$FDROID" -c "cat > /repo/${STRAW_APK}" < "$GITHUB_WORKSPACE/${STRAW_APK}"
|
|
||||||
# Re-sign the index (the index keystore lives only on Lucy).
|
|
||||||
docker run --rm -u 1000:1000 -v /mnt/cache/appdata/fdroid-sulkta:/repo "$FDROID" update
|
|
||||||
|
|
||||||
- name: Publish to Rackham (rsync signed repo to web168)
|
|
||||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
||||||
env:
|
|
||||||
RACKHAM_KEY: ${{ secrets.STRAW_FDROID_RACKHAM_KEY }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
mkdir -p ~/.ssh && echo "$RACKHAM_KEY" > ~/.ssh/id_deploy && chmod 600 ~/.ssh/id_deploy
|
|
||||||
ssh-keyscan -H 142.44.213.229 >> ~/.ssh/known_hosts 2>/dev/null
|
|
||||||
FDROID=git.sulkta.com/sulkta-infra/fdroid-sulkta:latest
|
|
||||||
# tar the signed repo out of the host-mounted volume and stream it to
|
|
||||||
# the forced-command deploy key on Rackham (which untars + sudo-rsyncs).
|
|
||||||
docker run --rm -u 1000:1000 -v /mnt/cache/appdata/fdroid-sulkta:/src \
|
|
||||||
--entrypoint sh "$FDROID" -c 'tar c -C /src repo' \
|
|
||||||
| ssh -i ~/.ssh/id_deploy kayos@142.44.213.229
|
|
||||||
echo "Published vc=${STRAW_VC} to fdroid.sulkta.com"
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
# .forgejo/workflows/gitleaks.yml
|
|
||||||
#
|
|
||||||
# Sulkta canonical gitleaks workflow. Drop a copy into every public repo at
|
|
||||||
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
|
|
||||||
# (task #295).
|
|
||||||
#
|
|
||||||
# Pairs with the pre-receive hook installed on every bare repo — that one is
|
|
||||||
# the strict enforcement layer (rejects the push); this one provides the
|
|
||||||
# per-PR red ✗ that branch-protection rules can require before merge.
|
|
||||||
#
|
|
||||||
# Layer 1 (this workflow): visible per-PR status, can be a required check.
|
|
||||||
# Layer 2 (pre-receive hook): strict enforcement at the server.
|
|
||||||
# Layer 3 (johnny5 cron sweep): nightly full-history sweep across all repos.
|
|
||||||
|
|
||||||
name: gitleaks
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
scan:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
# Full history — gitleaks needs depth to scan a commit range.
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: install gitleaks
|
|
||||||
run: |
|
|
||||||
curl -sSL -o gl.tar.gz \
|
|
||||||
https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz
|
|
||||||
tar xzf gl.tar.gz gitleaks
|
|
||||||
chmod +x gitleaks
|
|
||||||
./gitleaks version
|
|
||||||
|
|
||||||
- name: scan
|
|
||||||
run: |
|
|
||||||
./gitleaks detect --source . --no-banner --redact --verbose
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
# gitleaks config — straw
|
|
||||||
#
|
|
||||||
# Straw is a YouTube Android client. Patterns flagged:
|
|
||||||
# - SharedPreferences key constants — identifier strings, not credentials
|
|
||||||
# - GOOGLE_API_KEY in PoTokenWebView.kt — the InnerTube public API key
|
|
||||||
# every YouTube client (web, Android, iOS, NewPipe, all forks) ships
|
|
||||||
# hardcoded. Public-by-design; YouTube enforces auth via other channels.
|
|
||||||
|
|
||||||
[extend]
|
|
||||||
useDefault = true
|
|
||||||
|
|
||||||
[allowlist]
|
|
||||||
description = "Public InnerTube API key + SharedPreferences key-name constants"
|
|
||||||
regexTarget = "line"
|
|
||||||
regexes = [
|
|
||||||
# InnerTube hardcoded key, public on every YouTube client
|
|
||||||
'''GOOGLE_API_KEY\s*=\s*"AIza[A-Za-z0-9_-]{35}"''',
|
|
||||||
# Any const val whose name contains KEY — these are SharedPreferences
|
|
||||||
# / request-tag identifier strings, never credentials
|
|
||||||
'''(private\s+)?const\s+val\s+\w*KEY\w*\s*=\s*"''',
|
|
||||||
]
|
|
||||||
186
README.md
186
README.md
|
|
@ -1,77 +1,153 @@
|
||||||
# Straw
|
<h3 align="center">We are <i>rewriting</i> large chunks of the codebase, to bring about <a href="https://newpipe.net/blog/pinned/announcement/newpipe-0.27.6-rewrite-team-states/#the-refactor">a modern and stable NewPipe</a>! You can download nightly builds <a href="https://github.com/TeamNewPipe/NewPipe-refactor-nightly/releases">here</a>.</h3>
|
||||||
|
<h4 align="center">Please work on the <code>refactor</code> branch if you want to contribute <i>new features</i>. The current codebase is in maintenance mode and will only receive <i>bugfixes</i>.</h4>
|
||||||
|
|
||||||
A Sulkta fork of [NewPipe](https://github.com/TeamNewPipe/NewPipe). Android YouTube
|
<p align="center"><a href="https://newpipe.net"><img src="assets/new_pipe_icon_5.png" width="150"></a></p>
|
||||||
client, Compose UI, Media3 player, with [SponsorBlock](https://sponsor.ajay.app/)
|
<h2 align="center"><b>NewPipe</b></h2>
|
||||||
and [Return YouTube Dislike](https://returnyoutubedislike.com/) baked in.
|
<h4 align="center">A libre lightweight streaming front-end for Android.</h4>
|
||||||
|
|
||||||
The extractor is `strawcore`, a Rust port of NewPipeExtractor exposed to Kotlin
|
<p align="center"><a href="https://f-droid.org/packages/org.schabi.newpipe/"><img src="https://fdroid.gitlab.io/artwork/badge/get-it-on-en.svg" alt="Get it on F-Droid" width=206/></a></p>
|
||||||
via UniFFI. No InnerTube/JS deobf code path lives on the JVM anymore.
|
|
||||||
|
|
||||||
## Install
|
<p align="center">
|
||||||
|
<a href="https://github.com/TeamNewPipe/NewPipe/releases" alt="GitHub NewPipe releases"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe.svg" ></a>
|
||||||
|
<a href="https://github.com/TeamNewPipe/NewPipe-nightly/releases" alt="GitHub NewPipe nightly releases"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe-nightly.svg?labelColor=purple&label=dev%20nightly"></a>
|
||||||
|
<a href="https://github.com/TeamNewPipe/NewPipe-refactor-nightly/releases" alt="GitHub NewPipe refactor nightly releases"><img src="https://img.shields.io/github/release/TeamNewPipe/NewPipe-refactor-nightly.svg?labelColor=purple&label=refactor%20nightly"></a>
|
||||||
|
<a href="https://www.gnu.org/licenses/gpl-3.0" alt="License: GPLv3"><img src="https://img.shields.io/badge/License-GPL%20v3-blue.svg"></a>
|
||||||
|
<a href="https://github.com/TeamNewPipe/NewPipe/actions" alt="Build Status"><img src="https://github.com/TeamNewPipe/NewPipe/actions/workflows/ci.yml/badge.svg?branch=dev&event=push"></a>
|
||||||
|
<a href="https://hosted.weblate.org/engage/newpipe/" alt="Translation Status"><img src="https://hosted.weblate.org/widgets/newpipe/-/svg-badge.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
F-Droid repo: <https://fdroid.sulkta.com/fdroid/repo>
|
<p align="center">
|
||||||
|
<a href="https://web.libera.chat/#newpipe" alt="IRC channel: #newpipe"><img src="https://img.shields.io/badge/IRC%20chat-%23newpipe-brightgreen.svg"></a>
|
||||||
|
<a href="https://matrix.to/#/#newpipe:matrix.newpipe-ev.de" alt="Matrix channel: #newpipe"><img src="https://img.shields.io/badge/Matrix%20chat-%23newpipe-blue"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Add the repo in your F-Droid client of choice, then install Straw.
|
<hr>
|
||||||
|
<p align="center"><a href="#screenshots">Screenshots</a> • <a href="#supported-services">Supported Services</a> • <a href="#description">Description</a> • <a href="#features">Features</a> • <a href="#installation-and-updates">Installation and updates</a> • <a href="#contribution">Contribution</a> • <a href="#donate">Donate</a> • <a href="#license">License</a></p>
|
||||||
|
<p align="center"><a href="https://newpipe.net">Website</a> • <a href="https://newpipe.net/blog/">Blog</a> • <a href="https://newpipe.net/FAQ/">FAQ</a> • <a href="https://newpipe.net/press/">Press</a></p>
|
||||||
|
<hr>
|
||||||
|
|
||||||
The app also self-updates from the same repo when an APK lands there with a
|
*Read this document in other languages: [Deutsch](doc/README.de.md), [English](README.md), [Español](doc/README.es.md), [Français](doc/README.fr.md), [हिन्दी](doc/README.hi.md), [Italiano](doc/README.it.md), [한국어](doc/README.ko.md), [Português Brasil](doc/README.pt_BR.md), [Polski](doc/README.pl.md), [ਪੰਜਾਬੀ ](doc/README.pa.md), [日本語](doc/README.ja.md), [Română](doc/README.ro.md), [Soomaali](doc/README.so.md), [Türkçe](doc/README.tr.md), [正體中文](doc/README.zh_TW.md), [অসমীয়া](doc/README.asm.md), [Српски](doc/README.sr.md), [العربية](README.ar.md)*
|
||||||
higher `versionCode`.
|
|
||||||
|
|
||||||
## What's in
|
> [!warning]
|
||||||
|
> <b>THIS APP IS IN BETA, SO YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE IN OUR GITHUB REPOSITORY BY FILLING OUT THE ISSUE TEMPLATE.</b>
|
||||||
|
>
|
||||||
|
> <b>PUTTING NEWPIPE, OR ANY FORK OF IT, INTO THE GOOGLE PLAY STORE VIOLATES THEIR TERMS AND CONDITIONS.</b>
|
||||||
|
|
||||||
- Search, video detail, channel pages, playlists
|
## Screenshots
|
||||||
- Inline player + fullscreen + minibar + background audio + PiP
|
|
||||||
- Media3 ExoPlayer (DASH / HLS / progressive / merged DASH chunks)
|
|
||||||
- SponsorBlock auto-skip (categories user-toggleable)
|
|
||||||
- Return YouTube Dislike on video detail
|
|
||||||
- RSS-based subscription feed (fast — ~1s for 50 subs)
|
|
||||||
- Hide-shorts / hide-paid / hide-age-restricted feed filters
|
|
||||||
- Resume positions + watch history + search history
|
|
||||||
- Local playlists, downloads (video + audio)
|
|
||||||
- NewPipe-format settings import (subs + playlists + history)
|
|
||||||
|
|
||||||
## What's out (on purpose)
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/00.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/00.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/01.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/01.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/02.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/02.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/03.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/03.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/04.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/04.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/05.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/05.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/06.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/06.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/07.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/07.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/08.png" width=160>](fastlane/metadata/android/en-US/images/phoneScreenshots/08.png)
|
||||||
|
<br/><br/>
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/09.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/09.png)
|
||||||
|
[<img src="fastlane/metadata/android/en-US/images/tenInchScreenshots/10.png" width=405>](fastlane/metadata/android/en-US/images/tenInchScreenshots/10.png)
|
||||||
|
|
||||||
- Trending / algorithmic feeds. Subscriptions only.
|
### Supported Services
|
||||||
- iOS / desktop targets. Android-only for now.
|
|
||||||
- Google Play Services anything.
|
|
||||||
|
|
||||||
## Layout
|
NewPipe currently supports these services:
|
||||||
|
|
||||||
|
<!-- We link to the service websites separately to avoid people accidentally opening a website they didn't want to. -->
|
||||||
|
* YouTube ([website](https://www.youtube.com/)) and YouTube Music ([website](https://music.youtube.com/)) ([wiki](https://en.wikipedia.org/wiki/YouTube))
|
||||||
|
* PeerTube ([website](https://joinpeertube.org/)) and all its instances (open the website to know what that means!) ([wiki](https://en.wikipedia.org/wiki/PeerTube))
|
||||||
|
* Bandcamp ([website](https://bandcamp.com/)) ([wiki](https://en.wikipedia.org/wiki/Bandcamp))
|
||||||
|
* SoundCloud ([website](https://soundcloud.com/)) ([wiki](https://en.wikipedia.org/wiki/SoundCloud))
|
||||||
|
* media.ccc.de ([website](https://media.ccc.de/)) ([wiki](https://en.wikipedia.org/wiki/Chaos_Computer_Club))
|
||||||
|
|
||||||
|
As you can see, NewPipe supports multiple video and audio services. Though it started off with YouTube, other people have added more services over the years, making NewPipe more and more versatile!
|
||||||
|
|
||||||
|
Partially due to circumstance, and partially due to its popularity, YouTube is the best supported out of these services. If you use or are familiar with any of these other services, please help us improve support for them! We're looking for maintainers for SoundCloud and PeerTube.
|
||||||
|
|
||||||
|
If you intend to add a new service, please get in touch with us first! Our [docs](https://teamnewpipe.github.io/documentation/) provide more information on how a new service can be added to the app and to the [NewPipe Extractor](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
NewPipe works by fetching the required data from the official API (e.g. PeerTube) of the service you're using. If the official API is restricted (e.g. YouTube) for our purposes, or is proprietary, the app parses the website or uses an internal API instead. This means that you don't need an account on any service to use NewPipe.
|
||||||
|
|
||||||
|
Also, since they are free and open source software, neither the app nor the Extractor use any proprietary libraries or frameworks, such as Google Play Services. This means you can use NewPipe on devices or custom ROMs that do not have Google apps installed.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Watch videos at resolutions up to 4K
|
||||||
|
* Listen to audio in the background, only loading the audio stream to save data
|
||||||
|
* Popup mode (floating player, aka Picture-in-Picture)
|
||||||
|
* Watch live streams
|
||||||
|
* Show/hide subtitles/closed captions
|
||||||
|
* Search videos and audios (on YouTube, you can specify the content language as well)
|
||||||
|
* Enqueue videos (and optionally save them as local playlists)
|
||||||
|
* Show/hide general information about videos (such as description and tags)
|
||||||
|
* Show/hide next/related videos
|
||||||
|
* Show/hide comments
|
||||||
|
* Search videos, audios, channels, playlists and albums
|
||||||
|
* Browse videos and audios within a channel
|
||||||
|
* Subscribe to channels (yes, without logging into any account!)
|
||||||
|
* Get notifications about new videos from channels you're subscribed to
|
||||||
|
* Create and edit channel groups (for easier browsing and management)
|
||||||
|
* Browse video feeds generated from your channel groups
|
||||||
|
* View and search your watch history
|
||||||
|
* Search and watch playlists (these are remote playlists, which means they're fetched from the service you're browsing)
|
||||||
|
* Create and edit local playlists (these are created and saved within the app, and have nothing to do with any service)
|
||||||
|
* Download videos/audios/subtitles (closed captions)
|
||||||
|
* Open in Kodi
|
||||||
|
* Watch/Block age-restricted material
|
||||||
|
|
||||||
|
<!-- Hidden span to keep old links compatible. You should remove this span if you're translating the README into another language.-->
|
||||||
|
<span id="updates"></span>
|
||||||
|
|
||||||
|
## Installation and updates
|
||||||
|
You can install NewPipe using one of the following methods:
|
||||||
|
1. Add our custom repo to F-Droid and install it from there. The instructions are here: https://newpipe.net/FAQ/tutorials/install-add-fdroid-repo/
|
||||||
|
2. Download the APK from [GitHub Releases](https://github.com/TeamNewPipe/NewPipe/releases), [compare the signing key](#apk-info) and install it.
|
||||||
|
3. Update via F-Droid. This is the slowest method of getting updates, as F-Droid must recognize changes, build the APK itself, sign it, and then push the update to users.
|
||||||
|
4. Build a debug APK yourself. This is the fastest way to get new features on your device, but is much more complicated, so we recommend using one of the other methods.
|
||||||
|
5. If you're interested in a specific feature or bugfix provided in a Pull Request in this repo, you can also download its APK from within the PR. Read the PR description for instructions. The great thing about PR-specific APKs is that they're installed side-by-side the official app, so you don't have to worry about losing your data or messing anything up.
|
||||||
|
|
||||||
|
We recommend method 1 for most users. APKs installed using method 1 or 2 are compatible with each other (meaning that if you installed NewPipe using either method 1 or 2, you can also update NewPipe using the other), but not with those installed using method 3. This is due to the same signing key (ours) being used for 1 and 2, but a different signing key (F-Droid's) being used for 3. Building a debug APK using method 4 excludes a key entirely. Signing keys help ensure that a user isn't tricked into installing a malicious update to an app. When using method 5, each APK is signed with a different random key supplied by GitHub Actions, so you cannot even update it. You will have to backup and restore the app data each time you wish to use a new APK.
|
||||||
|
|
||||||
|
In the meanwhile, if you want to switch sources for some reason (e.g. NewPipe's core functionality breaks and F-Droid doesn't have the latest update yet), we recommend following this procedure:
|
||||||
|
1. Back up your data via Settings > Backup and Restore > Export Database so you keep your history, subscriptions, and playlists
|
||||||
|
2. Uninstall NewPipe
|
||||||
|
3. Download the APK from the new source and install it
|
||||||
|
4. Import the data from step 1 via Settings > Backup and Restore > Import Database
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> When you're importing a database into the official app, always make sure that it is the one you exported _from_ the official app. If you import a database exported from an APK other than the official app, it may break things. Such an action is unsupported, and you should only do so when you're absolutely certain you know what you're doing.
|
||||||
|
|
||||||
|
### APK Info
|
||||||
|
|
||||||
|
This is the SHA fingerprint of NewPipe's signing key to verify downloaded APKs which are signed by us. The fingerprint is also available on [NewPipe's website](https://newpipe.net#download). This is relevant for method 2.
|
||||||
```
|
```
|
||||||
strawApp/ Sulkta-authored app — Compose UI, Media3 wiring, SB + RYD clients
|
CB:84:06:9B:D6:81:16:BA:FA:E5:EE:4E:E5:B0:8A:56:7A:A6:D8:98:40:4E:7C:B1:2F:9E:75:6D:F5:CF:5C:AB
|
||||||
rust/ strawcore — UniFFI wrapper around the Rust extractor
|
|
||||||
shared/ KMP scaffold inherited from upstream NewPipe (unused for now)
|
|
||||||
app/ Upstream NewPipe :app module — kept for reference
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build
|
## Contribution
|
||||||
|
Whether you have ideas, translations, design changes, code cleaning, or even major code changes, help is always welcome. The app gets better and better with each contribution, no matter how big or small! If you'd like to get involved, check our [contribution notes](.github/CONTRIBUTING.md).
|
||||||
|
|
||||||
```
|
<a href="https://hosted.weblate.org/engage/newpipe/">
|
||||||
./gradlew :strawApp:assembleDebug
|
<img src="https://hosted.weblate.org/widgets/newpipe/-/287x66-grey.png" alt="Translation status" />
|
||||||
```
|
</a>
|
||||||
|
|
||||||
Requires the Rust toolchain plus the four Android targets:
|
## Donate
|
||||||
|
If you like NewPipe, you're welcome to send a donation. We prefer Liberapay, as it is both open-source and non-profit. For further info on donating to NewPipe, please visit our [website](https://newpipe.net/donate).
|
||||||
|
|
||||||
```
|
<table>
|
||||||
rustup target add aarch64-linux-android armv7-linux-androideabi \
|
<tr>
|
||||||
x86_64-linux-android i686-linux-android
|
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="https://upload.wikimedia.org/wikipedia/commons/2/27/Liberapay_logo_v2_white-on-yellow.svg" alt="Liberapay" width="80px" ></a></td>
|
||||||
cargo install cargo-ndk uniffi-bindgen
|
<td><a href="https://liberapay.com/TeamNewPipe/"><img src="assets/liberapay_qr_code.png" alt="Visit NewPipe at liberapay.com" width="100px"></a></td>
|
||||||
```
|
<td><a href="https://liberapay.com/TeamNewPipe/donate"><img src="assets/liberapay_donate_button.svg" alt="Donate via Liberapay" height="35px"></a></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
…and `ANDROID_NDK_HOME` pointing at NDK r27c (or newer). The Gradle build runs
|
## Privacy Policy
|
||||||
`cargo ndk` + `uniffi-bindgen` automatically.
|
|
||||||
|
The NewPipe project aims to provide a private, anonymous experience for using web-based media services. Therefore, the app does not collect any data without your consent. NewPipe's privacy policy explains in detail what data is sent and stored when you send a crash report, or leave a comment in our blog. You can find the document [here](https://newpipe.net/legal/privacy/).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
GPL-3.0-or-later, inherited from upstream NewPipe.
|
NewPipe is Free Software: You can use, study, share, and improve it at will. Specifically you can redistribute and/or modify it under the terms of the [GNU General Public License](https://www.gnu.org/licenses/gpl.html) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
## Upstream
|
|
||||||
|
|
||||||
This repo tracks <https://github.com/TeamNewPipe/NewPipe>. Upstream changes
|
|
||||||
get pulled periodically via the `upstream` remote.
|
|
||||||
|
|
||||||
## Disclaimer
|
|
||||||
|
|
||||||
Not affiliated with YouTube, Google, NewPipe e.V., the SponsorBlock project,
|
|
||||||
or Return YouTube Dislike. Trademarks belong to their owners. Straw uses
|
|
||||||
public web endpoints; nothing here authenticates to any account.
|
|
||||||
|
|
|
||||||
318
app/build.gradle.kts
Normal file
318
app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.ApplicationExtension
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.android.application)
|
||||||
|
alias(libs.plugins.android.legacy.kapt)
|
||||||
|
alias(libs.plugins.google.ksp)
|
||||||
|
alias(libs.plugins.jetbrains.kotlin.parcelize)
|
||||||
|
alias(libs.plugins.jetbrains.kotlinx.serialization)
|
||||||
|
alias(libs.plugins.sonarqube)
|
||||||
|
checkstyle
|
||||||
|
}
|
||||||
|
|
||||||
|
val gitWorkingBranch = providers.exec {
|
||||||
|
commandLine("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||||
|
}.standardOutput.asText.map { it.trim() }
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
compilerOptions {
|
||||||
|
// TODO: Drop annotation default target when it is stable
|
||||||
|
freeCompilerArgs.addAll(
|
||||||
|
"-Xannotation-default-target=param-property"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configure<ApplicationExtension> {
|
||||||
|
compileSdk {
|
||||||
|
version = release(NEWPIPE_VERSION_SDK_COMPILE_MAJOR) {
|
||||||
|
minorApiLevel = NEWPIPE_VERSION_SDK_COMPILE_MINOR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
namespace = NEWPIPE_APPLICATION_ID_OLD
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = NEWPIPE_APPLICATION_ID_OLD
|
||||||
|
resValue("string", "app_name", "NewPipe")
|
||||||
|
minSdk {
|
||||||
|
version = release(NEWPIPE_VERSION_SDK_MIN)
|
||||||
|
}
|
||||||
|
targetSdk {
|
||||||
|
version = release(NEWPIPE_VERSION_SDK_TARGET)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionCode = System.getProperty("versionCodeOverride")?.toInt() ?: NEWPIPE_VERSION_CODE
|
||||||
|
|
||||||
|
versionName = NEWPIPE_VERSION_NAME
|
||||||
|
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 = true
|
||||||
|
proguardFiles(
|
||||||
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
"proguard-rules.pro"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lint {
|
||||||
|
lintConfig = file("lint.xml")
|
||||||
|
// Continue the debug build even when errors are found
|
||||||
|
abortOnError = false
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
// Flag to enable support for the new language APIs
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
encoding = "utf-8"
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
getByName("androidTest") {
|
||||||
|
assets.directories += "$projectDir/schemas"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding = true
|
||||||
|
buildConfig = true
|
||||||
|
resValues = true
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
resources {
|
||||||
|
// remove two files which belong to jsoup
|
||||||
|
// no idea how they ended up in the META-INF dir...
|
||||||
|
excludes += setOf(
|
||||||
|
"META-INF/README.md",
|
||||||
|
"META-INF/CHANGES",
|
||||||
|
"META-INF/COPYRIGHT" // "COPYRIGHT" belongs to RxJava...
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Custom dependency configuration for ktlint
|
||||||
|
val ktlint by configurations.creating
|
||||||
|
|
||||||
|
checkstyle {
|
||||||
|
configDirectory = rootProject.file("checkstyle")
|
||||||
|
isIgnoreFailures = false
|
||||||
|
isShowViolations = true
|
||||||
|
toolVersion = libs.versions.checkstyle.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Checkstyle>("runCheckstyle") {
|
||||||
|
source("src")
|
||||||
|
include("**/*.java")
|
||||||
|
exclude("**/gen/**")
|
||||||
|
exclude("**/R.java")
|
||||||
|
exclude("**/BuildConfig.java")
|
||||||
|
exclude("main/java/us/shandian/giga/**")
|
||||||
|
|
||||||
|
classpath = configurations.getByName("checkstyle")
|
||||||
|
|
||||||
|
isShowViolations = true
|
||||||
|
|
||||||
|
reports {
|
||||||
|
xml.required = true
|
||||||
|
html.required = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val outputDir = project.layout.buildDirectory.dir("reports/ktlint/")
|
||||||
|
val inputFiles = fileTree("src") { include("**/*.kt") }
|
||||||
|
|
||||||
|
tasks.register<JavaExec>("runKtlint") {
|
||||||
|
inputs.files(inputFiles)
|
||||||
|
outputs.dir(outputDir)
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
classpath = configurations.getByName("ktlint")
|
||||||
|
args = listOf("--editorconfig=../.editorconfig", "src/**/*.kt")
|
||||||
|
jvmArgs = listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<JavaExec>("formatKtlint") {
|
||||||
|
inputs.files(inputFiles)
|
||||||
|
outputs.dir(outputDir)
|
||||||
|
mainClass.set("com.pinterest.ktlint.Main")
|
||||||
|
classpath = configurations.getByName("ktlint")
|
||||||
|
args = listOf("--editorconfig=../.editorconfig", "-F", "src/**/*.kt")
|
||||||
|
jvmArgs = listOf("--add-opens", "java.base/java.lang=ALL-UNNAMED")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<CheckDependenciesOrder>("checkDependenciesOrder") {
|
||||||
|
tomlFile = layout.projectDirectory.file("../gradle/libs.versions.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
tasks.named("preDebugBuild").configure {
|
||||||
|
if (!System.getProperties().containsKey("skipFormatKtlint")) {
|
||||||
|
dependsOn("formatKtlint")
|
||||||
|
}
|
||||||
|
dependsOn("runCheckstyle", "runKtlint", "checkDependenciesOrder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sonar {
|
||||||
|
properties {
|
||||||
|
property("sonar.projectKey", "TeamNewPipe_NewPipe")
|
||||||
|
property("sonar.organization", "teamnewpipe")
|
||||||
|
property("sonar.host.url", "https://sonarcloud.io")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Coroutines interop
|
||||||
|
implementation(libs.kotlinx.coroutines.rx3)
|
||||||
|
|
||||||
|
// Kotlinx Serialization
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
|
||||||
|
/** 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.coil.compose)
|
||||||
|
implementation(libs.coil.network.okhttp)
|
||||||
|
|
||||||
|
// Markdown library for Android
|
||||||
|
implementation(libs.noties.markwon.core)
|
||||||
|
implementation(libs.noties.markwon.linkify)
|
||||||
|
|
||||||
|
// Crash reporting
|
||||||
|
implementation(libs.acra.core)
|
||||||
|
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)
|
||||||
|
}
|
||||||
10
app/lint.xml
Normal file
10
app/lint.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
<lint>
|
||||||
|
<issue id="MissingTranslation" severity="ignore" />
|
||||||
|
<issue id="MissingQuantity" severity="ignore" />
|
||||||
|
<issue id="ImpliedQuantity" severity="ignore" />
|
||||||
|
</lint>
|
||||||
59
app/proguard-rules.pro
vendored
Normal file
59
app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# https://developer.android.com/build/shrink-code
|
||||||
|
|
||||||
|
## Helps debug release versions
|
||||||
|
-dontobfuscate
|
||||||
|
|
||||||
|
## Rules for NewPipeExtractor
|
||||||
|
-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.engine.** { *; }
|
||||||
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
|
-dontwarn org.mozilla.javascript.JavaToJSONConverters
|
||||||
|
-dontwarn org.mozilla.javascript.tools.**
|
||||||
|
-keep class javax.script.** { *; }
|
||||||
|
-dontwarn javax.script.**
|
||||||
|
-keep class jdk.dynalink.** { *; }
|
||||||
|
-dontwarn jdk.dynalink.**
|
||||||
|
|
||||||
|
## Rules for ExoPlayer
|
||||||
|
-keep class com.google.android.exoplayer2.** { *; }
|
||||||
|
|
||||||
|
## Rules for OkHttp. Copy pasted from https://github.com/square/okhttp
|
||||||
|
-dontwarn okhttp3.**
|
||||||
|
-dontwarn okio.**
|
||||||
|
|
||||||
|
## See https://github.com/TeamNewPipe/NewPipe/pull/1441
|
||||||
|
-keepclassmembers class * implements java.io.Serializable {
|
||||||
|
static final long serialVersionUID;
|
||||||
|
!static !transient <fields>;
|
||||||
|
private void writeObject(java.io.ObjectOutputStream);
|
||||||
|
private void readObject(java.io.ObjectInputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
## For some reason NotificationModeConfigFragment wasn't kept (only referenced in a preference xml)
|
||||||
|
-keep class org.schabi.newpipe.settings.notifications.** { *; }
|
||||||
|
|
||||||
|
# Prevent R8 from stripping or renaming Protobuf internal fields
|
||||||
|
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
## Keep Kotlinx Serialization classes
|
||||||
|
-keepclassmembers class kotlinx.serialization.json.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
-keep,includedescriptorclasses class org.schabi.newpipe.**$$serializer { *; }
|
||||||
|
-keepclassmembers class org.schabi.newpipe.** {
|
||||||
|
*** Companion;
|
||||||
|
}
|
||||||
|
-keepclasseswithmembers class org.schabi.newpipe.** {
|
||||||
|
kotlinx.serialization.KSerializer serializer(...);
|
||||||
|
}
|
||||||
|
|
||||||
|
# See https://github.com/TeamNewPipe/NewPipe/issues/13508
|
||||||
|
-keep class org.ocpsoft.prettytime.i18n.Resources* { *; }
|
||||||
19
app/sampledata/channels.json
Normal file
19
app/sampledata/channels.json
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"name": "BBC",
|
||||||
|
"additional": "12K subscribers•233 videos",
|
||||||
|
"description": "The BBC is the world’s leading public service broadcaster. We’re impartial and independent, and every day we create distinctive, world-class programmes and content which inform, educate and entertain millions of people in the UK and around the world. SUBSCRIBE to our YouTube channel to get the best of BBC entertainment and comedy programmes, stories from science and nature documentaries, and much more! https://bit.ly/2IXqEIn Get ALL your fresh TV, and sofa-hugging box sets on iPlayer https://bbc.in/2J18jYJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Linus Tech Tips",
|
||||||
|
"additional": "1M subscribers•233 videos",
|
||||||
|
"description": "Looking for a Tech YouTuber?\n\nLinus Tech Tips is a passionate team of \"professionally curious\" experts in consumer technology and video production which aims to inform and educate people of all ages through our entertaining videos. We create product reviews, step-by-step computer build guides, and a variety of other tech-focused content.\n\nSchedule:\nNew videos every Saturday to Thursday @ 10:00am Pacific\nLive WAN Show podcasts every Friday @ ~5:00pm Pacific"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Marques Brownlee",
|
||||||
|
"additional": "13 subscribers•12K videos",
|
||||||
|
"description": "MKBHD: Quality Tech Videos | YouTuber | Geek | Consumer Electronics | Tech Head | Internet Personality!\n\nbusiness@MKBHD.com\n\nNYC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
479
app/schemas/org.schabi.newpipe.database.AppDatabase/2.json
Normal file
479
app/schemas/org.schabi.newpipe.database.AppDatabase/2.json
Normal file
|
|
@ -0,0 +1,479 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "b7856223e2595ddf20a3ce6243ce9527",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressTime",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b7856223e2595ddf20a3ce6243ce9527\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
707
app/schemas/org.schabi.newpipe.database.AppDatabase/3.json
Normal file
707
app/schemas/org.schabi.newpipe.database.AppDatabase/3.json
Normal file
|
|
@ -0,0 +1,707 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "9f825b1ee281480bedd38b971feac327",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressTime",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9f825b1ee281480bedd38b971feac327')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
713
app/schemas/org.schabi.newpipe.database.AppDatabase/4.json
Normal file
|
|
@ -0,0 +1,713 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "d8070091972a7011bce18aed62f80b90",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd8070091972a7011bce18aed62f80b90')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
719
app/schemas/org.schabi.newpipe.database.AppDatabase/5.json
Normal file
719
app/schemas/org.schabi.newpipe.database.AppDatabase/5.json
Normal file
|
|
@ -0,0 +1,719 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 5,
|
||||||
|
"identityHash": "096731b513bb71dd44517639f4a2c1e3",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '096731b513bb71dd44517639f4a2c1e3')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
737
app/schemas/org.schabi.newpipe.database.AppDatabase/6.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/6.json
Normal file
|
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 6,
|
||||||
|
"identityHash": "4084aa342aef315dc7b558770a7755a9",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `thumbnail_url` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4084aa342aef315dc7b558770a7755a9')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
737
app/schemas/org.schabi.newpipe.database.AppDatabase/7.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/7.json
Normal file
|
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 7,
|
||||||
|
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
737
app/schemas/org.schabi.newpipe.database.AppDatabase/8.json
Normal file
|
|
@ -0,0 +1,737 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 8,
|
||||||
|
"identityHash": "012fc8e7ad3333f1597347f34e76a513",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT, `is_thumbnail_permanent` INTEGER NOT NULL, `thumbnail_stream_id` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `name` TEXT, `url` TEXT, `thumbnail_url` TEXT, `uploader` TEXT, `stream_count` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_name",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_remote_playlists_name` ON `${TABLE_NAME}` (`name`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '012fc8e7ad3333f1597347f34e76a513')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
730
app/schemas/org.schabi.newpipe.database.AppDatabase/9.json
Normal file
|
|
@ -0,0 +1,730 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 9,
|
||||||
|
"identityHash": "7591e8039faa74d8c0517dc867af9d3e",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "subscriptions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `service_id` INTEGER NOT NULL, `url` TEXT, `name` TEXT, `avatar_url` TEXT, `subscriber_count` INTEGER, `description` TEXT, `notification_mode` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatarUrl",
|
||||||
|
"columnName": "avatar_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriberCount",
|
||||||
|
"columnName": "subscriber_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationMode",
|
||||||
|
"columnName": "notification_mode",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_subscriptions_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_subscriptions_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "search_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`creation_date` INTEGER, `service_id` INTEGER NOT NULL, `search` TEXT, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "creationDate",
|
||||||
|
"columnName": "creation_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "search",
|
||||||
|
"columnName": "search",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_search_history_search",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"search"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_search_history_search` ON `${TABLE_NAME}` (`search`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "streams",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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, `uploader_url` TEXT, `thumbnail_url` TEXT, `view_count` INTEGER, `textual_upload_date` TEXT, `upload_date` INTEGER, `is_upload_date_approximation` INTEGER)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamType",
|
||||||
|
"columnName": "stream_type",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploaderUrl",
|
||||||
|
"columnName": "uploader_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "viewCount",
|
||||||
|
"columnName": "view_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "textualUploadDate",
|
||||||
|
"columnName": "textual_upload_date",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploadDate",
|
||||||
|
"columnName": "upload_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isUploadDateApproximation",
|
||||||
|
"columnName": "is_upload_date_approximation",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_streams_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_streams_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessDate",
|
||||||
|
"columnName": "access_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "repeatCount",
|
||||||
|
"columnName": "repeat_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"access_date"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_stream_history_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_stream_history_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stream_state",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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 )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "progressMillis",
|
||||||
|
"columnName": "progress_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isThumbnailPermanent",
|
||||||
|
"columnName": "is_thumbnail_permanent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailStreamId",
|
||||||
|
"columnName": "thumbnail_stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "playlist_stream_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "playlistUid",
|
||||||
|
"columnName": "playlist_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamUid",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "index",
|
||||||
|
"columnName": "join_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_playlist_id_join_index",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"playlist_id",
|
||||||
|
"join_index"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_stream_join_playlist_id_join_index` ON `${TABLE_NAME}` (`playlist_id`, `join_index`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_playlist_stream_join_stream_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_playlist_stream_join_stream_id` ON `${TABLE_NAME}` (`stream_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "playlists",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"playlist_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "remote_playlists",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "serviceId",
|
||||||
|
"columnName": "service_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "orderingName",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "thumbnailUrl",
|
||||||
|
"columnName": "thumbnail_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "uploader",
|
||||||
|
"columnName": "uploader",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayIndex",
|
||||||
|
"columnName": "display_index",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "streamCount",
|
||||||
|
"columnName": "stream_count",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_remote_playlists_service_id_url",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"service_id",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_remote_playlists_service_id_url` ON `${TABLE_NAME}` (`service_id`, `url`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "streamId",
|
||||||
|
"columnName": "stream_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"stream_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "streams",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"stream_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon_id` INTEGER NOT NULL, `sort_order` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortOrder",
|
||||||
|
"columnName": "sort_order",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_sort_order",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_order"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_sort_order` ON `${TABLE_NAME}` (`sort_order`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_group_subscription_join",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "feedGroupId",
|
||||||
|
"columnName": "group_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"group_id",
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_feed_group_subscription_join_subscription_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_feed_group_subscription_join_subscription_id` ON `${TABLE_NAME}` (`subscription_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "feed_group",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "feed_last_updated",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "subscriptionId",
|
||||||
|
"columnName": "subscription_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUpdated",
|
||||||
|
"columnName": "last_updated",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"subscription_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "subscriptions",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "CASCADE",
|
||||||
|
"columns": [
|
||||||
|
"subscription_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"uid"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7591e8039faa74d8c0517dc867af9d3e')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.sqlite.SQLiteDatabase
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.testing.MigrationTestHelper
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
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
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DatabaseMigrationTest {
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_SERVICE_ID = 0
|
||||||
|
private const val DEFAULT_URL = "https://www.youtube.com/watch?v=cDphUib5iG4"
|
||||||
|
private const val DEFAULT_TITLE = "Test Title"
|
||||||
|
private const val DEFAULT_NAME = "Test Name"
|
||||||
|
private val DEFAULT_TYPE = StreamType.VIDEO_STREAM
|
||||||
|
private const val DEFAULT_DURATION = 480L
|
||||||
|
private const val DEFAULT_UPLOADER_NAME = "Uploader Test"
|
||||||
|
private const val DEFAULT_THUMBNAIL = "https://example.com/example.jpg"
|
||||||
|
|
||||||
|
private const val DEFAULT_SECOND_SERVICE_ID = 1
|
||||||
|
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
|
||||||
|
val testHelper = MigrationTestHelper(
|
||||||
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
|
AppDatabase::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateDatabaseFrom2to3() {
|
||||||
|
val databaseInV2 = testHelper.createDatabase(AppDatabase.DATABASE_NAME, Migrations.DB_VER_2)
|
||||||
|
|
||||||
|
databaseInV2.run {
|
||||||
|
insert(
|
||||||
|
"streams",
|
||||||
|
SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_URL)
|
||||||
|
put("title", DEFAULT_TITLE)
|
||||||
|
put("stream_type", DEFAULT_TYPE.name)
|
||||||
|
put("duration", DEFAULT_DURATION)
|
||||||
|
put("uploader", DEFAULT_UPLOADER_NAME)
|
||||||
|
put("thumbnail_url", DEFAULT_THUMBNAIL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"streams",
|
||||||
|
SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SECOND_SERVICE_ID)
|
||||||
|
put("url", DEFAULT_SECOND_URL)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
insert(
|
||||||
|
"streams",
|
||||||
|
SQLiteDatabase.CONFLICT_FAIL,
|
||||||
|
ContentValues().apply {
|
||||||
|
put("service_id", DEFAULT_SERVICE_ID)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_3,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_2_3
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_4,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_3_4
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_5,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_4_5
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_6,
|
||||||
|
true,
|
||||||
|
Migrations.MIGRATION_5_6
|
||||||
|
)
|
||||||
|
|
||||||
|
testHelper.runMigrationsAndValidate(
|
||||||
|
AppDatabase.DATABASE_NAME,
|
||||||
|
Migrations.DB_VER_7,
|
||||||
|
true,
|
||||||
|
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 listFromDB = migratedDatabaseV3.streamDAO().getAll().blockingFirst()
|
||||||
|
|
||||||
|
// Only expect 2, the one with the null url will be ignored
|
||||||
|
assertEquals(2, listFromDB.size)
|
||||||
|
|
||||||
|
val streamFromMigratedDatabase = listFromDB[0]
|
||||||
|
assertEquals(DEFAULT_SERVICE_ID, streamFromMigratedDatabase.serviceId)
|
||||||
|
assertEquals(DEFAULT_URL, streamFromMigratedDatabase.url)
|
||||||
|
assertEquals(DEFAULT_TITLE, streamFromMigratedDatabase.title)
|
||||||
|
assertEquals(DEFAULT_TYPE, streamFromMigratedDatabase.streamType)
|
||||||
|
assertEquals(DEFAULT_DURATION, streamFromMigratedDatabase.duration)
|
||||||
|
assertEquals(DEFAULT_UPLOADER_NAME, streamFromMigratedDatabase.uploader)
|
||||||
|
assertEquals(DEFAULT_THUMBNAIL, streamFromMigratedDatabase.thumbnailUrl)
|
||||||
|
assertNull(streamFromMigratedDatabase.viewCount)
|
||||||
|
assertNull(streamFromMigratedDatabase.textualUploadDate)
|
||||||
|
assertNull(streamFromMigratedDatabase.uploadDate)
|
||||||
|
assertNull(streamFromMigratedDatabase.isUploadDateApproximation)
|
||||||
|
|
||||||
|
val secondStreamFromMigratedDatabase = listFromDB[1]
|
||||||
|
assertEquals(DEFAULT_SECOND_SERVICE_ID, secondStreamFromMigratedDatabase.serviceId)
|
||||||
|
assertEquals(DEFAULT_SECOND_URL, secondStreamFromMigratedDatabase.url)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.title)
|
||||||
|
// Should fallback to VIDEO_STREAM
|
||||||
|
assertEquals(StreamType.VIDEO_STREAM, secondStreamFromMigratedDatabase.streamType)
|
||||||
|
assertEquals(0, secondStreamFromMigratedDatabase.duration)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.uploader)
|
||||||
|
assertEquals("", secondStreamFromMigratedDatabase.thumbnailUrl)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.viewCount)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.textualUploadDate)
|
||||||
|
assertNull(secondStreamFromMigratedDatabase.uploadDate)
|
||||||
|
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().getAll().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().getAll().blockingFirst()
|
||||||
|
var remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().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(
|
||||||
|
name = "${DEFAULT_NAME}3",
|
||||||
|
isThumbnailPermanent = false,
|
||||||
|
thumbnailStreamId = -1,
|
||||||
|
displayIndex = -1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val remoteUid3 = migratedDatabaseV9.playlistRemoteDAO().insert(
|
||||||
|
PlaylistRemoteEntity(
|
||||||
|
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().getAll().blockingFirst()
|
||||||
|
remoteListFromDB = migratedDatabaseV9.playlistRemoteDAO().getAll().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 {
|
||||||
|
val database: AppDatabase = Room.databaseBuilder(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
AppDatabase::class.java,
|
||||||
|
AppDatabase.DATABASE_NAME
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
testHelper.closeWhenFinished(database)
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
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 java.io.IOException
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package org.schabi.newpipe.error;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.filters.LargeTest;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented tests for {@link ErrorInfo}.
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
@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() 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:
|
||||||
|
final Parcel parcel = Parcel.obtain();
|
||||||
|
info.writeToParcel(parcel, 0);
|
||||||
|
parcel.setDataPosition(0);
|
||||||
|
final ErrorInfo infoFromParcel = (ErrorInfo) ErrorInfo.CREATOR.createFromParcel(parcel);
|
||||||
|
|
||||||
|
assertTrue(Arrays.toString(infoFromParcel.getStackTraces())
|
||||||
|
.contains(ErrorInfoTest.class.getSimpleName()));
|
||||||
|
assertEquals(UserAction.USER_REPORT, infoFromParcel.getUserAction());
|
||||||
|
assertEquals(ServiceList.YouTube.getServiceInfo().getName(),
|
||||||
|
infoFromParcel.getServiceName());
|
||||||
|
assertEquals("request", infoFromParcel.getRequest());
|
||||||
|
assertEquals(R.string.parsing_error, getMessageFromErrorInfo(infoFromParcel));
|
||||||
|
|
||||||
|
parcel.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,176 @@
|
||||||
|
package org.schabi.newpipe.local.history
|
||||||
|
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||||
|
import org.schabi.newpipe.testUtil.TestDatabase
|
||||||
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||||
|
|
||||||
|
class HistoryRecordManagerTest {
|
||||||
|
|
||||||
|
private lateinit var manager: HistoryRecordManager
|
||||||
|
private lateinit var database: AppDatabase
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val trampolineScheduler = TrampolineSchedulerRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||||
|
manager = HistoryRecordManager(ApplicationProvider.getApplicationContext())
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onSearched() {
|
||||||
|
manager.onSearched(0, "Hello").test().await().assertValue(1)
|
||||||
|
|
||||||
|
// 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().getAll().blockingFirst()
|
||||||
|
assertThat(entities).hasSize(1)
|
||||||
|
assertThat(entities[0].id).isEqualTo(1)
|
||||||
|
assertThat(entities[0].serviceId).isEqualTo(0)
|
||||||
|
assertThat(entities[0].search).isEqualTo("Hello")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteSearchHistory() {
|
||||||
|
val entries = listOf(
|
||||||
|
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().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().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().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().getAll().blockingFirst()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun deleteCompleteSearchHistory() {
|
||||||
|
val entries = listOf(
|
||||||
|
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().getAll().blockingFirst()).hasSameSizeAs(entries)
|
||||||
|
|
||||||
|
// should remove everything
|
||||||
|
manager.deleteCompleteSearchHistory().test().await().assertValue(entries.size)
|
||||||
|
assertThat(database.searchHistoryDAO().getAll().blockingFirst()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertShuffledRelatedSearches(relatedSearches: Collection<SearchHistoryEntry>) {
|
||||||
|
// shuffle to make sure the order of items returned by queries depends only on
|
||||||
|
// SearchHistoryEntry.creationDate, not on the actual insertion time, so that we can
|
||||||
|
// verify that the `ORDER BY` clause does its job
|
||||||
|
database.searchHistoryDAO().insertAll(relatedSearches.shuffled())
|
||||||
|
|
||||||
|
// make sure all entries were inserted
|
||||||
|
assertEquals(
|
||||||
|
relatedSearches.size,
|
||||||
|
database.searchHistoryDAO().getAll().blockingFirst().size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRelatedSearches_emptyQuery() {
|
||||||
|
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||||
|
|
||||||
|
// make sure correct number of searches is returned and in correct order
|
||||||
|
val searches = manager.getRelatedSearches("", 6, 4).blockingFirst()
|
||||||
|
assertThat(searches).containsExactly(
|
||||||
|
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||||
|
RELATED_SEARCHES_ENTRIES[4].search, // B
|
||||||
|
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||||
|
RELATED_SEARCHES_ENTRIES[2].search // BA
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRelatedSearches_emptyQuery_manyDuplicates() {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getRelatedSearched_nonEmptyQuery() {
|
||||||
|
insertShuffledRelatedSearches(RELATED_SEARCHES_ENTRIES)
|
||||||
|
|
||||||
|
// make sure correct number of searches is returned and in correct order
|
||||||
|
val searches = manager.getRelatedSearches("A", 3, 5).blockingFirst()
|
||||||
|
assertThat(searches).containsExactly(
|
||||||
|
RELATED_SEARCHES_ENTRIES[6].search, // A (even if in two places)
|
||||||
|
RELATED_SEARCHES_ENTRIES[5].search, // AA
|
||||||
|
RELATED_SEARCHES_ENTRIES[1].search // BA
|
||||||
|
)
|
||||||
|
|
||||||
|
// also make sure that the string comparison is case insensitive
|
||||||
|
val searches2 = manager.getRelatedSearches("a", 3, 5).blockingFirst()
|
||||||
|
assertThat(searches).isEqualTo(searches2)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val time = OffsetDateTime.of(LocalDateTime.of(2000, 1, 1, 1, 1), ZoneOffset.UTC)
|
||||||
|
|
||||||
|
private val RELATED_SEARCHES_ENTRIES = listOf(
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package org.schabi.newpipe.local.playlist
|
||||||
|
|
||||||
|
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.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.testUtil.TestDatabase
|
||||||
|
import org.schabi.newpipe.testUtil.TrampolineSchedulerRule
|
||||||
|
|
||||||
|
class LocalPlaylistManagerTest {
|
||||||
|
|
||||||
|
private lateinit var manager: LocalPlaylistManager
|
||||||
|
private lateinit var database: AppDatabase
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val trampolineScheduler = TrampolineSchedulerRule()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
database = TestDatabase.createReplacingNewPipeDatabase()
|
||||||
|
manager = LocalPlaylistManager(database)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
database.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createPlaylist() {
|
||||||
|
val NEWPIPE_URL = "https://newpipe.net/"
|
||||||
|
val stream = StreamEntity(
|
||||||
|
serviceId = 1,
|
||||||
|
url = NEWPIPE_URL,
|
||||||
|
title = "title",
|
||||||
|
streamType = StreamType.VIDEO_STREAM,
|
||||||
|
duration = 1,
|
||||||
|
uploader = "uploader",
|
||||||
|
uploaderUrl = NEWPIPE_URL
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = manager.createPlaylist("name", listOf(stream))
|
||||||
|
|
||||||
|
// This should not behave like this.
|
||||||
|
// Currently list of all stream ids is returned instead of playlist id
|
||||||
|
result.test().await().assertValue(listOf(1L))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun createPlaylist_emptyPlaylistMustReturnEmpty() {
|
||||||
|
val result = manager.createPlaylist("name", emptyList())
|
||||||
|
|
||||||
|
// This should not behave like this.
|
||||||
|
// It should throw an error because currently the result is null
|
||||||
|
result.test().await().assertComplete()
|
||||||
|
manager.playlists.test().awaitCount(1).assertValue(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test()
|
||||||
|
fun createPlaylist_nonExistentStreamsAreUpserted() {
|
||||||
|
val stream = StreamEntity(
|
||||||
|
serviceId = 1,
|
||||||
|
url = "https://newpipe.net/",
|
||||||
|
title = "title",
|
||||||
|
streamType = StreamType.VIDEO_STREAM,
|
||||||
|
duration = 1,
|
||||||
|
uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
|
)
|
||||||
|
database.streamDAO().insert(stream)
|
||||||
|
val upserted = StreamEntity(
|
||||||
|
serviceId = 1,
|
||||||
|
url = "https://newpipe.net/2",
|
||||||
|
title = "title2",
|
||||||
|
streamType = StreamType.VIDEO_STREAM,
|
||||||
|
duration = 1,
|
||||||
|
uploader = "uploader",
|
||||||
|
uploaderUrl = "https://newpipe.net/"
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = manager.createPlaylist("name", listOf(stream, upserted))
|
||||||
|
|
||||||
|
result.test().await().assertComplete()
|
||||||
|
database.streamDAO().getAll().test().awaitCount(1).assertValue(listOf(stream, upserted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package org.schabi.newpipe.testUtil
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.schabi.newpipe.NewPipeDatabase
|
||||||
|
import org.schabi.newpipe.database.AppDatabase
|
||||||
|
|
||||||
|
class TestDatabase {
|
||||||
|
companion object {
|
||||||
|
fun createReplacingNewPipeDatabase(): AppDatabase {
|
||||||
|
val database = Room.inMemoryDatabaseBuilder(
|
||||||
|
ApplicationProvider.getApplicationContext(),
|
||||||
|
AppDatabase::class.java
|
||||||
|
)
|
||||||
|
.allowMainThreadQueries()
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val databaseField = NewPipeDatabase::class.java.getDeclaredField("databaseInstance")
|
||||||
|
databaseField.isAccessible = true
|
||||||
|
databaseField.set(NewPipeDatabase::class, database)
|
||||||
|
|
||||||
|
assertSame(
|
||||||
|
"Mocking database failed!",
|
||||||
|
database,
|
||||||
|
NewPipeDatabase.getInstance(ApplicationProvider.getApplicationContext())
|
||||||
|
)
|
||||||
|
|
||||||
|
return database
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.schabi.newpipe.testUtil
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.Description
|
||||||
|
import org.junit.runners.model.Statement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always run on [Schedulers.trampoline].
|
||||||
|
* This executes the task in the current thread in FIFO manner.
|
||||||
|
* This ensures that tasks are run quickly inside the tests
|
||||||
|
* and not scheduled away to another thread for later execution
|
||||||
|
*/
|
||||||
|
class TrampolineSchedulerRule : TestRule {
|
||||||
|
|
||||||
|
private val scheduler = Schedulers.trampoline()
|
||||||
|
|
||||||
|
override fun apply(base: Statement, description: Description): Statement = object : Statement() {
|
||||||
|
override fun evaluate() {
|
||||||
|
try {
|
||||||
|
RxJavaPlugins.setComputationSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setNewThreadSchedulerHandler { scheduler }
|
||||||
|
RxJavaPlugins.setSingleSchedulerHandler { scheduler }
|
||||||
|
RxAndroidPlugins.setInitMainThreadSchedulerHandler { scheduler }
|
||||||
|
|
||||||
|
base.evaluate()
|
||||||
|
} finally {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
RxAndroidPlugins.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,384 @@
|
||||||
|
package org.schabi.newpipe.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.INVISIBLE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.widget.Spinner
|
||||||
|
import androidx.collection.SparseArrayCompat
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.MediumTest
|
||||||
|
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
|
||||||
|
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.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
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.Stream
|
||||||
|
import org.schabi.newpipe.extractor.stream.SubtitlesStream
|
||||||
|
import org.schabi.newpipe.extractor.stream.VideoStream
|
||||||
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamInfoWrapper
|
||||||
|
|
||||||
|
@MediumTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class StreamItemAdapterTest {
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var spinner: Spinner
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
context = ApplicationProvider.getApplicationContext()
|
||||||
|
UiThreadStatement.runOnUiThread {
|
||||||
|
spinner = Spinner(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun videoStreams_noSecondaryStream() {
|
||||||
|
val adapter = StreamItemAdapter<VideoStream, AudioStream>(
|
||||||
|
getVideoStreams(true, true, true, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner.adapter = adapter
|
||||||
|
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 1, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun videoStreams_hasSecondaryStream() {
|
||||||
|
val adapter = StreamItemAdapter(
|
||||||
|
getVideoStreams(false, true, false, true),
|
||||||
|
getAudioStreams(false, true, false, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner.adapter = adapter
|
||||||
|
assertIconVisibility(spinner, 0, GONE, GONE)
|
||||||
|
assertIconVisibility(spinner, 1, GONE, GONE)
|
||||||
|
assertIconVisibility(spinner, 2, GONE, GONE)
|
||||||
|
assertIconVisibility(spinner, 3, GONE, GONE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun videoStreams_Mixed() {
|
||||||
|
val adapter = StreamItemAdapter(
|
||||||
|
getVideoStreams(true, true, true, true, true, false, true, true),
|
||||||
|
getAudioStreams(false, true, false, false, false, true, true, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
spinner.adapter = adapter
|
||||||
|
assertIconVisibility(spinner, 0, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 1, GONE, INVISIBLE)
|
||||||
|
assertIconVisibility(spinner, 2, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 3, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 4, VISIBLE, VISIBLE)
|
||||||
|
assertIconVisibility(spinner, 5, GONE, INVISIBLE)
|
||||||
|
assertIconVisibility(spinner, 6, GONE, INVISIBLE)
|
||||||
|
assertIconVisibility(spinner, 7, GONE, INVISIBLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun subtitleStreams_noIcon() {
|
||||||
|
val adapter = StreamItemAdapter<SubtitlesStream, Stream>(
|
||||||
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
|
(0 until 5).map {
|
||||||
|
SubtitlesStream.Builder()
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.SRT)
|
||||||
|
.setLanguageCode("pt-BR")
|
||||||
|
.setAutoGenerated(false)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
spinner.adapter = adapter
|
||||||
|
for (i in 0 until spinner.count) {
|
||||||
|
assertIconVisibility(spinner, i, GONE, GONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun audioStreams_noIcon() {
|
||||||
|
val adapter = StreamItemAdapter<AudioStream, Stream>(
|
||||||
|
StreamItemAdapter.StreamInfoWrapper(
|
||||||
|
(0 until 5).map {
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com/$it", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
spinner.adapter = adapter
|
||||||
|
for (i in 0 until spinner.count) {
|
||||||
|
assertIconVisibility(spinner, i, GONE, GONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
* [videoOnly] vararg.
|
||||||
|
*/
|
||||||
|
private fun getVideoStreams(vararg videoOnly: Boolean) = StreamInfoWrapper(
|
||||||
|
videoOnly.map {
|
||||||
|
VideoStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.MPEG_4)
|
||||||
|
.setResolution("720p")
|
||||||
|
.setIsVideoOnly(it)
|
||||||
|
.build()
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a list of audio streams, containing valid and null elements mirroring the provided
|
||||||
|
* [shouldBeValid] vararg.
|
||||||
|
*/
|
||||||
|
private fun getAudioStreams(vararg shouldBeValid: Boolean) = getSecondaryStreamsFromList(
|
||||||
|
shouldBeValid.map {
|
||||||
|
if (it) {
|
||||||
|
AudioStream.Builder()
|
||||||
|
.setId(Stream.ID_UNKNOWN)
|
||||||
|
.setContent("https://example.com", true)
|
||||||
|
.setMediaFormat(MediaFormat.OPUS)
|
||||||
|
.setAverageBitrate(192)
|
||||||
|
.build()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
* it is shown in normal mode (selected) and in dropdown mode (user is choosing one of a list).
|
||||||
|
*/
|
||||||
|
private fun assertIconVisibility(
|
||||||
|
spinner: Spinner,
|
||||||
|
position: Int,
|
||||||
|
normalVisibility: Int,
|
||||||
|
dropDownVisibility: Int
|
||||||
|
) {
|
||||||
|
spinner.setSelection(position)
|
||||||
|
spinner.adapter.getView(position, null, spinner).run {
|
||||||
|
Assert.assertEquals(
|
||||||
|
"normal visibility (pos=[$position]) is not correct",
|
||||||
|
findViewById<View>(R.id.wo_sound_icon).visibility,
|
||||||
|
normalVisibility
|
||||||
|
)
|
||||||
|
}
|
||||||
|
spinner.adapter.getDropDownView(position, null, spinner).run {
|
||||||
|
Assert.assertEquals(
|
||||||
|
"drop down visibility (pos=[$position]) is not correct",
|
||||||
|
findViewById<View>(R.id.wo_sound_icon).visibility,
|
||||||
|
dropDownVisibility
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that builds a secondary stream list.
|
||||||
|
*/
|
||||||
|
private fun <T : Stream> getSecondaryStreamsFromList(streams: List<T?>) = SparseArrayCompat<SecondaryStreamHelper<T>?>(streams.size).apply {
|
||||||
|
streams.forEachIndexed { index, stream ->
|
||||||
|
val secondaryStreamHelper: SecondaryStreamHelper<T>? = stream?.let {
|
||||||
|
SecondaryStreamHelper(
|
||||||
|
StreamItemAdapter.StreamInfoWrapper(streams, context),
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
app/src/debug/AndroidManifest.xml
Normal file
8
app/src/debug/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".DebugApp"
|
||||||
|
tools:replace="android:name" />
|
||||||
|
</manifest>
|
||||||
58
app/src/debug/java/org/schabi/newpipe/DebugApp.kt
Normal file
58
app/src/debug/java/org/schabi/newpipe/DebugApp.kt
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.facebook.stetho.Stetho
|
||||||
|
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||||
|
import leakcanary.LeakCanary
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
|
||||||
|
class DebugApp : App() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
initStetho()
|
||||||
|
|
||||||
|
LeakCanary.config = LeakCanary.config.copy(
|
||||||
|
dumpHeap = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(this).getBoolean(
|
||||||
|
getString(
|
||||||
|
R.string.allow_heap_dumping_key
|
||||||
|
),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.addNetworkInterceptor(StethoInterceptor())
|
||||||
|
)
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initStetho() {
|
||||||
|
// Create an InitializerBuilder
|
||||||
|
val initializerBuilder = Stetho.newInitializerBuilder(this)
|
||||||
|
|
||||||
|
// Enable Chrome DevTools
|
||||||
|
initializerBuilder.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this))
|
||||||
|
|
||||||
|
// Enable command line interface
|
||||||
|
initializerBuilder.enableDumpapp(
|
||||||
|
Stetho.defaultDumperPluginsProvider(applicationContext)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use the InitializerBuilder to generate an Initializer
|
||||||
|
val initializer = initializerBuilder.build()
|
||||||
|
|
||||||
|
// Initialize Stetho with the Initializer
|
||||||
|
Stetho.initialize(initializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isDisposedRxExceptionsReported(): Boolean {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
.getBoolean(getString(R.string.allow_disposed_exceptions_key), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package org.schabi.newpipe.settings;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import leakcanary.LeakCanary;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build variant dependent (BVD) leak canary API implementation for the debug settings fragment.
|
||||||
|
* This class is loaded via reflection by
|
||||||
|
* {@link DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused") // Class is used but loaded via reflection
|
||||||
|
public class DebugSettingsBVDLeakCanary
|
||||||
|
implements DebugSettingsFragment.DebugSettingsBVDLeakCanaryAPI {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent getNewLeakDisplayActivityIntent() {
|
||||||
|
return LeakCanary.INSTANCE.newLeakDisplayActivityIntent();
|
||||||
|
}
|
||||||
|
}
|
||||||
456
app/src/main/AndroidManifest.xml
Normal file
456
app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,456 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:installLocation="auto">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
|
<!-- We need to be able to open links in the browser on API 30+ -->
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<data android:scheme="http" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.hardware.touchscreen"
|
||||||
|
android:required="false" />
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.leanback"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".App"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:banner="@mipmap/newpipe_tv_banner"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:logo="@mipmap/ic_launcher"
|
||||||
|
android:resizeableActivity="true"
|
||||||
|
android:theme="@style/OpeningTheme"
|
||||||
|
tools:ignore="AllowBackup">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:launchMode="singleTask">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name="androidx.media.session.MediaButtonReceiver"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
android:enabled="false"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data
|
||||||
|
android:name="autoStoreLocales"
|
||||||
|
android:value="true" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".player.PlayerService"
|
||||||
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".player.PlayQueueActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_play_queue"
|
||||||
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".settings.SettingsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/settings" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".about.AboutActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/title_activity_about" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".local.feed.service.FeedLoadService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
tools:node="merge" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".PanicResponderActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleInstance"
|
||||||
|
android:noHistory="true"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="info.guardianproject.panic.action.TRIGGER" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ExitActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/general_error"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".error.ErrorActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- giga get related -->
|
||||||
|
<activity
|
||||||
|
android:name=".download.DownloadActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
|
<service android:name="us.shandian.giga.service.DownloadManagerService"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".util.FilePickerActivityHelper"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:theme="@style/FilePickerThemeDark">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.GET_CONTENT" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".error.ReCaptchaActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/recaptcha" />
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/nnf_provider_paths" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".RouterActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/preferred_open_action_share_menu_title"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/RouterActivityThemeDark">
|
||||||
|
|
||||||
|
<!-- Youtube filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="youtube.com" />
|
||||||
|
<data android:host="m.youtube.com" />
|
||||||
|
<data android:host="www.youtube.com" />
|
||||||
|
<data android:host="music.youtube.com" />
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/v/" />
|
||||||
|
<data android:pathPrefix="/embed/" />
|
||||||
|
<data android:pathPrefix="/watch" />
|
||||||
|
<data android:pathPrefix="/attribution_link" />
|
||||||
|
<data android:pathPrefix="/shorts/" />
|
||||||
|
<data android:pathPrefix="/live/" />
|
||||||
|
<!-- channel prefix -->
|
||||||
|
<data android:pathPrefix="/channel/" />
|
||||||
|
<data android:pathPrefix="/user/" />
|
||||||
|
<data android:pathPrefix="/c/" />
|
||||||
|
<data android:pathPrefix="/@" />
|
||||||
|
<!-- playlist prefix -->
|
||||||
|
<data android:pathPrefix="/playlist" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="youtu.be" />
|
||||||
|
<data android:pathPrefix="/" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="www.youtube-nocookie.com" />
|
||||||
|
<data android:pathPrefix="/embed/" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="vnd.youtube" />
|
||||||
|
<data android:scheme="vnd.youtube.launch" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Hooktube filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="hooktube.com" />
|
||||||
|
<data android:host="*.hooktube.com" />
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/v/" />
|
||||||
|
<data android:pathPrefix="/embed/" />
|
||||||
|
<data android:pathPrefix="/watch" />
|
||||||
|
<!-- channel prefix -->
|
||||||
|
<data android:pathPrefix="/channel/" />
|
||||||
|
<data android:pathPrefix="/user/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Invidious filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="tubus.eduvid.org" />
|
||||||
|
<data android:host="invidio.us" />
|
||||||
|
<data android:host="dev.invidio.us" />
|
||||||
|
<data android:host="www.invidio.us" />
|
||||||
|
<data android:host="redirect.invidious.io" />
|
||||||
|
<data android:host="invidious.snopyta.org" />
|
||||||
|
<data android:host="yewtu.be" />
|
||||||
|
<data android:host="tube.connect.cafe" />
|
||||||
|
<data android:host="invidious.kavin.rocks" />
|
||||||
|
<data android:host="invidious-us.kavin.rocks" />
|
||||||
|
<data android:host="piped.kavin.rocks" />
|
||||||
|
<data android:host="invidious.site" />
|
||||||
|
<data android:host="vid.mint.lgbt" />
|
||||||
|
<data android:host="invidiou.site" />
|
||||||
|
<data android:host="invidious.fdn.fr" />
|
||||||
|
<data android:host="invidious.048596.xyz" />
|
||||||
|
<data android:host="invidious.zee.li" />
|
||||||
|
<data android:host="vid.puffyan.us" />
|
||||||
|
<data android:host="ytprivate.com" />
|
||||||
|
<data android:host="invidious.namazso.eu" />
|
||||||
|
<data android:host="invidious.silkky.cloud" />
|
||||||
|
<data android:host="invidious.exonip.de" />
|
||||||
|
<data android:host="inv.riverside.rocks" />
|
||||||
|
<data android:host="invidious.blamefran.net" />
|
||||||
|
<data android:host="invidious.moomoo.me" />
|
||||||
|
<data android:host="ytb.trom.tf" />
|
||||||
|
<data android:host="yt.cyberhost.uk" />
|
||||||
|
<data android:host="y.com.cm" />
|
||||||
|
<data android:pathPrefix="/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- y2u.be filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="y2u.be" />
|
||||||
|
<data android:pathPrefix="/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Soundcloud filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="soundcloud.com" />
|
||||||
|
<data android:host="m.soundcloud.com" />
|
||||||
|
<data android:host="on.soundcloud.com" />
|
||||||
|
<data android:host="www.soundcloud.com" />
|
||||||
|
<data android:pathPrefix="/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Share filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:mimeType="text/plain" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- media.ccc.de filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="media.ccc.de" />
|
||||||
|
<!-- video prefix -->
|
||||||
|
<data android:pathPrefix="/v/" />
|
||||||
|
<!-- channel prefix-->
|
||||||
|
<data android:pathPrefix="/c/" />
|
||||||
|
<data android:pathPrefix="/b/" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- PeerTube filter -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
|
||||||
|
<data android:host="eduvid.org" />
|
||||||
|
<data android:host="framatube.org" />
|
||||||
|
<data android:host="indymotion.fr" />
|
||||||
|
<data android:host="media.assassinate-you.net" />
|
||||||
|
<data android:host="media.fsfe.org" />
|
||||||
|
<data android:host="peertube.co.uk" />
|
||||||
|
<data android:host="peertube.cpy.re" />
|
||||||
|
<data android:host="peertube.fr" />
|
||||||
|
<data android:host="peertube.mastodon.host" />
|
||||||
|
<data android:host="peertube.stream" />
|
||||||
|
<data android:host="skeptikon.fr" />
|
||||||
|
<data android:host="tilvids.com" />
|
||||||
|
<data android:host="video.lqdn.fr" />
|
||||||
|
<data android:host="video.ploud.fr" />
|
||||||
|
<data android:host="subscribeto.me" />
|
||||||
|
|
||||||
|
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||||
|
<data android:pathPrefix="/w/" /> <!-- short video URLs -->
|
||||||
|
<data android:pathPrefix="/w/p/" /> <!-- short playlist URLs -->
|
||||||
|
<data android:pathPrefix="/accounts/" />
|
||||||
|
<data android:pathPrefix="/a/" /> <!-- short account URLs -->
|
||||||
|
<data android:pathPrefix="/video-channels/" />
|
||||||
|
<data android:pathPrefix="/c/" /> <!-- short video-channels URLs -->
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Bandcamp filter for tracks, albums and playlists -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:host="*.bandcamp.com" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Bandcamp filter for radio -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
|
||||||
|
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="http" />
|
||||||
|
<data android:scheme="https" />
|
||||||
|
<data android:sspPattern="bandcamp.com/?show=*" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".RouterActivity$FetcherService"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- opting out of sending metrics to Google in Android System WebView -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||||
|
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.keepalive.density"
|
||||||
|
android:value="true" />
|
||||||
|
<!-- Version >= 3.0. DeX Dual Mode support -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||||
|
android:value="true" />
|
||||||
|
<!-- Android Auto -->
|
||||||
|
<meta-data android:name="com.google.android.gms.car.application"
|
||||||
|
android:resource="@xml/automotive_app_desc" />
|
||||||
|
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||||
|
android:resource="@mipmap/ic_launcher" />
|
||||||
|
</application>
|
||||||
|
</manifest>
|
||||||
162
app/src/main/assets/apache2.html
Normal file
162
app/src/main/assets/apache2.html
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<title>Apache License - Version 2.0, January 2004</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Apache License<br>Version 2.0, January 2004<br>
|
||||||
|
<a href="http://www.apache.org/licenses/">http://www.apache.org/licenses/</a> </p>
|
||||||
|
<p>TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION</p>
|
||||||
|
<p><strong><a name="definitions">1. Definitions</a></strong>.</p>
|
||||||
|
<p>"License" shall mean the terms and conditions for use, reproduction, and
|
||||||
|
distribution as defined by Sections 1 through 9 of this document.</p>
|
||||||
|
<p>"Licensor" shall mean the copyright owner or entity authorized by the
|
||||||
|
copyright owner that is granting the License.</p>
|
||||||
|
<p>"Legal Entity" shall mean the union of the acting entity and all other
|
||||||
|
entities that control, are controlled by, or are under common control with
|
||||||
|
that entity. For the purposes of this definition, "control" means (i) the
|
||||||
|
power, direct or indirect, to cause the direction or management of such
|
||||||
|
entity, whether by contract or otherwise, or (ii) ownership of fifty
|
||||||
|
percent (50%) or more of the outstanding shares, or (iii) beneficial
|
||||||
|
ownership of such entity.</p>
|
||||||
|
<p>"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||||
|
permissions granted by this License.</p>
|
||||||
|
<p>"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation source,
|
||||||
|
and configuration files.</p>
|
||||||
|
<p>"Object" form shall mean any form resulting from mechanical transformation
|
||||||
|
or translation of a Source form, including but not limited to compiled
|
||||||
|
object code, generated documentation, and conversions to other media types.</p>
|
||||||
|
<p>"Work" shall mean the work of authorship, whether in Source or Object form,
|
||||||
|
made available under the License, as indicated by a copyright notice that
|
||||||
|
is included in or attached to the work (an example is provided in the
|
||||||
|
Appendix below).</p>
|
||||||
|
<p>"Derivative Works" shall mean any work, whether in Source or Object form,
|
||||||
|
that is based on (or derived from) the Work and for which the editorial
|
||||||
|
revisions, annotations, elaborations, or other modifications represent, as
|
||||||
|
a whole, an original work of authorship. For the purposes of this License,
|
||||||
|
Derivative Works shall not include works that remain separable from, or
|
||||||
|
merely link (or bind by name) to the interfaces of, the Work and Derivative
|
||||||
|
Works thereof.</p>
|
||||||
|
<p>"Contribution" shall mean any work of authorship, including the original
|
||||||
|
version of the Work and any modifications or additions to that Work or
|
||||||
|
Derivative Works thereof, that is intentionally submitted to Licensor for
|
||||||
|
inclusion in the Work by the copyright owner or by an individual or Legal
|
||||||
|
Entity authorized to submit on behalf of the copyright owner. For the
|
||||||
|
purposes of this definition, "submitted" means any form of electronic,
|
||||||
|
verbal, or written communication sent to the Licensor or its
|
||||||
|
representatives, including but not limited to communication on electronic
|
||||||
|
mailing lists, source code control systems, and issue tracking systems that
|
||||||
|
are managed by, or on behalf of, the Licensor for the purpose of discussing
|
||||||
|
and improving the Work, but excluding communication that is conspicuously
|
||||||
|
marked or otherwise designated in writing by the copyright owner as "Not a
|
||||||
|
Contribution."</p>
|
||||||
|
<p>"Contributor" shall mean Licensor and any individual or Legal Entity on
|
||||||
|
behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.</p>
|
||||||
|
<p><strong><a name="copyright">2. Grant of Copyright License</a></strong>. Subject to the
|
||||||
|
terms and conditions of this License, each Contributor hereby grants to You
|
||||||
|
a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of, publicly
|
||||||
|
display, publicly perform, sublicense, and distribute the Work and such
|
||||||
|
Derivative Works in Source or Object form.</p>
|
||||||
|
<p><strong><a name="patent">3. Grant of Patent License</a></strong>. Subject to the terms
|
||||||
|
and conditions of this License, each Contributor hereby grants to You a
|
||||||
|
perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made, use,
|
||||||
|
offer to sell, sell, import, and otherwise transfer the Work, where such
|
||||||
|
license applies only to those patent claims licensable by such Contributor
|
||||||
|
that are necessarily infringed by their Contribution(s) alone or by
|
||||||
|
combination of their Contribution(s) with the Work to which such
|
||||||
|
Contribution(s) was submitted. If You institute patent litigation against
|
||||||
|
any entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that the Work or a Contribution incorporated within the Work constitutes
|
||||||
|
direct or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate as of the
|
||||||
|
date such litigation is filed.</p>
|
||||||
|
<p><strong><a name="redistribution">4. Redistribution</a></strong>. You may reproduce and
|
||||||
|
distribute copies of the Work or Derivative Works thereof in any medium,
|
||||||
|
with or without modifications, and in Source or Object form, provided that
|
||||||
|
You meet the following conditions:</p>
|
||||||
|
<ol style="list-style: lower-latin;">
|
||||||
|
<li>You must give any other recipients of the Work or Derivative Works a
|
||||||
|
copy of this License; and</li>
|
||||||
|
|
||||||
|
<li>You must cause any modified files to carry prominent notices stating
|
||||||
|
that You changed the files; and</li>
|
||||||
|
|
||||||
|
<li>You must retain, in the Source form of any Derivative Works that You
|
||||||
|
distribute, all copyright, patent, trademark, and attribution notices from
|
||||||
|
the Source form of the Work, excluding those notices that do not pertain to
|
||||||
|
any part of the Derivative Works; and</li>
|
||||||
|
|
||||||
|
<li>If the Work includes a "NOTICE" text file as part of its distribution,
|
||||||
|
then any Derivative Works that You distribute must include a readable copy
|
||||||
|
of the attribution notices contained within such NOTICE file, excluding
|
||||||
|
those notices that do not pertain to any part of the Derivative Works, in
|
||||||
|
at least one of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or documentation,
|
||||||
|
if provided along with the Derivative Works; or, within a display generated
|
||||||
|
by the Derivative Works, if and wherever such third-party notices normally
|
||||||
|
appear. The contents of the NOTICE file are for informational purposes only
|
||||||
|
and do not modify the License. You may add Your own attribution notices
|
||||||
|
within Derivative Works that You distribute, alongside or as an addendum to
|
||||||
|
the NOTICE text from the Work, provided that such additional attribution
|
||||||
|
notices cannot be construed as modifying the License.
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
You may add Your own copyright statement to Your modifications and may
|
||||||
|
provide additional or different license terms and conditions for use,
|
||||||
|
reproduction, or distribution of Your modifications, or for any such
|
||||||
|
Derivative Works as a whole, provided Your use, reproduction, and
|
||||||
|
distribution of the Work otherwise complies with the conditions stated in
|
||||||
|
this License.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p><strong><a name="contributions">5. Submission of Contributions</a></strong>. Unless You
|
||||||
|
explicitly state otherwise, any Contribution intentionally submitted for
|
||||||
|
inclusion in the Work by You to the Licensor shall be under the terms and
|
||||||
|
conditions of this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify the
|
||||||
|
terms of any separate license agreement you may have executed with Licensor
|
||||||
|
regarding such Contributions.</p>
|
||||||
|
<p><strong><a name="trademarks">6. Trademarks</a></strong>. This License does not grant
|
||||||
|
permission to use the trade names, trademarks, service marks, or product
|
||||||
|
names of the Licensor, except as required for reasonable and customary use
|
||||||
|
in describing the origin of the Work and reproducing the content of the
|
||||||
|
NOTICE file.</p>
|
||||||
|
<p><strong><a name="no-warranty">7. Disclaimer of Warranty</a></strong>. Unless required by
|
||||||
|
applicable law or agreed to in writing, Licensor provides the Work (and
|
||||||
|
each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT
|
||||||
|
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
|
||||||
|
without limitation, any warranties or conditions of TITLE,
|
||||||
|
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You
|
||||||
|
are solely responsible for determining the appropriateness of using or
|
||||||
|
redistributing the Work and assume any risks associated with Your exercise
|
||||||
|
of permissions under this License.</p>
|
||||||
|
<p><strong><a name="no-liability">8. Limitation of Liability</a></strong>. In no event and
|
||||||
|
under no legal theory, whether in tort (including negligence), contract, or
|
||||||
|
otherwise, unless required by applicable law (such as deliberate and
|
||||||
|
grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a result
|
||||||
|
of this License or out of the use or inability to use the Work (including
|
||||||
|
but not limited to damages for loss of goodwill, work stoppage, computer
|
||||||
|
failure or malfunction, or any and all other commercial damages or losses),
|
||||||
|
even if such Contributor has been advised of the possibility of such
|
||||||
|
damages.</p>
|
||||||
|
<p><strong><a name="additional">9. Accepting Warranty or Additional Liability</a></strong>.
|
||||||
|
While redistributing the Work or Derivative Works thereof, You may choose
|
||||||
|
to offer, and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this License.
|
||||||
|
However, in accepting such obligations, You may act only on Your own behalf
|
||||||
|
and on Your sole responsibility, not on behalf of any other Contributor,
|
||||||
|
and only if You agree to indemnify, defend, and hold each Contributor
|
||||||
|
harmless for any liability incurred by, or claims asserted against, such
|
||||||
|
Contributor by reason of your accepting any such warranty or additional
|
||||||
|
liability.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
245
app/src/main/assets/epl1.html
Normal file
245
app/src/main/assets/epl1.html
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<!-- saved from url=(0050)https://www.eclipse.org/org/documents/epl-v10.html -->
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
|
||||||
|
<title>Eclipse Public License - Version 1.0</title>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body cz-shortcut-listen="true" lang="EN-US">
|
||||||
|
|
||||||
|
<h2>Eclipse Public License - v 1.0</h2>
|
||||||
|
|
||||||
|
<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
|
||||||
|
PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR
|
||||||
|
DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
|
||||||
|
AGREEMENT.</p>
|
||||||
|
|
||||||
|
<p><b>1. DEFINITIONS</b></p>
|
||||||
|
|
||||||
|
<p>"Contribution" means:</p>
|
||||||
|
|
||||||
|
<p class="list">a) in the case of the initial Contributor, the initial
|
||||||
|
code and documentation distributed under this Agreement, and</p>
|
||||||
|
<p class="list">b) in the case of each subsequent Contributor:</p>
|
||||||
|
<p class="list">i) changes to the Program, and</p>
|
||||||
|
<p class="list">ii) additions to the Program;</p>
|
||||||
|
<p class="list">where such changes and/or additions to the Program
|
||||||
|
originate from and are distributed by that particular Contributor. A
|
||||||
|
Contribution 'originates' from a Contributor if it was added to the
|
||||||
|
Program by such Contributor itself or anyone acting on such
|
||||||
|
Contributor's behalf. Contributions do not include additions to the
|
||||||
|
Program which: (i) are separate modules of software distributed in
|
||||||
|
conjunction with the Program under their own license agreement, and (ii)
|
||||||
|
are not derivative works of the Program.</p>
|
||||||
|
|
||||||
|
<p>"Contributor" means any person or entity that distributes
|
||||||
|
the Program.</p>
|
||||||
|
|
||||||
|
<p>"Licensed Patents" mean patent claims licensable by a
|
||||||
|
Contributor which are necessarily infringed by the use or sale of its
|
||||||
|
Contribution alone or when combined with the Program.</p>
|
||||||
|
|
||||||
|
<p>"Program" means the Contributions distributed in accordance
|
||||||
|
with this Agreement.</p>
|
||||||
|
|
||||||
|
<p>"Recipient" means anyone who receives the Program under
|
||||||
|
this Agreement, including all Contributors.</p>
|
||||||
|
|
||||||
|
<p><b>2. GRANT OF RIGHTS</b></p>
|
||||||
|
|
||||||
|
<p class="list">a) Subject to the terms of this Agreement, each
|
||||||
|
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||||
|
royalty-free copyright license to reproduce, prepare derivative works
|
||||||
|
of, publicly display, publicly perform, distribute and sublicense the
|
||||||
|
Contribution of such Contributor, if any, and such derivative works, in
|
||||||
|
source code and object code form.</p>
|
||||||
|
|
||||||
|
<p class="list">b) Subject to the terms of this Agreement, each
|
||||||
|
Contributor hereby grants Recipient a non-exclusive, worldwide,
|
||||||
|
royalty-free patent license under Licensed Patents to make, use, sell,
|
||||||
|
offer to sell, import and otherwise transfer the Contribution of such
|
||||||
|
Contributor, if any, in source code and object code form. This patent
|
||||||
|
license shall apply to the combination of the Contribution and the
|
||||||
|
Program if, at the time the Contribution is added by the Contributor,
|
||||||
|
such addition of the Contribution causes such combination to be covered
|
||||||
|
by the Licensed Patents. The patent license shall not apply to any other
|
||||||
|
combinations which include the Contribution. No hardware per se is
|
||||||
|
licensed hereunder.</p>
|
||||||
|
|
||||||
|
<p class="list">c) Recipient understands that although each Contributor
|
||||||
|
grants the licenses to its Contributions set forth herein, no assurances
|
||||||
|
are provided by any Contributor that the Program does not infringe the
|
||||||
|
patent or other intellectual property rights of any other entity. Each
|
||||||
|
Contributor disclaims any liability to Recipient for claims brought by
|
||||||
|
any other entity based on infringement of intellectual property rights
|
||||||
|
or otherwise. As a condition to exercising the rights and licenses
|
||||||
|
granted hereunder, each Recipient hereby assumes sole responsibility to
|
||||||
|
secure any other intellectual property rights needed, if any. For
|
||||||
|
example, if a third party patent license is required to allow Recipient
|
||||||
|
to distribute the Program, it is Recipient's responsibility to acquire
|
||||||
|
that license before distributing the Program.</p>
|
||||||
|
|
||||||
|
<p class="list">d) Each Contributor represents that to its knowledge it
|
||||||
|
has sufficient copyright rights in its Contribution, if any, to grant
|
||||||
|
the copyright license set forth in this Agreement.</p>
|
||||||
|
|
||||||
|
<p><b>3. REQUIREMENTS</b></p>
|
||||||
|
|
||||||
|
<p>A Contributor may choose to distribute the Program in object code
|
||||||
|
form under its own license agreement, provided that:</p>
|
||||||
|
|
||||||
|
<p class="list">a) it complies with the terms and conditions of this
|
||||||
|
Agreement; and</p>
|
||||||
|
|
||||||
|
<p class="list">b) its license agreement:</p>
|
||||||
|
|
||||||
|
<p class="list">i) effectively disclaims on behalf of all Contributors
|
||||||
|
all warranties and conditions, express and implied, including warranties
|
||||||
|
or conditions of title and non-infringement, and implied warranties or
|
||||||
|
conditions of merchantability and fitness for a particular purpose;</p>
|
||||||
|
|
||||||
|
<p class="list">ii) effectively excludes on behalf of all Contributors
|
||||||
|
all liability for damages, including direct, indirect, special,
|
||||||
|
incidental and consequential damages, such as lost profits;</p>
|
||||||
|
|
||||||
|
<p class="list">iii) states that any provisions which differ from this
|
||||||
|
Agreement are offered by that Contributor alone and not by any other
|
||||||
|
party; and</p>
|
||||||
|
|
||||||
|
<p class="list">iv) states that source code for the Program is available
|
||||||
|
from such Contributor, and informs licensees how to obtain it in a
|
||||||
|
reasonable manner on or through a medium customarily used for software
|
||||||
|
exchange.</p>
|
||||||
|
|
||||||
|
<p>When the Program is made available in source code form:</p>
|
||||||
|
|
||||||
|
<p class="list">a) it must be made available under this Agreement; and</p>
|
||||||
|
|
||||||
|
<p class="list">b) a copy of this Agreement must be included with each
|
||||||
|
copy of the Program.</p>
|
||||||
|
|
||||||
|
<p>Contributors may not remove or alter any copyright notices contained
|
||||||
|
within the Program.</p>
|
||||||
|
|
||||||
|
<p>Each Contributor must identify itself as the originator of its
|
||||||
|
Contribution, if any, in a manner that reasonably allows subsequent
|
||||||
|
Recipients to identify the originator of the Contribution.</p>
|
||||||
|
|
||||||
|
<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
|
||||||
|
|
||||||
|
<p>Commercial distributors of software may accept certain
|
||||||
|
responsibilities with respect to end users, business partners and the
|
||||||
|
like. While this license is intended to facilitate the commercial use of
|
||||||
|
the Program, the Contributor who includes the Program in a commercial
|
||||||
|
product offering should do so in a manner which does not create
|
||||||
|
potential liability for other Contributors. Therefore, if a Contributor
|
||||||
|
includes the Program in a commercial product offering, such Contributor
|
||||||
|
("Commercial Contributor") hereby agrees to defend and
|
||||||
|
indemnify every other Contributor ("Indemnified Contributor")
|
||||||
|
against any losses, damages and costs (collectively "Losses")
|
||||||
|
arising from claims, lawsuits and other legal actions brought by a third
|
||||||
|
party against the Indemnified Contributor to the extent caused by the
|
||||||
|
acts or omissions of such Commercial Contributor in connection with its
|
||||||
|
distribution of the Program in a commercial product offering. The
|
||||||
|
obligations in this section do not apply to any claims or Losses
|
||||||
|
relating to any actual or alleged intellectual property infringement. In
|
||||||
|
order to qualify, an Indemnified Contributor must: a) promptly notify
|
||||||
|
the Commercial Contributor in writing of such claim, and b) allow the
|
||||||
|
Commercial Contributor to control, and cooperate with the Commercial
|
||||||
|
Contributor in, the defense and any related settlement negotiations. The
|
||||||
|
Indemnified Contributor may participate in any such claim at its own
|
||||||
|
expense.</p>
|
||||||
|
|
||||||
|
<p>For example, a Contributor might include the Program in a commercial
|
||||||
|
product offering, Product X. That Contributor is then a Commercial
|
||||||
|
Contributor. If that Commercial Contributor then makes performance
|
||||||
|
claims, or offers warranties related to Product X, those performance
|
||||||
|
claims and warranties are such Commercial Contributor's responsibility
|
||||||
|
alone. Under this section, the Commercial Contributor would have to
|
||||||
|
defend claims against the other Contributors related to those
|
||||||
|
performance claims and warranties, and if a court requires any other
|
||||||
|
Contributor to pay any damages as a result, the Commercial Contributor
|
||||||
|
must pay those damages.</p>
|
||||||
|
|
||||||
|
<p><b>5. NO WARRANTY</b></p>
|
||||||
|
|
||||||
|
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
|
||||||
|
PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||||
|
OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
|
||||||
|
ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
|
||||||
|
OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
|
||||||
|
responsible for determining the appropriateness of using and
|
||||||
|
distributing the Program and assumes all risks associated with its
|
||||||
|
exercise of rights under this Agreement , including but not limited to
|
||||||
|
the risks and costs of program errors, compliance with applicable laws,
|
||||||
|
damage to or loss of data, programs or equipment, and unavailability or
|
||||||
|
interruption of operations.</p>
|
||||||
|
|
||||||
|
<p><b>6. DISCLAIMER OF LIABILITY</b></p>
|
||||||
|
|
||||||
|
<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
|
||||||
|
NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
|
||||||
|
WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
|
||||||
|
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||||
|
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
|
||||||
|
DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
|
||||||
|
HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
|
||||||
|
|
||||||
|
<p><b>7. GENERAL</b></p>
|
||||||
|
|
||||||
|
<p>If any provision of this Agreement is invalid or unenforceable under
|
||||||
|
applicable law, it shall not affect the validity or enforceability of
|
||||||
|
the remainder of the terms of this Agreement, and without further action
|
||||||
|
by the parties hereto, such provision shall be reformed to the minimum
|
||||||
|
extent necessary to make such provision valid and enforceable.</p>
|
||||||
|
|
||||||
|
<p>If Recipient institutes patent litigation against any entity
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that the
|
||||||
|
Program itself (excluding combinations of the Program with other
|
||||||
|
software or hardware) infringes such Recipient's patent(s), then such
|
||||||
|
Recipient's rights granted under Section 2(b) shall terminate as of the
|
||||||
|
date such litigation is filed.</p>
|
||||||
|
|
||||||
|
<p>All Recipient's rights under this Agreement shall terminate if it
|
||||||
|
fails to comply with any of the material terms or conditions of this
|
||||||
|
Agreement and does not cure such failure in a reasonable period of time
|
||||||
|
after becoming aware of such noncompliance. If all Recipient's rights
|
||||||
|
under this Agreement terminate, Recipient agrees to cease use and
|
||||||
|
distribution of the Program as soon as reasonably practicable. However,
|
||||||
|
Recipient's obligations under this Agreement and any licenses granted by
|
||||||
|
Recipient relating to the Program shall continue and survive.</p>
|
||||||
|
|
||||||
|
<p>Everyone is permitted to copy and distribute copies of this
|
||||||
|
Agreement, but in order to avoid inconsistency the Agreement is
|
||||||
|
copyrighted and may only be modified in the following manner. The
|
||||||
|
Agreement Steward reserves the right to publish new versions (including
|
||||||
|
revisions) of this Agreement from time to time. No one other than the
|
||||||
|
Agreement Steward has the right to modify this Agreement. The Eclipse
|
||||||
|
Foundation is the initial Agreement Steward. The Eclipse Foundation may
|
||||||
|
assign the responsibility to serve as the Agreement Steward to a
|
||||||
|
suitable separate entity. Each new version of the Agreement will be
|
||||||
|
given a distinguishing version number. The Program (including
|
||||||
|
Contributions) may always be distributed subject to the version of the
|
||||||
|
Agreement under which it was received. In addition, after a new version
|
||||||
|
of the Agreement is published, Contributor may elect to distribute the
|
||||||
|
Program (including its Contributions) under the new version. Except as
|
||||||
|
expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
|
||||||
|
rights or licenses to the intellectual property of any Contributor under
|
||||||
|
this Agreement, whether expressly, by implication, estoppel or
|
||||||
|
otherwise. All rights in the Program not expressly granted under this
|
||||||
|
Agreement are reserved.</p>
|
||||||
|
|
||||||
|
<p>This Agreement is governed by the laws of the State of New York and
|
||||||
|
the intellectual property laws of the United States of America. No party
|
||||||
|
to this Agreement will bring a legal action under this Agreement more
|
||||||
|
than one year after the cause of action arose. Each party waives its
|
||||||
|
rights to a jury trial in any resulting litigation.</p>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
639
app/src/main/assets/gpl_3.html
Normal file
639
app/src/main/assets/gpl_3.html
Normal file
|
|
@ -0,0 +1,639 @@
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||||
|
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||||
|
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<title>GNU General Public License v3.0 - GNU Project - Free Software Foundation (FSF)</title>
|
||||||
|
<link rel="alternate" type="application/rdf+xml"
|
||||||
|
href="http://www.gnu.org/licenses/gpl-3.0.rdf" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3 style="text-align: center;">GNU GENERAL PUBLIC LICENSE</h3>
|
||||||
|
<p style="text-align: center;">Version 3, 29 June 2007</p>
|
||||||
|
|
||||||
|
<p>Copyright © 2007 Free Software Foundation, Inc.
|
||||||
|
<<a href="http://fsf.org/">http://fsf.org/</a>></p><p>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.</p>
|
||||||
|
|
||||||
|
<h3><a name="preamble"></a>Preamble</h3>
|
||||||
|
|
||||||
|
<p>The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.</p>
|
||||||
|
|
||||||
|
<p>The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.</p>
|
||||||
|
|
||||||
|
<p>When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.</p>
|
||||||
|
|
||||||
|
<p>To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.</p>
|
||||||
|
|
||||||
|
<p>For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.</p>
|
||||||
|
|
||||||
|
<p>Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.</p>
|
||||||
|
|
||||||
|
<p>For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.</p>
|
||||||
|
|
||||||
|
<p>Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.</p>
|
||||||
|
|
||||||
|
<p>Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.</p>
|
||||||
|
|
||||||
|
<p>The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.</p>
|
||||||
|
|
||||||
|
<h3><a name="terms"></a>TERMS AND CONDITIONS</h3>
|
||||||
|
|
||||||
|
<h4><a name="section0"></a>0. Definitions.</h4>
|
||||||
|
|
||||||
|
<p>“This License” refers to version 3 of the GNU General Public License.</p>
|
||||||
|
|
||||||
|
<p>“Copyright” also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.</p>
|
||||||
|
|
||||||
|
<p>“The Program” refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as “you”. “Licensees” and
|
||||||
|
“recipients” may be individuals or organizations.</p>
|
||||||
|
|
||||||
|
<p>To “modify” a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a “modified version” of the
|
||||||
|
earlier work or a work “based on” the earlier work.</p>
|
||||||
|
|
||||||
|
<p>A “covered work” means either the unmodified Program or a work based
|
||||||
|
on the Program.</p>
|
||||||
|
|
||||||
|
<p>To “propagate” a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.</p>
|
||||||
|
|
||||||
|
<p>To “convey” a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.</p>
|
||||||
|
|
||||||
|
<p>An interactive user interface displays “Appropriate Legal Notices”
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.</p>
|
||||||
|
|
||||||
|
<h4><a name="section1"></a>1. Source Code.</h4>
|
||||||
|
|
||||||
|
<p>The “source code” for a work means the preferred form of the work
|
||||||
|
for making modifications to it. “Object code” means any non-source
|
||||||
|
form of a work.</p>
|
||||||
|
|
||||||
|
<p>A “Standard Interface” means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.</p>
|
||||||
|
|
||||||
|
<p>The “System Libraries” of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
“Major Component”, in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.</p>
|
||||||
|
|
||||||
|
<p>The “Corresponding Source” for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.</p>
|
||||||
|
|
||||||
|
<p>The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.</p>
|
||||||
|
|
||||||
|
<p>The Corresponding Source for a work in source code form is that
|
||||||
|
same work.</p>
|
||||||
|
|
||||||
|
<h4><a name="section2"></a>2. Basic Permissions.</h4>
|
||||||
|
|
||||||
|
<p>All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.</p>
|
||||||
|
|
||||||
|
<p>You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.</p>
|
||||||
|
|
||||||
|
<p>Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.</p>
|
||||||
|
|
||||||
|
<h4><a name="section3"></a>3. Protecting Users' Legal Rights From Anti-Circumvention Law.</h4>
|
||||||
|
|
||||||
|
<p>No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.</p>
|
||||||
|
|
||||||
|
<p>When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.</p>
|
||||||
|
|
||||||
|
<h4><a name="section4"></a>4. Conveying Verbatim Copies.</h4>
|
||||||
|
|
||||||
|
<p>You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.</p>
|
||||||
|
|
||||||
|
<p>You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.</p>
|
||||||
|
|
||||||
|
<h4><a name="section5"></a>5. Conveying Modified Source Versions.</h4>
|
||||||
|
|
||||||
|
<p>You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.</li>
|
||||||
|
|
||||||
|
<li>b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
“keep intact all notices”.</li>
|
||||||
|
|
||||||
|
<li>c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.</li>
|
||||||
|
|
||||||
|
<li>d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
“aggregate” if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.</p>
|
||||||
|
|
||||||
|
<h4><a name="section6"></a>6. Conveying Non-Source Forms.</h4>
|
||||||
|
|
||||||
|
<p>You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.</li>
|
||||||
|
|
||||||
|
<li>b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.</li>
|
||||||
|
|
||||||
|
<li>c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.</li>
|
||||||
|
|
||||||
|
<li>d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.</li>
|
||||||
|
|
||||||
|
<li>e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.</p>
|
||||||
|
|
||||||
|
<p>A “User Product” is either (1) a “consumer product”, which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, “normally used” refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.</p>
|
||||||
|
|
||||||
|
<p>“Installation Information” for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.</p>
|
||||||
|
|
||||||
|
<p>If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).</p>
|
||||||
|
|
||||||
|
<p>The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.</p>
|
||||||
|
|
||||||
|
<p>Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.</p>
|
||||||
|
|
||||||
|
<h4><a name="section7"></a>7. Additional Terms.</h4>
|
||||||
|
|
||||||
|
<p>“Additional permissions” are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.</p>
|
||||||
|
|
||||||
|
<p>When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.</p>
|
||||||
|
|
||||||
|
<p>Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or</li>
|
||||||
|
|
||||||
|
<li>b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or</li>
|
||||||
|
|
||||||
|
<li>c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or</li>
|
||||||
|
|
||||||
|
<li>d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or</li>
|
||||||
|
|
||||||
|
<li>e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or</li>
|
||||||
|
|
||||||
|
<li>f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>All other non-permissive additional terms are considered “further
|
||||||
|
restrictions” within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.</p>
|
||||||
|
|
||||||
|
<p>If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.</p>
|
||||||
|
|
||||||
|
<p>Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.</p>
|
||||||
|
|
||||||
|
<h4><a name="section8"></a>8. Termination.</h4>
|
||||||
|
|
||||||
|
<p>You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).</p>
|
||||||
|
|
||||||
|
<p>However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.</p>
|
||||||
|
|
||||||
|
<p>Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.</p>
|
||||||
|
|
||||||
|
<p>Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.</p>
|
||||||
|
|
||||||
|
<h4><a name="section9"></a>9. Acceptance Not Required for Having Copies.</h4>
|
||||||
|
|
||||||
|
<p>You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.</p>
|
||||||
|
|
||||||
|
<h4><a name="section10"></a>10. Automatic Licensing of Downstream Recipients.</h4>
|
||||||
|
|
||||||
|
<p>Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.</p>
|
||||||
|
|
||||||
|
<p>An “entity transaction” is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.</p>
|
||||||
|
|
||||||
|
<p>You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.</p>
|
||||||
|
|
||||||
|
<h4><a name="section11"></a>11. Patents.</h4>
|
||||||
|
|
||||||
|
<p>A “contributor” is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's “contributor version”.</p>
|
||||||
|
|
||||||
|
<p>A contributor's “essential patent claims” are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, “control” includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.</p>
|
||||||
|
|
||||||
|
<p>Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.</p>
|
||||||
|
|
||||||
|
<p>In the following three paragraphs, a “patent license” is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To “grant” such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.</p>
|
||||||
|
|
||||||
|
<p>If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. “Knowingly relying” means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.</p>
|
||||||
|
|
||||||
|
<p>If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.</p>
|
||||||
|
|
||||||
|
<p>A patent license is “discriminatory” if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.</p>
|
||||||
|
|
||||||
|
<p>Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.</p>
|
||||||
|
|
||||||
|
<h4><a name="section12"></a>12. No Surrender of Others' Freedom.</h4>
|
||||||
|
|
||||||
|
<p>If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.</p>
|
||||||
|
|
||||||
|
<h4><a name="section13"></a>13. Use with the GNU Affero General Public License.</h4>
|
||||||
|
|
||||||
|
<p>Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.</p>
|
||||||
|
|
||||||
|
<h4><a name="section14"></a>14. Revised Versions of this License.</h4>
|
||||||
|
|
||||||
|
<p>The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.</p>
|
||||||
|
|
||||||
|
<p>Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License “or any later version” applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.</p>
|
||||||
|
|
||||||
|
<p>If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.</p>
|
||||||
|
|
||||||
|
<p>Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.</p>
|
||||||
|
|
||||||
|
<h4><a name="section15"></a>15. Disclaimer of Warranty.</h4>
|
||||||
|
|
||||||
|
<p>THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.</p>
|
||||||
|
|
||||||
|
<h4><a name="section16"></a>16. Limitation of Liability.</h4>
|
||||||
|
|
||||||
|
<p>IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.</p>
|
||||||
|
|
||||||
|
<h4><a name="section17"></a>17. Interpretation of Sections 15 and 16.</h4>
|
||||||
|
|
||||||
|
<p>If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.</p>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
26
app/src/main/assets/mit.html
Normal file
26
app/src/main/assets/mit.html
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p>Copyright (c) <year> <copyright holders></p>
|
||||||
|
|
||||||
|
<p>Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.</p>
|
||||||
|
<p>
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.<br />
|
||||||
|
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
261
app/src/main/assets/mpl2.html
Normal file
261
app/src/main/assets/mpl2.html
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- saved from url=(0038)https://www.mozilla.org/en-US/MPL/2.0/ -->
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||||
|
<title>Mozilla Public License, version 2.0</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 id="mozilla-public-license-version-2.0">Mozilla Public License<br>Version 2.0</h1>
|
||||||
|
<h2 id="definitions">1. Definitions</h2>
|
||||||
|
<dl>
|
||||||
|
<dt>1.1. “Contributor”</dt>
|
||||||
|
<dd><p>means each individual or legal entity that creates, contributes to the creation of, or
|
||||||
|
owns Covered Software.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.2. “Contributor Version”</dt>
|
||||||
|
<dd><p>means the combination of the Contributions of others (if any) used by a Contributor and
|
||||||
|
that particular Contributor’s Contribution.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.3. “Contribution”</dt>
|
||||||
|
<dd><p>means Covered Software of a particular Contributor.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.4. “Covered Software”</dt>
|
||||||
|
<dd><p>means Source Code Form to which the initial Contributor has attached the notice in
|
||||||
|
Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source
|
||||||
|
Code Form, in each case including portions thereof.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.5. “Incompatible With Secondary Licenses”</dt>
|
||||||
|
<dd><p>means</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><p>that the initial Contributor has attached the notice described in Exhibit B to
|
||||||
|
the Covered Software; or</p></li>
|
||||||
|
<li><p>that the Covered Software was made available under the terms of version 1.1 or
|
||||||
|
earlier of the License, but not also under the terms of a Secondary License.</p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</dd>
|
||||||
|
<dt>1.6. “Executable Form”</dt>
|
||||||
|
<dd><p>means any form of the work other than Source Code Form.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.7. “Larger Work”</dt>
|
||||||
|
<dd><p>means a work that combines Covered Software with other material, in a separate file or
|
||||||
|
files, that is not Covered Software.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.8. “License”</dt>
|
||||||
|
<dd><p>means this document.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.9. “Licensable”</dt>
|
||||||
|
<dd><p>means having the right to grant, to the maximum extent possible, whether at the time of
|
||||||
|
the initial grant or subsequently, any and all of the rights conveyed by this License.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.10. “Modifications”</dt>
|
||||||
|
<dd><p>means any of the following:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><p>any file in Source Code Form that results from an addition to, deletion from, or
|
||||||
|
modification of the contents of Covered Software; or</p></li>
|
||||||
|
<li><p>any new file in Source Code Form that contains any Covered Software.</p></li>
|
||||||
|
</ol>
|
||||||
|
</dd>
|
||||||
|
<dt>1.11. “Patent Claims” of a Contributor</dt>
|
||||||
|
<dd><p>means any patent claim(s), including without limitation, method, process, and apparatus
|
||||||
|
claims, in any patent Licensable by such Contributor that would be infringed, but for the
|
||||||
|
grant of the License, by the making, using, selling, offering for sale, having made, import,
|
||||||
|
or transfer of either its Contributions or its Contributor Version.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.12. “Secondary License”</dt>
|
||||||
|
<dd><p>means either the GNU General Public License, Version 2.0, the GNU Lesser General Public
|
||||||
|
License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later
|
||||||
|
versions of those licenses.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.13. “Source Code Form”</dt>
|
||||||
|
<dd><p>means the form of the work preferred for making modifications.</p>
|
||||||
|
</dd>
|
||||||
|
<dt>1.14. “You” (or “Your”)</dt>
|
||||||
|
<dd><p>means an individual or a legal entity exercising rights under this License. For legal
|
||||||
|
entities, “You” includes any entity that controls, is controlled by, or is under common
|
||||||
|
control with You. For purposes of this definition, “control” means (a) the power, direct or
|
||||||
|
indirect, to cause the direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or
|
||||||
|
beneficial ownership of such entity.</p>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<h2 id="license-grants-and-conditions">2. License Grants and Conditions</h2>
|
||||||
|
<h3 id="grants">2.1. Grants</h3>
|
||||||
|
<p>Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><p>under intellectual property rights (other than patent or trademark) Licensable by such
|
||||||
|
Contributor to use, reproduce, make available, modify, display, perform, distribute, and
|
||||||
|
otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and</p></li>
|
||||||
|
<li><p>under Patent Claims of such Contributor to make, use, sell, offer for sale, have made,
|
||||||
|
import, and otherwise transfer either its Contributions or its Contributor Version.</p></li>
|
||||||
|
</ol>
|
||||||
|
<h3 id="effective-date">2.2. Effective Date</h3>
|
||||||
|
<p>The licenses granted in Section 2.1 with respect to any Contribution become effective for
|
||||||
|
each Contribution on the date the Contributor first distributes such Contribution.</p>
|
||||||
|
<h3 id="limitations-on-grant-scope">2.3. Limitations on Grant Scope</h3>
|
||||||
|
<p>The licenses granted in this Section 2 are the only rights granted under this License. No
|
||||||
|
additional rights or licenses will be implied from the distribution or licensing of Covered
|
||||||
|
Software under this License. Notwithstanding Section 2.1(b) above, no patent license is
|
||||||
|
granted by a Contributor:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><p>for any code that a Contributor has removed from Covered Software; or</p></li>
|
||||||
|
<li><p>for infringements caused by: (i) Your and any other third party’s modifications of
|
||||||
|
Covered Software, or (ii) the combination of its Contributions with other software (except
|
||||||
|
as part of its Contributor Version); or</p></li>
|
||||||
|
<li><p>under Patent Claims infringed by Covered Software in the absence of its
|
||||||
|
Contributions.</p></li>
|
||||||
|
</ol>
|
||||||
|
<p>This License does not grant any rights in the trademarks, service marks, or logos of any
|
||||||
|
Contributor (except as may be necessary to comply with the notice requirements in Section 3.4).</p>
|
||||||
|
<h3 id="subsequent-licenses">2.4. Subsequent Licenses</h3>
|
||||||
|
<p>No Contributor makes additional grants as a result of Your choice to distribute the Covered
|
||||||
|
Software under a subsequent version of this License (see Section 10.2) or under the terms
|
||||||
|
of a Secondary License (if permitted under the terms of Section 3.3).</p>
|
||||||
|
<h3 id="representation">2.5. Representation</h3>
|
||||||
|
<p>Each Contributor represents that the Contributor believes its Contributions are its original
|
||||||
|
creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by
|
||||||
|
this License.</p>
|
||||||
|
<h3 id="fair-use">2.6. Fair Use</h3>
|
||||||
|
<p>This License is not intended to limit any rights You have under applicable copyright doctrines of
|
||||||
|
fair use, fair dealing, or other equivalents.</p>
|
||||||
|
<h3 id="conditions">2.7. Conditions</h3>
|
||||||
|
<p>Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.</p>
|
||||||
|
<h2 id="responsibilities">3. Responsibilities</h2>
|
||||||
|
<h3 id="distribution-of-source-form">3.1. Distribution of Source Form</h3>
|
||||||
|
<p>All distribution of Covered Software in Source Code Form, including any Modifications that You
|
||||||
|
create or to which You contribute, must be under the terms of this License. You must inform
|
||||||
|
recipients that the Source Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not attempt to alter or
|
||||||
|
restrict the recipients’ rights in the Source Code Form.</p>
|
||||||
|
<h3 id="distribution-of-executable-form">3.2. Distribution of Executable Form</h3>
|
||||||
|
<p>If You distribute Covered Software in Executable Form then:</p>
|
||||||
|
<ol type="a">
|
||||||
|
<li><p>such Covered Software must also be made available in Source Code Form, as described in
|
||||||
|
Section 3.1, and You must inform recipients of the Executable Form how they can obtain
|
||||||
|
a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and</p></li>
|
||||||
|
<li><p>You may distribute such Executable Form under the terms of this License, or sublicense it
|
||||||
|
under different terms, provided that the license for the Executable Form does not attempt to
|
||||||
|
limit or alter the recipients’ rights in the Source Code Form under this License.</p></li>
|
||||||
|
</ol>
|
||||||
|
<h3 id="distribution-of-a-larger-work">3.3. Distribution of a Larger Work</h3>
|
||||||
|
<p>You may create and distribute a Larger Work under terms of Your choice, provided that You also
|
||||||
|
comply with the requirements of this License for the Covered Software. If the Larger Work is a
|
||||||
|
combination of Covered Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this License permits You to
|
||||||
|
additionally distribute such Covered Software under the terms of such Secondary License(s), so
|
||||||
|
that the recipient of the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary License(s).</p>
|
||||||
|
<h3 id="notices">3.4. Notices</h3>
|
||||||
|
<p>You may not remove or alter the substance of any license notices (including copyright notices,
|
||||||
|
patent notices, disclaimers of warranty, or limitations of liability) contained within the
|
||||||
|
Source Code Form of the Covered Software, except that You may alter any license notices to the
|
||||||
|
extent required to remedy known factual inaccuracies.</p>
|
||||||
|
<h3 id="application-of-additional-terms">3.5. Application of Additional Terms</h3>
|
||||||
|
<p>You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability
|
||||||
|
obligations to one or more recipients of Covered Software. However, You may do so only on Your
|
||||||
|
own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by You alone, and You
|
||||||
|
hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a
|
||||||
|
result of warranty, support, indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any jurisdiction.</p>
|
||||||
|
<h2 id="inability-to-comply-due-to-statute-or-regulation">4. Inability to Comply Due to Statute or
|
||||||
|
Regulation</h2>
|
||||||
|
<p>If it is impossible for You to comply with any of the terms of this License with respect to some
|
||||||
|
or all of the Covered Software due to statute, judicial order, or regulation then You must: (a)
|
||||||
|
comply with the terms of this License to the maximum extent possible; and (b) describe the
|
||||||
|
limitations and the code they affect. Such description must be placed in a text file included
|
||||||
|
with all distributions of the Covered Software under this License. Except to the extent
|
||||||
|
prohibited by statute or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.</p>
|
||||||
|
<h2 id="termination">5. Termination</h2>
|
||||||
|
<p>5.1. The rights granted under this License will terminate automatically if You fail to comply
|
||||||
|
with any of its terms. However, if You become compliant, then the rights granted under this
|
||||||
|
License from a particular Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such
|
||||||
|
Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days
|
||||||
|
after You have come back into compliance. Moreover, Your grants from a particular Contributor
|
||||||
|
are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by
|
||||||
|
some reasonable means, this is the first time You have received notice of non-compliance with
|
||||||
|
this License from such Contributor, and You become compliant prior to 30 days after Your receipt
|
||||||
|
of the notice.</p>
|
||||||
|
<p>5.2. If You initiate litigation against any entity by asserting a patent infringement claim
|
||||||
|
(excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a
|
||||||
|
Contributor Version directly or indirectly infringes any patent, then the rights granted to You
|
||||||
|
by any and all Contributors for the Covered Software under Section 2.1 of this License
|
||||||
|
shall terminate.</p>
|
||||||
|
<p>5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license
|
||||||
|
agreements (excluding distributors and resellers) which have been validly granted by You or Your
|
||||||
|
distributors under this License prior to termination shall survive termination.</p>
|
||||||
|
<h2 id="disclaimer-of-warranty">6. Disclaimer of Warranty</h2>
|
||||||
|
<p><em>Covered Software is provided under this License on an “as is” basis, without warranty of any
|
||||||
|
kind, either expressed, implied, or statutory, including, without limitation, warranties that
|
||||||
|
the Covered Software is free of defects, merchantable, fit for a particular purpose or
|
||||||
|
non-infringing. The entire risk as to the quality and performance of the Covered Software is
|
||||||
|
with You. Should any Covered Software prove defective in any respect, You (not any Contributor)
|
||||||
|
assume the cost of any necessary servicing, repair, or correction. This disclaimer of warranty
|
||||||
|
constitutes an essential part of this License. No use of any Covered Software is authorized
|
||||||
|
under this License except under this disclaimer.</em></p>
|
||||||
|
<h2 id="limitation-of-liability">7. Limitation of Liability</h2>
|
||||||
|
<p><em>Under no circumstances and under no legal theory, whether tort (including negligence),
|
||||||
|
contract, or otherwise, shall any Contributor, or anyone who distributes Covered Software as
|
||||||
|
permitted above, be liable to You for any direct, indirect, special, incidental, or
|
||||||
|
consequential damages of any character including, without limitation, damages for lost profits,
|
||||||
|
loss of goodwill, work stoppage, computer failure or malfunction, or any and all other
|
||||||
|
commercial damages or losses, even if such party shall have been informed of the possibility of
|
||||||
|
such damages. This limitation of liability shall not apply to liability for death or personal
|
||||||
|
injury resulting from such party’s negligence to the extent applicable law prohibits such
|
||||||
|
limitation. Some jurisdictions do not allow the exclusion or limitation of incidental or
|
||||||
|
consequential damages, so this exclusion and limitation may not apply to You.</em></p>
|
||||||
|
<h2 id="litigation">8. Litigation</h2>
|
||||||
|
<p>Any litigation relating to this License may be brought only in the courts of a jurisdiction where
|
||||||
|
the defendant maintains its principal place of business and such litigation shall be governed by
|
||||||
|
laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this
|
||||||
|
Section shall prevent a party’s ability to bring cross-claims or counter-claims.</p>
|
||||||
|
<h2 id="miscellaneous">9. Miscellaneous</h2>
|
||||||
|
<p>This License represents the complete agreement concerning the subject matter hereof. If any
|
||||||
|
provision of this License is held to be unenforceable, such provision shall be reformed only to
|
||||||
|
the extent necessary to make it enforceable. Any law or regulation which provides that the
|
||||||
|
language of a contract shall be construed against the drafter shall not be used to construe this
|
||||||
|
License against a Contributor.</p>
|
||||||
|
<h2 id="versions-of-the-license">10. Versions of the License</h2>
|
||||||
|
<h3 id="new-versions">10.1. New Versions</h3>
|
||||||
|
<p>Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other
|
||||||
|
than the license steward has the right to modify or publish new versions of this License. Each
|
||||||
|
version will be given a distinguishing version number.</p>
|
||||||
|
<h3 id="effect-of-new-versions">10.2. Effect of New Versions</h3>
|
||||||
|
<p>You may distribute the Covered Software under the terms of the version of the License under which
|
||||||
|
You originally received the Covered Software, or under the terms of any subsequent version
|
||||||
|
published by the license steward.</p>
|
||||||
|
<h3 id="modified-versions">10.3. Modified Versions</h3>
|
||||||
|
<p>If you create software not governed by this License, and you want to create a new license for
|
||||||
|
such software, you may create and use a modified version of this License if you rename the
|
||||||
|
license and remove any references to the name of the license steward (except to note that such
|
||||||
|
modified license differs from this License).</p>
|
||||||
|
<h3 id="distributing-source-code-form-that-is-incompatible-with-secondary-licenses">10.4.
|
||||||
|
Distributing Source Code Form that is Incompatible With Secondary Licenses</h3>
|
||||||
|
<p>If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under
|
||||||
|
the terms of this version of the License, the notice described in Exhibit B of this License must
|
||||||
|
be attached.</p>
|
||||||
|
<h2 id="exhibit-a---source-code-form-license-notice">Exhibit A - Source Code Form License
|
||||||
|
Notice</h2>
|
||||||
|
<blockquote>
|
||||||
|
<p>This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a
|
||||||
|
copy of the MPL was not distributed with this file, You can obtain one at
|
||||||
|
https://mozilla.org/MPL/2.0/.</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>If it is not possible or desirable to put the notice in a particular file, then You may include
|
||||||
|
the notice in a location (such as a LICENSE file in a relevant directory) where a recipient
|
||||||
|
would be likely to look for such a notice.</p>
|
||||||
|
<p>You may add additional accurate notices of copyright ownership.</p>
|
||||||
|
<h2 id="exhibit-b---incompatible-with-secondary-licenses-notice">Exhibit B - “Incompatible With
|
||||||
|
Secondary Licenses” Notice</h2>
|
||||||
|
<blockquote>
|
||||||
|
<p>This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla
|
||||||
|
Public License, v. 2.0.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
127
app/src/main/assets/po_token.html
Normal file
127
app/src/main/assets/po_token.html
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<!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>
|
||||||
|
|
@ -0,0 +1,344 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2018 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package androidx.fragment.app;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.os.BundleCompat;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.viewpager.widget.PagerAdapter;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
// TODO: Replace this deprecated class with its ViewPager2 counterpart
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a copy from {@link androidx.fragment.app.FragmentStatePagerAdapter}.
|
||||||
|
* <p>
|
||||||
|
* It includes a workaround to fix the menu visibility when the adapter is restored.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* When restoring the state of this adapter, all the fragments' menu visibility were set to false,
|
||||||
|
* effectively disabling the menu from the user until he switched pages or another event
|
||||||
|
* that triggered the menu to be visible again happened.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* <b>Check out the changes in:</b>
|
||||||
|
* </p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #saveState()}</li>
|
||||||
|
* <li>{@link #restoreState(Parcelable, ClassLoader)}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
|
||||||
|
* {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Deprecated
|
||||||
|
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
|
||||||
|
private static final String TAG = "FragmentStatePagerAdapt";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
|
||||||
|
private @interface Behavior { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that {@link Fragment#setUserVisibleHint(boolean)} will be called when the current
|
||||||
|
* fragment changes.
|
||||||
|
*
|
||||||
|
* @deprecated This behavior relies on the deprecated
|
||||||
|
* {@link Fragment#setUserVisibleHint(boolean)} API. Use
|
||||||
|
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} to switch to its replacement,
|
||||||
|
* {@link FragmentTransaction#setMaxLifecycle}.
|
||||||
|
* @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}
|
||||||
|
* state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.
|
||||||
|
*
|
||||||
|
* @see #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)
|
||||||
|
*/
|
||||||
|
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;
|
||||||
|
|
||||||
|
private final FragmentManager mFragmentManager;
|
||||||
|
private final int mBehavior;
|
||||||
|
private FragmentTransaction mCurTransaction = null;
|
||||||
|
|
||||||
|
private final ArrayList<Fragment.SavedState> mSavedState = new ArrayList<>();
|
||||||
|
private final ArrayList<Fragment> mFragments = new ArrayList<>();
|
||||||
|
private Fragment mCurrentPrimaryItem = null;
|
||||||
|
private boolean mExecutingFinishUpdate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
|
||||||
|
* that sets the fragment manager for the adapter. This is the equivalent of calling
|
||||||
|
* {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} and passing in
|
||||||
|
* {@link #BEHAVIOR_SET_USER_VISIBLE_HINT}.
|
||||||
|
*
|
||||||
|
* <p>Fragments will have {@link Fragment#setUserVisibleHint(boolean)} called whenever the
|
||||||
|
* current Fragment changes.</p>
|
||||||
|
*
|
||||||
|
* @param fm fragment manager that will interact with this adapter
|
||||||
|
* @deprecated use {@link #FragmentStatePagerAdapterMenuWorkaround(FragmentManager, int)} with
|
||||||
|
* {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT}
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm) {
|
||||||
|
this(fm, BEHAVIOR_SET_USER_VISIBLE_HINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}.
|
||||||
|
*
|
||||||
|
* If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current
|
||||||
|
* Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are
|
||||||
|
* capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is
|
||||||
|
* passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be
|
||||||
|
* callbacks to {@link Fragment#setUserVisibleHint(boolean)}.
|
||||||
|
*
|
||||||
|
* @param fm fragment manager that will interact with this adapter
|
||||||
|
* @param behavior determines if only current fragments are in a resumed state
|
||||||
|
*/
|
||||||
|
public FragmentStatePagerAdapterMenuWorkaround(@NonNull final FragmentManager fm,
|
||||||
|
@Behavior final int behavior) {
|
||||||
|
mFragmentManager = fm;
|
||||||
|
mBehavior = behavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param position the position of the item you want
|
||||||
|
* @return the {@link Fragment} associated with a specified position
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public abstract Fragment getItem(int position);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void startUpdate(@NonNull final ViewGroup container) {
|
||||||
|
if (container.getId() == View.NO_ID) {
|
||||||
|
throw new IllegalStateException("ViewPager with adapter " + this
|
||||||
|
+ " requires a view id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
|
||||||
|
// If we already have this item instantiated, there is nothing
|
||||||
|
// to do. This can happen when we are restoring the entire pager
|
||||||
|
// from its saved state, where the fragment manager has already
|
||||||
|
// taken care of restoring the fragments we previously had instantiated.
|
||||||
|
if (mFragments.size() > position) {
|
||||||
|
final Fragment f = mFragments.get(position);
|
||||||
|
if (f != null) {
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Fragment fragment = getItem(position);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
|
||||||
|
}
|
||||||
|
if (mSavedState.size() > position) {
|
||||||
|
final Fragment.SavedState fss = mSavedState.get(position);
|
||||||
|
if (fss != null) {
|
||||||
|
fragment.setInitialSavedState(fss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (mFragments.size() <= position) {
|
||||||
|
mFragments.add(null);
|
||||||
|
}
|
||||||
|
fragment.setMenuVisibility(false);
|
||||||
|
if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
|
||||||
|
fragment.setUserVisibleHint(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
mFragments.set(position, fragment);
|
||||||
|
mCurTransaction.add(container.getId(), fragment);
|
||||||
|
|
||||||
|
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyItem(@NonNull final ViewGroup container, final int position,
|
||||||
|
@NonNull final Object object) {
|
||||||
|
final Fragment fragment = (Fragment) object;
|
||||||
|
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.v(TAG, "Removing item #" + position + ": f=" + object
|
||||||
|
+ " v=" + ((Fragment) object).getView());
|
||||||
|
}
|
||||||
|
while (mSavedState.size() <= position) {
|
||||||
|
mSavedState.add(null);
|
||||||
|
}
|
||||||
|
mSavedState.set(position, fragment.isAdded()
|
||||||
|
? mFragmentManager.saveFragmentInstanceState(fragment) : null);
|
||||||
|
mFragments.set(position, null);
|
||||||
|
|
||||||
|
mCurTransaction.remove(fragment);
|
||||||
|
if (fragment.equals(mCurrentPrimaryItem)) {
|
||||||
|
mCurrentPrimaryItem = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({"ReferenceEquality", "deprecation"})
|
||||||
|
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||||
|
@NonNull final Object object) {
|
||||||
|
final Fragment fragment = (Fragment) object;
|
||||||
|
if (fragment != mCurrentPrimaryItem) {
|
||||||
|
if (mCurrentPrimaryItem != null) {
|
||||||
|
mCurrentPrimaryItem.setMenuVisibility(false);
|
||||||
|
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
|
||||||
|
} else {
|
||||||
|
mCurrentPrimaryItem.setUserVisibleHint(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fragment.setMenuVisibility(true);
|
||||||
|
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||||
|
if (mCurTransaction == null) {
|
||||||
|
mCurTransaction = mFragmentManager.beginTransaction();
|
||||||
|
}
|
||||||
|
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
|
||||||
|
} else {
|
||||||
|
fragment.setUserVisibleHint(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
mCurrentPrimaryItem = fragment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finishUpdate(@NonNull final ViewGroup container) {
|
||||||
|
if (mCurTransaction != null) {
|
||||||
|
// We drop any transactions that attempt to be committed
|
||||||
|
// from a re-entrant call to finishUpdate(). We need to
|
||||||
|
// do this as a workaround for Robolectric running measure/layout
|
||||||
|
// calls inline rather than allowing them to be posted
|
||||||
|
// as they would on a real device.
|
||||||
|
if (!mExecutingFinishUpdate) {
|
||||||
|
try {
|
||||||
|
mExecutingFinishUpdate = true;
|
||||||
|
mCurTransaction.commitNowAllowingStateLoss();
|
||||||
|
} finally {
|
||||||
|
mExecutingFinishUpdate = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mCurTransaction = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isViewFromObject(@NonNull final View view, @NonNull final Object object) {
|
||||||
|
return ((Fragment) object).getView() == view;
|
||||||
|
}
|
||||||
|
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
private final String selectedFragment = "selected_fragment";
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Nullable
|
||||||
|
public Parcelable saveState() {
|
||||||
|
Bundle state = null;
|
||||||
|
if (!mSavedState.isEmpty()) {
|
||||||
|
state = new Bundle();
|
||||||
|
state.putParcelableArrayList("states", mSavedState);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < mFragments.size(); i++) {
|
||||||
|
final Fragment f = mFragments.get(i);
|
||||||
|
if (f != null && f.isAdded()) {
|
||||||
|
if (state == null) {
|
||||||
|
state = new Bundle();
|
||||||
|
}
|
||||||
|
final String key = "f" + i;
|
||||||
|
mFragmentManager.putFragment(state, key, f);
|
||||||
|
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
// Check if it's the same fragment instance
|
||||||
|
if (f == mCurrentPrimaryItem) {
|
||||||
|
state.putString(selectedFragment, key);
|
||||||
|
}
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restoreState(@Nullable final Parcelable state, @Nullable final ClassLoader loader) {
|
||||||
|
if (state != null) {
|
||||||
|
final Bundle bundle = (Bundle) state;
|
||||||
|
bundle.setClassLoader(loader);
|
||||||
|
final var states = BundleCompat.getParcelableArrayList(bundle, "states",
|
||||||
|
Fragment.SavedState.class);
|
||||||
|
mSavedState.clear();
|
||||||
|
mFragments.clear();
|
||||||
|
if (states != null) {
|
||||||
|
mSavedState.addAll(states);
|
||||||
|
}
|
||||||
|
final Iterable<String> keys = bundle.keySet();
|
||||||
|
for (final String key : keys) {
|
||||||
|
if (key.startsWith("f")) {
|
||||||
|
final int index = Integer.parseInt(key.substring(1));
|
||||||
|
final Fragment f = mFragmentManager.getFragment(bundle, key);
|
||||||
|
if (f != null) {
|
||||||
|
while (mFragments.size() <= index) {
|
||||||
|
mFragments.add(null);
|
||||||
|
}
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
final boolean wasSelected = bundle.getString(selectedFragment, "")
|
||||||
|
.equals(key);
|
||||||
|
f.setMenuVisibility(wasSelected);
|
||||||
|
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
mFragments.set(index, f);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Bad fragment at key " + key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
package com.google.android.material.appbar;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.OverScroller;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.coordinatorlayout.widget.CoordinatorLayout;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
// See https://stackoverflow.com/questions/56849221#57997489
|
||||||
|
public final class FlingBehavior extends AppBarLayout.Behavior {
|
||||||
|
private final Rect focusScrollRect = new Rect();
|
||||||
|
|
||||||
|
public FlingBehavior(final Context context, final AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean allowScroll = true;
|
||||||
|
private final Rect globalRect = new Rect();
|
||||||
|
private final List<Integer> skipInterceptionOfElements = List.of(
|
||||||
|
R.id.itemsListPanel, R.id.playbackSeekBar,
|
||||||
|
R.id.playPauseButton, R.id.playPreviousButton, R.id.playNextButton);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onRequestChildRectangleOnScreen(
|
||||||
|
@NonNull final CoordinatorLayout coordinatorLayout, @NonNull final AppBarLayout child,
|
||||||
|
@NonNull final Rect rectangle, final boolean immediate) {
|
||||||
|
focusScrollRect.set(rectangle);
|
||||||
|
|
||||||
|
coordinatorLayout.offsetDescendantRectToMyCoords(child, focusScrollRect);
|
||||||
|
|
||||||
|
final int height = coordinatorLayout.getHeight();
|
||||||
|
|
||||||
|
if (focusScrollRect.top <= 0 && focusScrollRect.bottom >= height) {
|
||||||
|
// the child is too big to fit inside ourselves completely, ignore request
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int dy;
|
||||||
|
|
||||||
|
if (focusScrollRect.bottom > height) {
|
||||||
|
dy = focusScrollRect.top;
|
||||||
|
} else if (focusScrollRect.top < 0) {
|
||||||
|
// scrolling up
|
||||||
|
dy = -(height - focusScrollRect.bottom);
|
||||||
|
} else {
|
||||||
|
// nothing to do
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int consumed = scroll(coordinatorLayout, child, dy, getMaxDragOffset(child), 0);
|
||||||
|
|
||||||
|
return consumed == dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onInterceptTouchEvent(@NonNull final CoordinatorLayout parent,
|
||||||
|
@NonNull final AppBarLayout child,
|
||||||
|
@NonNull final MotionEvent ev) {
|
||||||
|
for (final int element : skipInterceptionOfElements) {
|
||||||
|
final View view = child.findViewById(element);
|
||||||
|
if (view != null) {
|
||||||
|
final boolean visible = view.getGlobalVisibleRect(globalRect);
|
||||||
|
if (visible && globalRect.contains((int) ev.getRawX(), (int) ev.getRawY())) {
|
||||||
|
allowScroll = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allowScroll = true;
|
||||||
|
switch (ev.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
// remove reference to old nested scrolling child
|
||||||
|
resetNestedScrollingChild();
|
||||||
|
// Stop fling when your finger touches the screen
|
||||||
|
stopAppBarLayoutFling();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return super.onInterceptTouchEvent(parent, child, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStartNestedScroll(@NonNull final CoordinatorLayout parent,
|
||||||
|
@NonNull final AppBarLayout child,
|
||||||
|
@NonNull final View directTargetChild,
|
||||||
|
final View target,
|
||||||
|
final int nestedScrollAxes,
|
||||||
|
final int type) {
|
||||||
|
return allowScroll && super.onStartNestedScroll(
|
||||||
|
parent, child, directTargetChild, target, nestedScrollAxes, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onNestedFling(@NonNull final CoordinatorLayout coordinatorLayout,
|
||||||
|
@NonNull final AppBarLayout child,
|
||||||
|
@NonNull final View target, final float velocityX,
|
||||||
|
final float velocityY, final boolean consumed) {
|
||||||
|
return allowScroll && super.onNestedFling(
|
||||||
|
coordinatorLayout, child, target, velocityX, velocityY, consumed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private OverScroller getScrollerField() {
|
||||||
|
try {
|
||||||
|
final Class<?> headerBehaviorType = this.getClass()
|
||||||
|
.getSuperclass().getSuperclass().getSuperclass();
|
||||||
|
if (headerBehaviorType != null) {
|
||||||
|
final Field field = headerBehaviorType.getDeclaredField("scroller");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return ((OverScroller) field.get(this));
|
||||||
|
}
|
||||||
|
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||||
|
// ?
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private Field getLastNestedScrollingChildRefField() {
|
||||||
|
try {
|
||||||
|
final Class<?> headerBehaviorType = this.getClass().getSuperclass().getSuperclass();
|
||||||
|
if (headerBehaviorType != null) {
|
||||||
|
final Field field =
|
||||||
|
headerBehaviorType.getDeclaredField("lastNestedScrollingChildRef");
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
} catch (final NoSuchFieldException e) {
|
||||||
|
// ?
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetNestedScrollingChild() {
|
||||||
|
final Field field = getLastNestedScrollingChildRefField();
|
||||||
|
if (field != null) {
|
||||||
|
try {
|
||||||
|
final Object value = field.get(this);
|
||||||
|
if (value != null) {
|
||||||
|
field.set(this, null);
|
||||||
|
}
|
||||||
|
} catch (final IllegalAccessException e) {
|
||||||
|
// ?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopAppBarLayoutFling() {
|
||||||
|
final OverScroller scroller = getScrollerField();
|
||||||
|
if (scroller != null) {
|
||||||
|
scroller.forceFinished(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||||
|
* contributor license agreements. See the NOTICE file distributed with
|
||||||
|
* this work for additional information regarding copyright ownership.
|
||||||
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||||
|
* (the "License"); you may not use this file except in compliance with
|
||||||
|
* the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.commons.text.similarity;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A matching algorithm that is similar to the searching algorithms implemented in editors such
|
||||||
|
* as Sublime Text, TextMate, Atom and others.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* One point is given for every matched character. Subsequent matches yield two bonus points.
|
||||||
|
* A higher score indicates a higher similarity.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This code has been adapted from Apache Commons Lang 3.3.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @since 1.0
|
||||||
|
*
|
||||||
|
* Note: This class was forked from
|
||||||
|
* <a href="https://git.io/JyYJg">
|
||||||
|
* apache/commons-text (8cfdafc) FuzzyScore.java
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class FuzzyScore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locale used to change the case of text.
|
||||||
|
*/
|
||||||
|
private final Locale locale;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This returns a {@link Locale}-specific {@link FuzzyScore}.
|
||||||
|
*
|
||||||
|
* @param locale The string matching logic is case insensitive.
|
||||||
|
A {@link Locale} is necessary to normalize both Strings to lower case.
|
||||||
|
* @throws IllegalArgumentException
|
||||||
|
* This is thrown if the {@link Locale} parameter is {@code null}.
|
||||||
|
*/
|
||||||
|
public FuzzyScore(final Locale locale) {
|
||||||
|
if (locale == null) {
|
||||||
|
throw new IllegalArgumentException("Locale must not be null");
|
||||||
|
}
|
||||||
|
this.locale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the Fuzzy Score which indicates the similarity score between two
|
||||||
|
* Strings.
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* score.fuzzyScore(null, null) = IllegalArgumentException
|
||||||
|
* score.fuzzyScore("not null", null) = IllegalArgumentException
|
||||||
|
* score.fuzzyScore(null, "not null") = IllegalArgumentException
|
||||||
|
* score.fuzzyScore("", "") = 0
|
||||||
|
* score.fuzzyScore("Workshop", "b") = 0
|
||||||
|
* score.fuzzyScore("Room", "o") = 1
|
||||||
|
* score.fuzzyScore("Workshop", "w") = 1
|
||||||
|
* score.fuzzyScore("Workshop", "ws") = 2
|
||||||
|
* score.fuzzyScore("Workshop", "wo") = 4
|
||||||
|
* score.fuzzyScore("Apache Software Foundation", "asf") = 3
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @param term a full term that should be matched against, must not be null
|
||||||
|
* @param query the query that will be matched against a term, must not be
|
||||||
|
* null
|
||||||
|
* @return result score
|
||||||
|
* @throws IllegalArgumentException if the term or query is {@code null}
|
||||||
|
*/
|
||||||
|
public Integer fuzzyScore(final CharSequence term, final CharSequence query) {
|
||||||
|
if (term == null || query == null) {
|
||||||
|
throw new IllegalArgumentException("CharSequences must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzy logic is case insensitive. We normalize the Strings to lower
|
||||||
|
// case right from the start. Turning characters to lower case
|
||||||
|
// via Character.toLowerCase(char) is unfortunately insufficient
|
||||||
|
// as it does not accept a locale.
|
||||||
|
final String termLowerCase = term.toString().toLowerCase(locale);
|
||||||
|
final String queryLowerCase = query.toString().toLowerCase(locale);
|
||||||
|
|
||||||
|
// the resulting score
|
||||||
|
int score = 0;
|
||||||
|
|
||||||
|
// the position in the term which will be scanned next for potential
|
||||||
|
// query character matches
|
||||||
|
int termIndex = 0;
|
||||||
|
|
||||||
|
// index of the previously matched character in the term
|
||||||
|
int previousMatchingCharacterIndex = Integer.MIN_VALUE;
|
||||||
|
|
||||||
|
for (int queryIndex = 0; queryIndex < queryLowerCase.length(); queryIndex++) {
|
||||||
|
final char queryChar = queryLowerCase.charAt(queryIndex);
|
||||||
|
|
||||||
|
boolean termCharacterMatchFound = false;
|
||||||
|
for (; termIndex < termLowerCase.length()
|
||||||
|
&& !termCharacterMatchFound; termIndex++) {
|
||||||
|
final char termChar = termLowerCase.charAt(termIndex);
|
||||||
|
|
||||||
|
if (queryChar == termChar) {
|
||||||
|
// simple character matches result in one point
|
||||||
|
score++;
|
||||||
|
|
||||||
|
// subsequent character matches further improve
|
||||||
|
// the score.
|
||||||
|
if (previousMatchingCharacterIndex + 1 == termIndex) {
|
||||||
|
score += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousMatchingCharacterIndex = termIndex;
|
||||||
|
|
||||||
|
// we can leave the nested loop. Every character in the
|
||||||
|
// query can match at most one character in the term.
|
||||||
|
termCharacterMatchFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the locale.
|
||||||
|
*
|
||||||
|
* @return The locale
|
||||||
|
*/
|
||||||
|
public Locale getLocale() {
|
||||||
|
return locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
293
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
293
app/src/main/java/org/schabi/newpipe/App.kt
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationChannelCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import coil3.ImageLoader
|
||||||
|
import coil3.SingletonImageLoader
|
||||||
|
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||||
|
import coil3.request.allowRgb565
|
||||||
|
import coil3.request.crossfade
|
||||||
|
import coil3.util.DebugLogger
|
||||||
|
import com.jakewharton.processphoenix.ProcessPhoenix
|
||||||
|
import io.reactivex.rxjava3.exceptions.CompositeException
|
||||||
|
import io.reactivex.rxjava3.exceptions.MissingBackpressureException
|
||||||
|
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException
|
||||||
|
import io.reactivex.rxjava3.exceptions.UndeliverableException
|
||||||
|
import io.reactivex.rxjava3.functions.Consumer
|
||||||
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InterruptedIOException
|
||||||
|
import java.net.SocketException
|
||||||
|
import org.acra.ACRA.init
|
||||||
|
import org.acra.ACRA.isACRASenderServiceProcess
|
||||||
|
import org.acra.config.CoreConfigurationBuilder
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
|
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeStreamExtractor
|
||||||
|
import org.schabi.newpipe.ktx.hasAssignableCause
|
||||||
|
import org.schabi.newpipe.settings.NewPipeSettings
|
||||||
|
import org.schabi.newpipe.util.BridgeStateSaverInitializer
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ServiceHelper
|
||||||
|
import org.schabi.newpipe.util.StateSaver
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
import org.schabi.newpipe.util.image.PreferredImageQuality
|
||||||
|
import org.schabi.newpipe.util.potoken.PoTokenProviderImpl
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* App.kt 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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
open class App :
|
||||||
|
Application(),
|
||||||
|
SingletonImageLoader.Factory {
|
||||||
|
var isFirstRun = false
|
||||||
|
private set
|
||||||
|
var notificationsRequested = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun setNotificationsRequested() {
|
||||||
|
notificationsRequested = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
super.attachBaseContext(base)
|
||||||
|
initACRA()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
instance = this
|
||||||
|
|
||||||
|
if (ProcessPhoenix.isPhoenixProcess(this)) {
|
||||||
|
Log.i(TAG, "This is a phoenix process! Aborting initialization of App[onCreate]")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the last used preference version is set
|
||||||
|
// to determine whether this is the first app run
|
||||||
|
val 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)
|
||||||
|
|
||||||
|
NewPipe.init(
|
||||||
|
getDownloader(),
|
||||||
|
Localization.getPreferredLocalization(this),
|
||||||
|
Localization.getPreferredContentCountry(this)
|
||||||
|
)
|
||||||
|
Localization.initPrettyTime(Localization.resolvePrettyTime())
|
||||||
|
|
||||||
|
BridgeStateSaverInitializer.init(this)
|
||||||
|
StateSaver.init(this)
|
||||||
|
initNotificationChannels()
|
||||||
|
|
||||||
|
ServiceHelper.initServices(this)
|
||||||
|
|
||||||
|
// Initialize image loader
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
ImageStrategy.setPreferredImageQuality(
|
||||||
|
PreferredImageQuality.fromPreferenceKey(
|
||||||
|
this,
|
||||||
|
prefs.getString(
|
||||||
|
getString(R.string.image_quality_key),
|
||||||
|
getString(R.string.image_quality_default)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
configureRxJavaErrorHandler()
|
||||||
|
|
||||||
|
YoutubeStreamExtractor.setPoTokenProvider(PoTokenProviderImpl)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun newImageLoader(context: Context): ImageLoader = ImageLoader
|
||||||
|
.Builder(this)
|
||||||
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
|
.allowRgb565(getSystemService<ActivityManager>()!!.isLowRamDevice)
|
||||||
|
.crossfade(true)
|
||||||
|
.components {
|
||||||
|
add(OkHttpNetworkFetcherFactory(callFactory = DownloaderImpl.getInstance().client))
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
protected open fun getDownloader(): Downloader {
|
||||||
|
val downloader = DownloaderImpl.init(null)
|
||||||
|
setCookiesToDownloader(downloader)
|
||||||
|
return downloader
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setCookiesToDownloader(downloader: DownloaderImpl) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
val key = getString(R.string.recaptcha_cookies_key)
|
||||||
|
downloader.setCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY, prefs.getString(key, null))
|
||||||
|
downloader.updateYoutubeRestrictedModeCookies(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureRxJavaErrorHandler() {
|
||||||
|
// https://github.com/ReactiveX/RxJava/wiki/What's-different-in-2.0#error-handling
|
||||||
|
RxJavaPlugins.setErrorHandler(
|
||||||
|
object : Consumer<Throwable> {
|
||||||
|
override fun accept(throwable: Throwable) {
|
||||||
|
Log.e(TAG, "RxJavaPlugins.ErrorHandler called with -> : throwable = [${throwable.javaClass.getName()}]")
|
||||||
|
|
||||||
|
// As UndeliverableException is a wrapper,
|
||||||
|
// get the cause of it to get the "real" exception
|
||||||
|
val actualThrowable = (throwable as? UndeliverableException)?.cause ?: throwable
|
||||||
|
|
||||||
|
val errors = (actualThrowable as? CompositeException)?.exceptions ?: listOf(actualThrowable)
|
||||||
|
|
||||||
|
for (error in errors) {
|
||||||
|
if (isThrowableIgnored(error)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isThrowableCritical(error)) {
|
||||||
|
reportException(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Out-of-lifecycle exceptions should only be reported if a debug user wishes so,
|
||||||
|
// When exception is not reported, log it
|
||||||
|
if (isDisposedRxExceptionsReported()) {
|
||||||
|
reportException(actualThrowable)
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "RxJavaPlugin: Undeliverable Exception received: ", actualThrowable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableIgnored(throwable: Throwable): Boolean {
|
||||||
|
// Don't crash the application over a simple network problem
|
||||||
|
return throwable // network api cancellation
|
||||||
|
.hasAssignableCause(
|
||||||
|
IOException::class.java,
|
||||||
|
SocketException::class.java, // blocking code disposed
|
||||||
|
InterruptedException::class.java,
|
||||||
|
InterruptedIOException::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isThrowableCritical(throwable: Throwable): Boolean {
|
||||||
|
// Though these exceptions cannot be ignored
|
||||||
|
return throwable
|
||||||
|
.hasAssignableCause(
|
||||||
|
// bug in app
|
||||||
|
NullPointerException::class.java,
|
||||||
|
IllegalArgumentException::class.java,
|
||||||
|
OnErrorNotImplementedException::class.java,
|
||||||
|
MissingBackpressureException::class.java,
|
||||||
|
// bug in operator
|
||||||
|
IllegalStateException::class.java
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reportException(throwable: Throwable) {
|
||||||
|
// Throw uncaught exception that will trigger the report system
|
||||||
|
Thread
|
||||||
|
.currentThread()
|
||||||
|
.uncaughtExceptionHandler
|
||||||
|
.uncaughtException(Thread.currentThread(), throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called in [.attachBaseContext] after calling the `super` method.
|
||||||
|
* Should be overridden if MultiDex is enabled, since it has to be initialized before ACRA.
|
||||||
|
*/
|
||||||
|
protected fun initACRA() {
|
||||||
|
if (isACRASenderServiceProcess()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val acraConfig =
|
||||||
|
CoreConfigurationBuilder()
|
||||||
|
.withBuildConfigClass(BuildConfig::class.java)
|
||||||
|
init(this, acraConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initNotificationChannels() {
|
||||||
|
// Keep the importance below DEFAULT to avoid making noise on every notification update for
|
||||||
|
// the main and update channels
|
||||||
|
val mainChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
).setName(getString(R.string.notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val appUpdateChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.app_update_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
).setName(getString(R.string.app_update_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.app_update_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
val hashChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.hash_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_HIGH
|
||||||
|
).setName(getString(R.string.hash_channel_name))
|
||||||
|
.setDescription(getString(R.string.hash_channel_description))
|
||||||
|
.build()
|
||||||
|
val errorReportChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.error_report_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_LOW
|
||||||
|
).setName(getString(R.string.error_report_channel_name))
|
||||||
|
.setDescription(getString(R.string.error_report_channel_description))
|
||||||
|
.build()
|
||||||
|
val newStreamChannel =
|
||||||
|
NotificationChannelCompat
|
||||||
|
.Builder(
|
||||||
|
getString(R.string.streams_notification_channel_id),
|
||||||
|
NotificationManagerCompat.IMPORTANCE_DEFAULT
|
||||||
|
).setName(getString(R.string.streams_notification_channel_name))
|
||||||
|
.setDescription(getString(R.string.streams_notification_channel_description))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val channels = listOf(mainChannel, appUpdateChannel, hashChannel, errorReportChannel, newStreamChannel)
|
||||||
|
|
||||||
|
NotificationManagerCompat.from(this).createNotificationChannelsCompat(channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun isDisposedRxExceptionsReported(): Boolean = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PACKAGE_NAME: String = BuildConfig.APPLICATION_ID
|
||||||
|
private val TAG = App::class.java.toString()
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
lateinit var instance: App
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
}
|
||||||
140
app/src/main/java/org/schabi/newpipe/BaseFragment.java
Normal file
140
app/src/main/java/org/schabi/newpipe/BaseFragment.java
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import com.evernote.android.state.State;
|
||||||
|
import com.livefront.bridge.Bridge;
|
||||||
|
|
||||||
|
|
||||||
|
public abstract class BaseFragment extends Fragment {
|
||||||
|
protected final String TAG = getClass().getSimpleName() + "@" + Integer.toHexString(hashCode());
|
||||||
|
protected static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
protected AppCompatActivity activity;
|
||||||
|
//These values are used for controlling fragments when they are part of the frontpage
|
||||||
|
@State
|
||||||
|
protected boolean useAsFrontPage = false;
|
||||||
|
|
||||||
|
public void useAsFrontPage(final boolean value) {
|
||||||
|
useAsFrontPage = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fragment's Lifecycle
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(@NonNull final Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
activity = (AppCompatActivity) context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetach() {
|
||||||
|
super.onDetach();
|
||||||
|
activity = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called with: "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
Bridge.restoreInstanceState(this, savedInstanceState);
|
||||||
|
if (savedInstanceState != null) {
|
||||||
|
onRestoreInstanceState(savedInstanceState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(rootView, savedInstanceState);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onViewCreated() called with: "
|
||||||
|
+ "rootView = [" + rootView + "], "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
initViews(rootView, savedInstanceState);
|
||||||
|
initListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(@NonNull final Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
Bridge.saveInstanceState(this, outState);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// 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) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
// Utils
|
||||||
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
|
public void setTitle(final String title) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
|
||||||
|
}
|
||||||
|
if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
|
||||||
|
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
|
||||||
|
activity.getSupportActionBar().setTitle(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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() {
|
||||||
|
Fragment current = this;
|
||||||
|
while (current.getParentFragment() != null) {
|
||||||
|
current = current.getParentFragment();
|
||||||
|
}
|
||||||
|
return current.getFragmentManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
181
app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
Normal file
181
app/src/main/java/org/schabi/newpipe/DownloaderImpl.java
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Downloader;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Request;
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Response;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException;
|
||||||
|
import org.schabi.newpipe.util.InfoCache;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import okhttp3.ResponseBody;
|
||||||
|
|
||||||
|
public final class DownloaderImpl extends Downloader {
|
||||||
|
public static final String USER_AGENT =
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0";
|
||||||
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE_KEY =
|
||||||
|
"youtube_restricted_mode_key";
|
||||||
|
public static final String YOUTUBE_RESTRICTED_MODE_COOKIE = "PREF=f2=8000000";
|
||||||
|
public static final String YOUTUBE_DOMAIN = "youtube.com";
|
||||||
|
|
||||||
|
private static DownloaderImpl instance;
|
||||||
|
private final Map<String, String> mCookies;
|
||||||
|
private final OkHttpClient client;
|
||||||
|
|
||||||
|
private DownloaderImpl(final OkHttpClient.Builder builder) {
|
||||||
|
this.client = builder
|
||||||
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
|
// .cache(new Cache(new File(context.getExternalCacheDir(), "okhttp"),
|
||||||
|
// 16 * 1024 * 1024))
|
||||||
|
.build();
|
||||||
|
this.mCookies = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public OkHttpClient getClient() {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's recommended to call exactly once in the entire lifetime of the application.
|
||||||
|
*
|
||||||
|
* @param builder if null, default builder will be used
|
||||||
|
* @return a new instance of {@link DownloaderImpl}
|
||||||
|
*/
|
||||||
|
public static DownloaderImpl init(@Nullable final OkHttpClient.Builder builder) {
|
||||||
|
instance = new DownloaderImpl(
|
||||||
|
builder != null ? builder : new OkHttpClient.Builder());
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloaderImpl getInstance() {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCookies(final String url) {
|
||||||
|
final String youtubeCookie = url.contains(YOUTUBE_DOMAIN)
|
||||||
|
? getCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY) : null;
|
||||||
|
|
||||||
|
// Recaptcha cookie is always added TODO: not sure if this is necessary
|
||||||
|
return Stream.of(youtubeCookie, getCookie(ReCaptchaActivity.RECAPTCHA_COOKIES_KEY))
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.flatMap(cookies -> Arrays.stream(cookies.split("; *")))
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCookie(final String key) {
|
||||||
|
return mCookies.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCookie(final String key, final String cookie) {
|
||||||
|
mCookies.put(key, cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeCookie(final String key) {
|
||||||
|
mCookies.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateYoutubeRestrictedModeCookies(final Context context) {
|
||||||
|
final String restrictedModeEnabledKey =
|
||||||
|
context.getString(R.string.youtube_restricted_mode_enabled);
|
||||||
|
final boolean restrictedModeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(restrictedModeEnabledKey, false);
|
||||||
|
updateYoutubeRestrictedModeCookies(restrictedModeEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateYoutubeRestrictedModeCookies(final boolean youtubeRestrictedModeEnabled) {
|
||||||
|
if (youtubeRestrictedModeEnabled) {
|
||||||
|
setCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY,
|
||||||
|
YOUTUBE_RESTRICTED_MODE_COOKIE);
|
||||||
|
} else {
|
||||||
|
removeCookie(YOUTUBE_RESTRICTED_MODE_COOKIE_KEY);
|
||||||
|
}
|
||||||
|
InfoCache.getInstance().clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||||
|
*
|
||||||
|
* @param url an url pointing to the content
|
||||||
|
* @return the size of the content, in bytes
|
||||||
|
*/
|
||||||
|
public long getContentLength(final String url) throws IOException {
|
||||||
|
try {
|
||||||
|
final Response response = head(url);
|
||||||
|
return Long.parseLong(response.getHeader("Content-Length"));
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
throw new IOException("Invalid content length", e);
|
||||||
|
} catch (final ReCaptchaException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response execute(@NonNull final Request request)
|
||||||
|
throws IOException, ReCaptchaException {
|
||||||
|
final String httpMethod = request.httpMethod();
|
||||||
|
final String url = request.url();
|
||||||
|
final Map<String, List<String>> headers = request.headers();
|
||||||
|
final byte[] dataToSend = request.dataToSend();
|
||||||
|
|
||||||
|
RequestBody requestBody = null;
|
||||||
|
if (dataToSend != null) {
|
||||||
|
requestBody = RequestBody.create(dataToSend);
|
||||||
|
}
|
||||||
|
|
||||||
|
final okhttp3.Request.Builder requestBuilder = new okhttp3.Request.Builder()
|
||||||
|
.method(httpMethod, requestBody)
|
||||||
|
.url(url)
|
||||||
|
.addHeader("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
|
final String cookies = getCookies(url);
|
||||||
|
if (!cookies.isEmpty()) {
|
||||||
|
requestBuilder.addHeader("Cookie", cookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.forEach((headerName, headerValueList) -> {
|
||||||
|
requestBuilder.removeHeader(headerName);
|
||||||
|
headerValueList.forEach(headerValue ->
|
||||||
|
requestBuilder.addHeader(headerName, headerValue));
|
||||||
|
});
|
||||||
|
|
||||||
|
try (
|
||||||
|
okhttp3.Response response = client.newCall(requestBuilder.build()).execute()
|
||||||
|
) {
|
||||||
|
if (response.code() == 429) {
|
||||||
|
throw new ReCaptchaException("reCaptcha Challenge requested", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
String responseBodyToReturn = null;
|
||||||
|
try (ResponseBody body = response.body()) {
|
||||||
|
responseBodyToReturn = body.string();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String latestUrl = response.request().url().toString();
|
||||||
|
return new Response(
|
||||||
|
response.code(),
|
||||||
|
response.message(),
|
||||||
|
response.headers().toMultimap(),
|
||||||
|
responseBodyToReturn,
|
||||||
|
latestUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
36
app/src/main/java/org/schabi/newpipe/ExitActivity.kt
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper
|
||||||
|
|
||||||
|
class ExitActivity : Activity() {
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
finishAndRemoveTask()
|
||||||
|
NavigationHelper.restartApp(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun exitAndRemoveFromRecentApps(activity: Activity) {
|
||||||
|
val intent = Intent(activity, ExitActivity::class.java)
|
||||||
|
intent.addFlags(
|
||||||
|
Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
or Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||||
|
)
|
||||||
|
|
||||||
|
activity.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1061
app/src/main/java/org/schabi/newpipe/MainActivity.java
Normal file
1061
app/src/main/java/org/schabi/newpipe/MainActivity.java
Normal file
File diff suppressed because it is too large
Load diff
80
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt
Normal file
80
app/src/main/java/org/schabi/newpipe/NewPipeDatabase.kt
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Room.databaseBuilder
|
||||||
|
import kotlin.concurrent.Volatile
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
186
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
186
app/src/main/java/org/schabi/newpipe/NewVersionWorker.kt
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
package org.schabi.newpipe
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
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.core.content.edit
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import com.grack.nanojson.JsonParser
|
||||||
|
import com.grack.nanojson.JsonParserException
|
||||||
|
import java.io.IOException
|
||||||
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||||
|
import org.schabi.newpipe.util.ReleaseVersionUtil
|
||||||
|
|
||||||
|
class NewVersionWorker(
|
||||||
|
context: Context,
|
||||||
|
workerParams: WorkerParameters
|
||||||
|
) : Worker(context, workerParams) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to compare the current and latest available app version.
|
||||||
|
* If a newer version is available, we show the update notification.
|
||||||
|
*
|
||||||
|
* @param versionName Name of new version
|
||||||
|
* @param apkLocationUrl Url with the new apk
|
||||||
|
* @param versionCode Code of new version
|
||||||
|
*/
|
||||||
|
private fun compareAppVersionAndShowNotification(
|
||||||
|
versionName: String,
|
||||||
|
apkLocationUrl: String?,
|
||||||
|
versionCode: Int
|
||||||
|
) {
|
||||||
|
if (BuildConfig.VERSION_CODE >= versionCode) {
|
||||||
|
if (inputData.getBoolean(IS_MANUAL, false)) {
|
||||||
|
// Show toast stating that the app is up-to-date if the update check was manual.
|
||||||
|
ContextCompat.getMainExecutor(applicationContext).execute {
|
||||||
|
Toast.makeText(
|
||||||
|
applicationContext,
|
||||||
|
R.string.app_update_unavailable_toast,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pending intent to open the apk location url in the browser.
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW, apkLocationUrl?.toUri())
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
val pendingIntent = PendingIntentCompat.getActivity(
|
||||||
|
applicationContext,
|
||||||
|
0,
|
||||||
|
intent,
|
||||||
|
0,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
val channelId = applicationContext.getString(R.string.app_update_notification_channel_id)
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(applicationContext, channelId)
|
||||||
|
.setSmallIcon(R.drawable.ic_newpipe_update)
|
||||||
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setContentTitle(
|
||||||
|
applicationContext.getString(R.string.app_update_available_notification_title)
|
||||||
|
)
|
||||||
|
.setContentText(
|
||||||
|
applicationContext.getString(
|
||||||
|
R.string.app_update_available_notification_text,
|
||||||
|
versionName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(2000, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
|
private fun checkNewVersion() {
|
||||||
|
// Check if the current apk is a github one or not.
|
||||||
|
if (!ReleaseVersionUtil.isReleaseApk) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputData.getBoolean(IS_MANUAL, false)) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
// Check if the last request has happened a certain time ago
|
||||||
|
// to reduce the number of API requests.
|
||||||
|
val expiry = prefs.getLong(applicationContext.getString(R.string.update_expiry_key), 0)
|
||||||
|
if (!ReleaseVersionUtil.isLastUpdateCheckExpired(expiry)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a network request to get latest NewPipe data.
|
||||||
|
val response = DownloaderImpl.getInstance().get(NEWPIPE_API_URL)
|
||||||
|
handleResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleResponse(response: Response) {
|
||||||
|
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
|
try {
|
||||||
|
// Store a timestamp which needs to be exceeded,
|
||||||
|
// before a new request to the API is made.
|
||||||
|
val newExpiry = ReleaseVersionUtil.coerceUpdateCheckExpiry(response.getHeader("expires"))
|
||||||
|
prefs.edit {
|
||||||
|
putLong(applicationContext.getString(R.string.update_expiry_key), newExpiry)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Could not extract and save new expiry date", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the json from the response.
|
||||||
|
try {
|
||||||
|
val newpipeVersionInfo = JsonParser.`object`()
|
||||||
|
.from(response.responseBody()).getObject("flavors")
|
||||||
|
.getObject("newpipe")
|
||||||
|
|
||||||
|
val versionName = newpipeVersionInfo.getString("version")
|
||||||
|
val versionCode = newpipeVersionInfo.getInt("version_code")
|
||||||
|
val apkLocationUrl = newpipeVersionInfo.getString("apk")
|
||||||
|
compareAppVersionAndShowNotification(versionName, apkLocationUrl, versionCode)
|
||||||
|
} catch (e: JsonParserException) {
|
||||||
|
// Most likely something is wrong in data received from NEWPIPE_API_URL.
|
||||||
|
// Do not alarm user and fail silently.
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.w(TAG, "Could not get NewPipe API: invalid json", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
return try {
|
||||||
|
checkNewVersion()
|
||||||
|
Result.success()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, "Could not fetch NewPipe API: probably network problem", e)
|
||||||
|
Result.failure()
|
||||||
|
} catch (e: ReCaptchaException) {
|
||||||
|
Log.e(TAG, "ReCaptchaException should never happen here.", e)
|
||||||
|
Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEBUG = MainActivity.DEBUG
|
||||||
|
private val TAG = NewVersionWorker::class.java.simpleName
|
||||||
|
private const val NEWPIPE_API_URL = "https://newpipe.net/api/data.json"
|
||||||
|
private const val IS_MANUAL = "isManual"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new worker which checks if all conditions for performing a version check are met,
|
||||||
|
* fetches the API endpoint [.NEWPIPE_API_URL] containing info about the latest NewPipe
|
||||||
|
* version and displays a notification about an available update if one is available.
|
||||||
|
* <br></br>
|
||||||
|
* Following conditions need to be met, before data is requested from the server:
|
||||||
|
*
|
||||||
|
* * The app is signed with the correct signing key (by TeamNewPipe / schabi).
|
||||||
|
* If the signing key differs from the one used upstream, the update cannot be installed.
|
||||||
|
* * The user enabled searching for and notifying about updates in the settings.
|
||||||
|
* * The app did not recently check for updates.
|
||||||
|
* We do not want to make unnecessary connections and DOS our servers.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun enqueueNewVersionCheckingWork(context: Context, isManual: Boolean) {
|
||||||
|
val workRequest = OneTimeWorkRequestBuilder<NewVersionWorker>()
|
||||||
|
.setInputData(workDataOf(IS_MANUAL to isManual))
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueue(workRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* PanicResponderActivity.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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class PanicResponderActivity extends Activity {
|
||||||
|
public static final String PANIC_TRIGGER_ACTION = "info.guardianproject.panic.action.TRIGGER";
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
if (intent != null && PANIC_TRIGGER_ACTION.equals(intent.getAction())) {
|
||||||
|
// TODO: Explicitly clear the search results
|
||||||
|
// once they are restored when the app restarts
|
||||||
|
// or if the app reloads the current video after being killed,
|
||||||
|
// that should be cleared also
|
||||||
|
ExitActivity.exitAndRemoveFromRecentApps(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
finishAndRemoveTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
94
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
94
app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.util.SparseItemUtil.fetchStreamInfoAndSaveToDatabase;
|
||||||
|
import static org.schabi.newpipe.util.external_communication.ShareUtils.shareText;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.ContextThemeWrapper;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.PopupMenu;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity;
|
||||||
|
import org.schabi.newpipe.download.DownloadDialog;
|
||||||
|
import org.schabi.newpipe.local.dialog.PlaylistDialog;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueue;
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.SparseItemUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class QueueItemMenuUtil {
|
||||||
|
private QueueItemMenuUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openPopupMenu(final PlayQueue playQueue,
|
||||||
|
final PlayQueueItem item,
|
||||||
|
final View view,
|
||||||
|
final boolean hideDetails,
|
||||||
|
final FragmentManager fragmentManager,
|
||||||
|
final Context context) {
|
||||||
|
final ContextThemeWrapper themeWrapper =
|
||||||
|
new ContextThemeWrapper(context, R.style.DarkPopupMenu);
|
||||||
|
|
||||||
|
final PopupMenu popupMenu = new PopupMenu(themeWrapper, view);
|
||||||
|
popupMenu.inflate(R.menu.menu_play_queue_item);
|
||||||
|
|
||||||
|
if (hideDetails) {
|
||||||
|
popupMenu.getMenu().findItem(R.id.menu_item_details).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||||
|
final int itemId = menuItem.getItemId();
|
||||||
|
if (itemId == R.id.menu_item_remove) {
|
||||||
|
final int index = playQueue.indexOf(item);
|
||||||
|
playQueue.remove(index);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_item_details) {
|
||||||
|
// playQueue is null since we don't want any queue change
|
||||||
|
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||||
|
item.getUrl(), item.getTitle(), null,
|
||||||
|
false);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_item_append_playlist) {
|
||||||
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
|
context,
|
||||||
|
List.of(new StreamEntity(item)),
|
||||||
|
dialog -> dialog.show(
|
||||||
|
fragmentManager,
|
||||||
|
"QueueItemMenuUtil@append_playlist"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_item_channel_details) {
|
||||||
|
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||||
|
item.getUrl(), item.getUploaderUrl(),
|
||||||
|
// An intent must be used here.
|
||||||
|
// Opening with FragmentManager transactions is not working,
|
||||||
|
// as PlayQueueActivity doesn't use fragments.
|
||||||
|
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||||
|
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
|
item.getThumbnails());
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.menu_item_download) {
|
||||||
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
|
info -> {
|
||||||
|
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
||||||
|
info);
|
||||||
|
downloadDialog.show(fragmentManager, "downloadDialog");
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
popupMenu.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
1078
app/src/main/java/org/schabi/newpipe/RouterActivity.java
Normal file
1078
app/src/main/java/org/schabi/newpipe/RouterActivity.java
Normal file
File diff suppressed because it is too large
Load diff
260
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Normal file
260
app/src/main/java/org/schabi/newpipe/about/AboutActivity.kt
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ActivityAboutBinding
|
||||||
|
import org.schabi.newpipe.databinding.FragmentAboutBinding
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
class AboutActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
title = getString(R.string.title_activity_about)
|
||||||
|
|
||||||
|
val aboutBinding = ActivityAboutBinding.inflate(layoutInflater)
|
||||||
|
setContentView(aboutBinding.root)
|
||||||
|
setSupportActionBar(aboutBinding.aboutToolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
|
||||||
|
// Create the adapter that will return a fragment for each of the three
|
||||||
|
// primary sections of the activity.
|
||||||
|
val mAboutStateAdapter = AboutStateAdapter(this)
|
||||||
|
// Set up the ViewPager with the sections adapter.
|
||||||
|
aboutBinding.aboutViewPager2.adapter = mAboutStateAdapter
|
||||||
|
TabLayoutMediator(
|
||||||
|
aboutBinding.aboutTabLayout,
|
||||||
|
aboutBinding.aboutViewPager2
|
||||||
|
) { tab, position ->
|
||||||
|
tab.setText(mAboutStateAdapter.getPageTitle(position))
|
||||||
|
}.attach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == android.R.id.home) {
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A placeholder fragment containing a simple view.
|
||||||
|
*/
|
||||||
|
class AboutFragment : Fragment() {
|
||||||
|
private fun Button.openLink(@StringRes url: Int) {
|
||||||
|
setOnClickListener {
|
||||||
|
ShareUtils.openUrlInApp(context, requireContext().getString(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
FragmentAboutBinding.inflate(inflater, container, false).apply {
|
||||||
|
aboutAppVersion.text = BuildConfig.VERSION_NAME
|
||||||
|
aboutGithubLink.openLink(R.string.github_url)
|
||||||
|
aboutDonationLink.openLink(R.string.donation_url)
|
||||||
|
aboutWebsiteLink.openLink(R.string.website_url)
|
||||||
|
aboutPrivacyPolicyLink.openLink(R.string.privacy_policy_url)
|
||||||
|
faqLink.openLink(R.string.faq_url)
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [FragmentStateAdapter] that returns a fragment corresponding to
|
||||||
|
* one of the sections/tabs/pages.
|
||||||
|
*/
|
||||||
|
private class AboutStateAdapter(fa: FragmentActivity) : FragmentStateAdapter(fa) {
|
||||||
|
private val posAbout = 0
|
||||||
|
private val posLicense = 1
|
||||||
|
private val totalCount = 2
|
||||||
|
|
||||||
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> AboutFragment()
|
||||||
|
posLicense -> LicenseFragment.newInstance(SOFTWARE_COMPONENTS)
|
||||||
|
else -> error("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
// Show 2 total pages.
|
||||||
|
return totalCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPageTitle(position: Int): Int {
|
||||||
|
return when (position) {
|
||||||
|
posAbout -> R.string.tab_about
|
||||||
|
posLicense -> R.string.tab_licenses
|
||||||
|
else -> error("Unknown position for ViewPager2")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* List of all software components.
|
||||||
|
*/
|
||||||
|
private val SOFTWARE_COMPONENTS = arrayListOf(
|
||||||
|
SoftwareComponent(
|
||||||
|
"ACRA",
|
||||||
|
"2013",
|
||||||
|
"Kevin Gaudin",
|
||||||
|
"https://github.com/ACRA/acra",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"AndroidX",
|
||||||
|
"2005 - 2011",
|
||||||
|
"The Android Open Source Project",
|
||||||
|
"https://developer.android.com/jetpack",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ExoPlayer",
|
||||||
|
"2014 - 2020",
|
||||||
|
"Google, Inc.",
|
||||||
|
"https://github.com/google/ExoPlayer",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"GigaGet",
|
||||||
|
"2014 - 2015",
|
||||||
|
"Peter Cai",
|
||||||
|
"https://github.com/PaperAirplane-Dev-Team/GigaGet",
|
||||||
|
StandardLicenses.GPL3
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Groupie",
|
||||||
|
"2016",
|
||||||
|
"Lisa Wray",
|
||||||
|
"https://github.com/lisawray/groupie",
|
||||||
|
StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Android-State",
|
||||||
|
"2018",
|
||||||
|
"Evernote",
|
||||||
|
"https://github.com/Evernote/android-state",
|
||||||
|
StandardLicenses.EPL1
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Bridge",
|
||||||
|
"2021",
|
||||||
|
"Livefront",
|
||||||
|
"https://github.com/livefront/bridge",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Jsoup",
|
||||||
|
"2009 - 2020",
|
||||||
|
"Jonathan Hedley",
|
||||||
|
"https://github.com/jhy/jsoup",
|
||||||
|
StandardLicenses.MIT
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Markwon",
|
||||||
|
"2019",
|
||||||
|
"Dimitry Ivanov",
|
||||||
|
"https://github.com/noties/Markwon",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Material Components for Android",
|
||||||
|
"2016 - 2020",
|
||||||
|
"Google, Inc.",
|
||||||
|
"https://github.com/material-components/material-components-android",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"NewPipe Extractor",
|
||||||
|
"2017 - 2020",
|
||||||
|
"Christian Schabesberger",
|
||||||
|
"https://github.com/TeamNewPipe/NewPipeExtractor",
|
||||||
|
StandardLicenses.GPL3
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"NoNonsense-FilePicker",
|
||||||
|
"2016",
|
||||||
|
"Jonas Kalderstam",
|
||||||
|
"https://github.com/spacecowboy/NoNonsense-FilePicker",
|
||||||
|
StandardLicenses.MPL2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"OkHttp",
|
||||||
|
"2019",
|
||||||
|
"Square, Inc.",
|
||||||
|
"https://square.github.io/okhttp/",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"Coil",
|
||||||
|
"2023",
|
||||||
|
"Coil Contributors",
|
||||||
|
"https://coil-kt.github.io/coil/",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"PrettyTime",
|
||||||
|
"2012 - 2020",
|
||||||
|
"Lincoln Baxter, III",
|
||||||
|
"https://github.com/ocpsoft/prettytime",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"ProcessPhoenix",
|
||||||
|
"2015",
|
||||||
|
"Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/ProcessPhoenix",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxAndroid",
|
||||||
|
"2015",
|
||||||
|
"The RxAndroid authors",
|
||||||
|
"https://github.com/ReactiveX/RxAndroid",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxBinding",
|
||||||
|
"2015",
|
||||||
|
"Jake Wharton",
|
||||||
|
"https://github.com/JakeWharton/RxBinding",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"RxJava",
|
||||||
|
"2016 - 2020",
|
||||||
|
"RxJava Contributors",
|
||||||
|
"https://github.com/ReactiveX/RxJava",
|
||||||
|
StandardLicenses.APACHE2
|
||||||
|
),
|
||||||
|
SoftwareComponent(
|
||||||
|
"SearchPreference",
|
||||||
|
"2018",
|
||||||
|
"ByteHamster",
|
||||||
|
"https://github.com/ByteHamster/SearchPreference",
|
||||||
|
StandardLicenses.MIT
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
11
app/src/main/java/org/schabi/newpipe/about/License.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import java.io.Serializable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for storing information about a software license.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
class License(val name: String, val abbreviation: String, val filename: String) : Parcelable, Serializable
|
||||||
142
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
142
app/src/main/java/org/schabi/newpipe/about/LicenseFragment.kt
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Base64
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
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
|
||||||
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.FragmentLicensesBinding
|
||||||
|
import org.schabi.newpipe.databinding.ItemSoftwareComponentBinding
|
||||||
|
import org.schabi.newpipe.ktx.parcelableArrayList
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment containing the software licenses.
|
||||||
|
*/
|
||||||
|
class LicenseFragment : Fragment() {
|
||||||
|
private lateinit var softwareComponents: List<SoftwareComponent>
|
||||||
|
private var activeSoftwareComponent: SoftwareComponent? = null
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
softwareComponents = arguments?.parcelableArrayList<SoftwareComponent>(ARG_COMPONENTS)!!
|
||||||
|
.sortedBy { it.name } // Sort components by name
|
||||||
|
activeSoftwareComponent = savedInstanceState?.let {
|
||||||
|
BundleCompat.getSerializable(it, SOFTWARE_COMPONENT_KEY, SoftwareComponent::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
compositeDisposable.dispose()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View {
|
||||||
|
val binding = FragmentLicensesBinding.inflate(inflater, container, false)
|
||||||
|
binding.licensesAppReadLicense.setOnClickListener {
|
||||||
|
compositeDisposable.add(
|
||||||
|
showLicense(NEWPIPE_SOFTWARE_COMPONENT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (component in softwareComponents) {
|
||||||
|
val componentBinding = ItemSoftwareComponentBinding
|
||||||
|
.inflate(inflater, container, false)
|
||||||
|
componentBinding.name.text = component.name
|
||||||
|
componentBinding.copyright.text = getString(
|
||||||
|
R.string.copyright,
|
||||||
|
component.years,
|
||||||
|
component.copyrightOwner,
|
||||||
|
component.license.abbreviation
|
||||||
|
)
|
||||||
|
val root: View = componentBinding.root
|
||||||
|
root.tag = component
|
||||||
|
root.setOnClickListener {
|
||||||
|
compositeDisposable.add(
|
||||||
|
showLicense(component)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
binding.licensesSoftwareComponents.addView(root)
|
||||||
|
registerForContextMenu(root)
|
||||||
|
}
|
||||||
|
activeSoftwareComponent?.let { compositeDisposable.add(showLicense(it)) }
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
|
activeSoftwareComponent?.let { savedInstanceState.putSerializable(SOFTWARE_COMPONENT_KEY, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLicense(
|
||||||
|
softwareComponent: SoftwareComponent
|
||||||
|
): Disposable {
|
||||||
|
return if (context == null) {
|
||||||
|
Disposable.empty()
|
||||||
|
} else {
|
||||||
|
val context = requireContext()
|
||||||
|
activeSoftwareComponent = softwareComponent
|
||||||
|
Observable.fromCallable { getFormattedLicense(context, softwareComponent.license) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { formattedLicense ->
|
||||||
|
val webViewData = Base64.encodeToString(
|
||||||
|
formattedLicense.toByteArray(),
|
||||||
|
Base64.NO_PADDING
|
||||||
|
)
|
||||||
|
val webView = WebView(context)
|
||||||
|
webView.loadData(webViewData, "text/html; charset=UTF-8", "base64")
|
||||||
|
|
||||||
|
val builder = AlertDialog.Builder(requireContext())
|
||||||
|
.setTitle(softwareComponent.name)
|
||||||
|
.setView(webView)
|
||||||
|
.setOnCancelListener { activeSoftwareComponent = null }
|
||||||
|
.setOnDismissListener { activeSoftwareComponent = null }
|
||||||
|
.setPositiveButton(R.string.done) { dialog, _ -> dialog.dismiss() }
|
||||||
|
|
||||||
|
if (softwareComponent != NEWPIPE_SOFTWARE_COMPONENT) {
|
||||||
|
builder.setNeutralButton(R.string.open_website_license) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(requireContext(), softwareComponent.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ARG_COMPONENTS = "components"
|
||||||
|
private const val SOFTWARE_COMPONENT_KEY = "ACTIVE_SOFTWARE_COMPONENT"
|
||||||
|
private val NEWPIPE_SOFTWARE_COMPONENT = SoftwareComponent(
|
||||||
|
"NewPipe",
|
||||||
|
"2014-2023",
|
||||||
|
"Team NewPipe",
|
||||||
|
"https://newpipe.net/",
|
||||||
|
StandardLicenses.GPL3,
|
||||||
|
BuildConfig.VERSION_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
fun newInstance(softwareComponents: ArrayList<SoftwareComponent>): LicenseFragment {
|
||||||
|
val fragment = LicenseFragment()
|
||||||
|
fragment.arguments = bundleOf(ARG_COMPONENTS to softwareComponents)
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.IOException
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the context to use
|
||||||
|
* @param license the license
|
||||||
|
* @return String which contains a HTML formatted license page
|
||||||
|
* styled according to the context's theme
|
||||||
|
*/
|
||||||
|
fun getFormattedLicense(context: Context, license: License): String {
|
||||||
|
try {
|
||||||
|
return context.assets.open(license.filename).bufferedReader().use { it.readText() }
|
||||||
|
// split the HTML file and insert the stylesheet into the HEAD of the file
|
||||||
|
.replace("</head>", "<style>${getLicenseStylesheet(context)}</style></head>")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw IllegalArgumentException("Could not get license file: ${license.filename}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param context the Android context
|
||||||
|
* @return String which is a CSS stylesheet according to the context's theme
|
||||||
|
*/
|
||||||
|
fun getLicenseStylesheet(context: Context): String {
|
||||||
|
val isLightTheme = ThemeHelper.isLightThemeSelected(context)
|
||||||
|
val licenseBackgroundColor = getHexRGBColor(
|
||||||
|
context,
|
||||||
|
if (isLightTheme) R.color.light_license_background_color else R.color.dark_license_background_color
|
||||||
|
)
|
||||||
|
val licenseTextColor = getHexRGBColor(
|
||||||
|
context,
|
||||||
|
if (isLightTheme) R.color.light_license_text_color else R.color.dark_license_text_color
|
||||||
|
)
|
||||||
|
val youtubePrimaryColor = getHexRGBColor(
|
||||||
|
context,
|
||||||
|
if (isLightTheme) R.color.light_youtube_primary_color else R.color.dark_youtube_primary_color
|
||||||
|
)
|
||||||
|
return "body{padding:12px 15px;margin:0;background:#$licenseBackgroundColor;color:#$licenseTextColor}" +
|
||||||
|
"a[href]{color:#$youtubePrimaryColor}pre{white-space:pre-wrap}"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cast R.color to a hexadecimal color value.
|
||||||
|
*
|
||||||
|
* @param context the context to use
|
||||||
|
* @param color the color number from R.color
|
||||||
|
* @return a six characters long String with hexadecimal RGB values
|
||||||
|
*/
|
||||||
|
fun getHexRGBColor(context: Context, color: Int): String {
|
||||||
|
return context.getString(color).substring(3)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import java.io.Serializable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
class SoftwareComponent
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
val name: String,
|
||||||
|
val years: String,
|
||||||
|
val copyrightOwner: String,
|
||||||
|
val link: String,
|
||||||
|
val license: License,
|
||||||
|
val version: String? = null
|
||||||
|
) : Parcelable, Serializable
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package org.schabi.newpipe.about
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class containing information about standard software licenses.
|
||||||
|
*/
|
||||||
|
object StandardLicenses {
|
||||||
|
@JvmField
|
||||||
|
val GPL3 = License("GNU General Public License, Version 3.0", "GPLv3", "gpl_3.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val APACHE2 = License("Apache License, Version 2.0", "ALv2", "apache2.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MPL2 = License("Mozilla Public License, Version 2.0", "MPL 2.0", "mpl2.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val MIT = License("MIT License", "MIT", "mit.html")
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val EPL1 = License("Eclipse Public License, Version 1.0", "EPL 1.0", "epl1.html")
|
||||||
|
}
|
||||||
68
app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt
Normal file
68
app/src/main/java/org/schabi/newpipe/database/AppDatabase.kt
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import org.schabi.newpipe.database.feed.dao.FeedDAO
|
||||||
|
import org.schabi.newpipe.database.feed.dao.FeedGroupDAO
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
|
import org.schabi.newpipe.database.history.dao.SearchHistoryDAO
|
||||||
|
import org.schabi.newpipe.database.history.dao.StreamHistoryDAO
|
||||||
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistRemoteDAO
|
||||||
|
import org.schabi.newpipe.database.playlist.dao.PlaylistStreamDAO
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamDAO
|
||||||
|
import org.schabi.newpipe.database.stream.dao.StreamStateDAO
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionDAO
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
@Database(
|
||||||
|
version = Migrations.DB_VER_9,
|
||||||
|
entities = [
|
||||||
|
SubscriptionEntity::class,
|
||||||
|
SearchHistoryEntry::class,
|
||||||
|
StreamEntity::class,
|
||||||
|
StreamHistoryEntity::class,
|
||||||
|
StreamStateEntity::class,
|
||||||
|
PlaylistEntity::class,
|
||||||
|
PlaylistStreamEntity::class,
|
||||||
|
PlaylistRemoteEntity::class,
|
||||||
|
FeedEntity::class,
|
||||||
|
FeedGroupEntity::class,
|
||||||
|
FeedGroupSubscriptionEntity::class,
|
||||||
|
FeedLastUpdatedEntity::class
|
||||||
|
]
|
||||||
|
)
|
||||||
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
abstract fun feedDAO(): FeedDAO
|
||||||
|
abstract fun feedGroupDAO(): FeedGroupDAO
|
||||||
|
abstract fun playlistDAO(): PlaylistDAO
|
||||||
|
abstract fun playlistRemoteDAO(): PlaylistRemoteDAO
|
||||||
|
abstract fun playlistStreamDAO(): PlaylistStreamDAO
|
||||||
|
abstract fun searchHistoryDAO(): SearchHistoryDAO
|
||||||
|
abstract fun streamDAO(): StreamDAO
|
||||||
|
abstract fun streamHistoryDAO(): StreamHistoryDAO
|
||||||
|
abstract fun streamStateDAO(): StreamStateDAO
|
||||||
|
abstract fun subscriptionDAO(): SubscriptionDAO
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DATABASE_NAME: String = "newpipe.db"
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt
Normal file
42
app/src/main/java/org/schabi/newpipe/database/BasicDAO.kt
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface BasicDAO<Entity> {
|
||||||
|
|
||||||
|
/* Inserts */
|
||||||
|
@Insert
|
||||||
|
fun insert(entity: Entity): Long
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
fun insertAll(entities: Collection<Entity>): List<Long>
|
||||||
|
|
||||||
|
/* Searches */
|
||||||
|
fun getAll(): Flowable<List<Entity>>
|
||||||
|
|
||||||
|
fun listByService(serviceId: Int): Flowable<List<Entity>>
|
||||||
|
|
||||||
|
/* Deletes */
|
||||||
|
@Delete
|
||||||
|
fun delete(entity: Entity)
|
||||||
|
|
||||||
|
fun deleteAll(): Int
|
||||||
|
|
||||||
|
/* Updates */
|
||||||
|
@Update
|
||||||
|
fun update(entity: Entity): Int
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(entities: Collection<Entity>)
|
||||||
|
}
|
||||||
52
app/src/main/java/org/schabi/newpipe/database/Converters.kt
Normal file
52
app/src/main/java/org/schabi/newpipe/database/Converters.kt
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
/**
|
||||||
|
* Convert a long value to a [OffsetDateTime].
|
||||||
|
*
|
||||||
|
* @param value the long value
|
||||||
|
* @return the `OffsetDateTime`
|
||||||
|
*/
|
||||||
|
@TypeConverter
|
||||||
|
fun offsetDateTimeFromTimestamp(value: Long?): OffsetDateTime? {
|
||||||
|
return value?.let { OffsetDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a [OffsetDateTime] to a long value.
|
||||||
|
*
|
||||||
|
* @param offsetDateTime the `OffsetDateTime`
|
||||||
|
* @return the long value
|
||||||
|
*/
|
||||||
|
@TypeConverter
|
||||||
|
fun offsetDateTimeToTimestamp(offsetDateTime: OffsetDateTime?): Long? {
|
||||||
|
return offsetDateTime?.withOffsetSameInstant(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun streamTypeOf(value: String): StreamType {
|
||||||
|
return StreamType.valueOf(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringOf(streamType: StreamType): String {
|
||||||
|
return streamType.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun integerOf(feedGroupIcon: FeedGroupIcon): Int {
|
||||||
|
return feedGroupIcon.id
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun feedGroupIconOf(id: Int): FeedGroupIcon {
|
||||||
|
return FeedGroupIcon.entries.first { it.id == id }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
Normal file
19
app/src/main/java/org/schabi/newpipe/database/LocalItem.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2020 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
interface LocalItem {
|
||||||
|
val localItemType: LocalItemType
|
||||||
|
|
||||||
|
enum class LocalItemType {
|
||||||
|
PLAYLIST_LOCAL_ITEM,
|
||||||
|
PLAYLIST_REMOTE_ITEM,
|
||||||
|
|
||||||
|
PLAYLIST_STREAM_ITEM,
|
||||||
|
STATISTIC_STREAM_ITEM
|
||||||
|
}
|
||||||
|
}
|
||||||
351
app/src/main/java/org/schabi/newpipe/database/Migrations.kt
Normal file
351
app/src/main/java/org/schabi/newpipe/database/Migrations.kt
Normal file
|
|
@ -0,0 +1,351 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import 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 = Migration(DB_VER_1, DB_VER_2) { db ->
|
||||||
|
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 = Migration(DB_VER_2, DB_VER_3) { db ->
|
||||||
|
// 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 = Migration(DB_VER_3, DB_VER_4) { db ->
|
||||||
|
db.execSQL("ALTER TABLE streams ADD COLUMN uploader_url TEXT")
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_4_5 = Migration(DB_VER_4, DB_VER_5) { db ->
|
||||||
|
db.execSQL(
|
||||||
|
"ALTER TABLE `subscriptions` ADD COLUMN `notification_mode` " +
|
||||||
|
"INTEGER NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_5_6 = Migration(DB_VER_5, DB_VER_6) { db ->
|
||||||
|
db.execSQL(
|
||||||
|
"ALTER TABLE `playlists` ADD COLUMN `is_thumbnail_permanent` " +
|
||||||
|
"INTEGER NOT NULL DEFAULT 0"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_6_7 = Migration(DB_VER_6, DB_VER_7) { db ->
|
||||||
|
// 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 = Migration(DB_VER_7, DB_VER_8) { db ->
|
||||||
|
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 = Migration(DB_VER_8, DB_VER_9) { db ->
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
package org.schabi.newpipe.database.feed.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
|
||||||
|
import org.schabi.newpipe.database.stream.StreamWithState
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.NotificationMode
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FeedDAO {
|
||||||
|
@Query("DELETE FROM feed")
|
||||||
|
abstract fun deleteAll(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param groupId the group id to get feed streams of; use
|
||||||
|
* [FeedGroupEntity.GROUP_ALL_ID] to not filter by group
|
||||||
|
* @param includePlayed if false, only return all of the live, never-played or non-finished
|
||||||
|
* feed streams (see `@see` items); if true no filter is applied
|
||||||
|
* @param uploadDateBefore get only streams uploaded before this date (useful to filter out
|
||||||
|
* future streams); use null to not filter by upload date
|
||||||
|
* @return the feed streams filtered according to the conditions provided in the parameters
|
||||||
|
* @see StreamStateEntity.isFinished()
|
||||||
|
* @see StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||||
|
* @see StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT s.*, sst.progress_time
|
||||||
|
FROM streams s
|
||||||
|
|
||||||
|
LEFT JOIN stream_state sst
|
||||||
|
ON s.uid = sst.stream_id
|
||||||
|
|
||||||
|
LEFT JOIN stream_history sh
|
||||||
|
ON s.uid = sh.stream_id
|
||||||
|
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON (
|
||||||
|
:groupId <> ${FeedGroupEntity.GROUP_ALL_ID}
|
||||||
|
AND fgs.subscription_id = f.subscription_id
|
||||||
|
)
|
||||||
|
|
||||||
|
WHERE (
|
||||||
|
:groupId = ${FeedGroupEntity.GROUP_ALL_ID}
|
||||||
|
OR fgs.group_id = :groupId
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
:includePlayed
|
||||||
|
OR sh.stream_id IS NULL
|
||||||
|
OR sst.stream_id IS NULL
|
||||||
|
OR sst.progress_time < s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||||
|
OR sst.progress_time < s.duration * 1000 * 3 / 4
|
||||||
|
OR s.stream_type = 'LIVE_STREAM'
|
||||||
|
OR s.stream_type = 'AUDIO_LIVE_STREAM'
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
:includePartiallyPlayed
|
||||||
|
OR sh.stream_id IS NULL
|
||||||
|
OR sst.stream_id IS NULL
|
||||||
|
OR (sst.progress_time <= ${StreamStateEntity.PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS}
|
||||||
|
AND sst.progress_time <= s.duration * 1000 / 4)
|
||||||
|
OR (sst.progress_time >= s.duration * 1000 - ${StreamStateEntity.PLAYBACK_FINISHED_END_MILLISECONDS}
|
||||||
|
AND sst.progress_time >= s.duration * 1000 * 3 / 4)
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
:uploadDateBefore IS NULL
|
||||||
|
OR s.upload_date IS NULL
|
||||||
|
OR s.upload_date < :uploadDateBefore
|
||||||
|
)
|
||||||
|
|
||||||
|
ORDER BY s.upload_date IS NULL DESC, s.upload_date DESC, s.uploader ASC
|
||||||
|
LIMIT 500
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getStreams(
|
||||||
|
groupId: Long,
|
||||||
|
includePlayed: Boolean,
|
||||||
|
includePartiallyPlayed: Boolean,
|
||||||
|
uploadDateBefore: OffsetDateTime?
|
||||||
|
): Maybe<List<StreamWithState>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove links to streams that are older than the given date
|
||||||
|
* **but keep at least one stream per uploader**.
|
||||||
|
*
|
||||||
|
* One stream per uploader is kept because it is needed as reference
|
||||||
|
* when fetching new streams to check if they are new or not.
|
||||||
|
* @param offsetDateTime the newest date to keep, older streams are removed
|
||||||
|
*/
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM feed
|
||||||
|
WHERE feed.stream_id IN (SELECT uid from (
|
||||||
|
SELECT s.uid,
|
||||||
|
(SELECT MAX(upload_date)
|
||||||
|
FROM streams s1
|
||||||
|
INNER JOIN feed f1
|
||||||
|
ON s1.uid = f1.stream_id
|
||||||
|
WHERE f1.subscription_id = f.subscription_id) max_upload_date
|
||||||
|
FROM streams s
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.upload_date < :offsetDateTime
|
||||||
|
AND s.upload_date <> max_upload_date))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun unlinkStreamsOlderThan(offsetDateTime: OffsetDateTime)
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM feed
|
||||||
|
|
||||||
|
WHERE feed.subscription_id = :subscriptionId
|
||||||
|
|
||||||
|
AND feed.stream_id IN (
|
||||||
|
SELECT s.uid FROM streams s
|
||||||
|
|
||||||
|
INNER JOIN feed f
|
||||||
|
ON s.uid = f.stream_id
|
||||||
|
|
||||||
|
WHERE s.stream_type = "LIVE_STREAM" OR s.stream_type = "AUDIO_LIVE_STREAM"
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun unlinkOldLivestreams(subscriptionId: Long)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun insert(feedEntity: FeedEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun insertAll(entities: List<FeedEntity>): List<Long>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun insertLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity): Long
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun updateLastUpdated(lastUpdatedEntity: FeedLastUpdatedEntity)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun setLastUpdatedForSubscription(lastUpdatedEntity: FeedLastUpdatedEntity) {
|
||||||
|
val id = insertLastUpdated(lastUpdatedEntity)
|
||||||
|
|
||||||
|
if (id == -1L) {
|
||||||
|
updateLastUpdated(lastUpdatedEntity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT MIN(lu.last_updated) FROM feed_last_updated lu
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON fgs.subscription_id = lu.subscription_id AND fgs.group_id = :groupId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun oldestSubscriptionUpdate(groupId: Long): Flowable<List<OffsetDateTime?>>
|
||||||
|
|
||||||
|
@Query("SELECT MIN(last_updated) FROM feed_last_updated")
|
||||||
|
abstract fun oldestSubscriptionUpdateFromAll(): Flowable<List<OffsetDateTime?>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM feed_last_updated WHERE last_updated IS NULL")
|
||||||
|
abstract fun notLoadedCount(): Flowable<Long>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT COUNT(*) FROM subscriptions s
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE lu.last_updated IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun notLoadedCountForGroup(groupId: Long): Flowable<Long>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getAllOutdated(outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
|
INNER JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id AND fgs.group_id = :groupId
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getAllOutdatedForGroup(groupId: Long, outdatedThreshold: OffsetDateTime): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT s.* FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_last_updated lu
|
||||||
|
ON s.uid = lu.subscription_id
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
(lu.last_updated IS NULL OR lu.last_updated < :outdatedThreshold)
|
||||||
|
AND s.notification_mode = :notificationMode
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getOutdatedWithNotificationMode(
|
||||||
|
outdatedThreshold: OffsetDateTime,
|
||||||
|
@NotificationMode notificationMode: Int
|
||||||
|
): Flowable<List<SubscriptionEntity>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package org.schabi.newpipe.database.feed.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FeedGroupDAO {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM feed_group ORDER BY sort_order ASC")
|
||||||
|
abstract fun getAll(): Flowable<List<FeedGroupEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM feed_group WHERE uid = :groupId")
|
||||||
|
abstract fun getGroup(groupId: Long): Maybe<FeedGroupEntity>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun insert(feedGroupEntity: FeedGroupEntity): Long {
|
||||||
|
val nextSortOrder = nextSortOrder()
|
||||||
|
feedGroupEntity.sortOrder = nextSortOrder
|
||||||
|
return insertInternal(feedGroupEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun update(feedGroupEntity: FeedGroupEntity): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM feed_group")
|
||||||
|
abstract fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM feed_group WHERE uid = :groupId")
|
||||||
|
abstract fun delete(groupId: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT subscription_id FROM feed_group_subscription_join WHERE group_id = :groupId")
|
||||||
|
abstract fun getSubscriptionIdsFor(groupId: Long): Flowable<List<Long>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM feed_group_subscription_join WHERE group_id = :groupId")
|
||||||
|
abstract fun deleteSubscriptionsFromGroup(groupId: Long): Int
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
abstract fun insertSubscriptionsToGroup(entities: List<FeedGroupSubscriptionEntity>): List<Long>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun updateSubscriptionsForGroup(groupId: Long, subscriptionIds: List<Long>) {
|
||||||
|
deleteSubscriptionsFromGroup(groupId)
|
||||||
|
insertSubscriptionsToGroup(subscriptionIds.map { FeedGroupSubscriptionEntity(groupId, it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun updateOrder(orderMap: Map<Long, Long>) {
|
||||||
|
orderMap.forEach { (groupId, sortOrder) -> updateOrder(groupId, sortOrder) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("UPDATE feed_group SET sort_order = :sortOrder WHERE uid = :groupId")
|
||||||
|
abstract fun updateOrder(groupId: Long, sortOrder: Long): Int
|
||||||
|
|
||||||
|
@Query("SELECT IFNULL(MAX(sort_order) + 1, 0) FROM feed_group")
|
||||||
|
protected abstract fun nextSortOrder(): Long
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
protected abstract fun insertInternal(feedGroupEntity: FeedGroupEntity): Long
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.FEED_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.STREAM_ID
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedEntity.Companion.SUBSCRIPTION_ID
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_TABLE,
|
||||||
|
primaryKeys = [STREAM_ID, SUBSCRIPTION_ID],
|
||||||
|
indices = [Index(SUBSCRIPTION_ID)],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = StreamEntity::class,
|
||||||
|
parentColumns = [StreamEntity.STREAM_ID],
|
||||||
|
childColumns = [STREAM_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
deferred = true
|
||||||
|
),
|
||||||
|
ForeignKey(
|
||||||
|
entity = SubscriptionEntity::class,
|
||||||
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
deferred = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class FeedEntity(
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
var streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||||
|
var subscriptionId: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FEED_TABLE = "feed"
|
||||||
|
|
||||||
|
const val STREAM_ID = "stream_id"
|
||||||
|
const val SUBSCRIPTION_ID = "subscription_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.FEED_GROUP_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupEntity.Companion.SORT_ORDER
|
||||||
|
import org.schabi.newpipe.local.subscription.FeedGroupIcon
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_GROUP_TABLE,
|
||||||
|
indices = [Index(SORT_ORDER)]
|
||||||
|
)
|
||||||
|
data class FeedGroupEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = ID)
|
||||||
|
val uid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = NAME)
|
||||||
|
var name: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = ICON)
|
||||||
|
var icon: FeedGroupIcon,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SORT_ORDER)
|
||||||
|
var sortOrder: Long = -1
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val FEED_GROUP_TABLE = "feed_group"
|
||||||
|
|
||||||
|
const val ID = "uid"
|
||||||
|
const val NAME = "name"
|
||||||
|
const val ICON = "icon_id"
|
||||||
|
const val SORT_ORDER = "sort_order"
|
||||||
|
|
||||||
|
const val GROUP_ALL_ID = -1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.Index
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.FEED_GROUP_SUBSCRIPTION_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.GROUP_ID
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedGroupSubscriptionEntity.Companion.SUBSCRIPTION_ID
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_GROUP_SUBSCRIPTION_TABLE,
|
||||||
|
primaryKeys = [GROUP_ID, SUBSCRIPTION_ID],
|
||||||
|
indices = [Index(SUBSCRIPTION_ID)],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = FeedGroupEntity::class,
|
||||||
|
parentColumns = [FeedGroupEntity.ID],
|
||||||
|
childColumns = [GROUP_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
deferred = true
|
||||||
|
),
|
||||||
|
|
||||||
|
ForeignKey(
|
||||||
|
entity = SubscriptionEntity::class,
|
||||||
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
deferred = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class FeedGroupSubscriptionEntity(
|
||||||
|
@ColumnInfo(name = GROUP_ID)
|
||||||
|
var feedGroupId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||||
|
var subscriptionId: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val FEED_GROUP_SUBSCRIPTION_TABLE = "feed_group_subscription_join"
|
||||||
|
|
||||||
|
const val GROUP_ID = "group_id"
|
||||||
|
const val SUBSCRIPTION_ID = "subscription_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package org.schabi.newpipe.database.feed.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.FEED_LAST_UPDATED_TABLE
|
||||||
|
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity.Companion.SUBSCRIPTION_ID
|
||||||
|
import org.schabi.newpipe.database.subscription.SubscriptionEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = FEED_LAST_UPDATED_TABLE,
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = SubscriptionEntity::class,
|
||||||
|
parentColumns = [SubscriptionEntity.SUBSCRIPTION_UID],
|
||||||
|
childColumns = [SUBSCRIPTION_ID],
|
||||||
|
onDelete = ForeignKey.CASCADE,
|
||||||
|
onUpdate = ForeignKey.CASCADE,
|
||||||
|
deferred = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class FeedLastUpdatedEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_ID)
|
||||||
|
var subscriptionId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = LAST_UPDATED)
|
||||||
|
var lastUpdated: OffsetDateTime? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val FEED_LAST_UPDATED_TABLE = "feed_last_updated"
|
||||||
|
|
||||||
|
const val SUBSCRIPTION_ID = "subscription_id"
|
||||||
|
const val LAST_UPDATED = "last_updated"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2021 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.history.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.history.model.SearchHistoryEntry
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface SearchHistoryDAO : BasicDAO<SearchHistoryEntry> {
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM search_history WHERE id = (SELECT MAX(id) FROM search_history)")
|
||||||
|
val latestEntry: SearchHistoryEntry?
|
||||||
|
|
||||||
|
@Query("DELETE FROM search_history")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM search_history WHERE search = :query")
|
||||||
|
fun deleteAllWhereQuery(query: String): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM search_history ORDER BY creation_date DESC")
|
||||||
|
override fun getAll(): Flowable<List<SearchHistoryEntry>>
|
||||||
|
|
||||||
|
@Query("SELECT search FROM search_history GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit")
|
||||||
|
fun getUniqueEntries(limit: Int): Flowable<MutableList<String>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM search_history WHERE service_id = :serviceId ORDER BY creation_date DESC")
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<SearchHistoryEntry>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT search FROM search_history WHERE search LIKE :query ||
|
||||||
|
'%' GROUP BY search ORDER BY MAX(creation_date) DESC LIMIT :limit
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getSimilarEntries(query: String, limit: Int): Flowable<MutableList<String>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.history.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntity
|
||||||
|
import org.schabi.newpipe.database.history.model.StreamHistoryEntry
|
||||||
|
import org.schabi.newpipe.database.stream.StreamStatisticsEntry
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class StreamHistoryDAO : BasicDAO<StreamHistoryEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM stream_history")
|
||||||
|
abstract override fun getAll(): Flowable<List<StreamHistoryEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM stream_history")
|
||||||
|
abstract override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<StreamHistoryEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY access_date DESC")
|
||||||
|
abstract val history: Flowable<MutableList<StreamHistoryEntry>>
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM streams INNER JOIN stream_history ON uid = stream_id ORDER BY uid ASC")
|
||||||
|
abstract val historySortedById: Flowable<MutableList<StreamHistoryEntry>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM stream_history WHERE stream_id = :streamId ORDER BY access_date DESC LIMIT 1")
|
||||||
|
abstract fun getLatestEntry(streamId: Long): StreamHistoryEntity?
|
||||||
|
|
||||||
|
@Query("DELETE FROM stream_history WHERE stream_id = :streamId")
|
||||||
|
abstract fun deleteStreamHistory(streamId: Long): Int
|
||||||
|
|
||||||
|
// Select the latest entry and watch count for each stream id on history table
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM streams
|
||||||
|
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT stream_id, MAX(access_date) AS latestAccess, SUM(repeat_count) AS watchCount
|
||||||
|
FROM stream_history
|
||||||
|
GROUP BY stream_id
|
||||||
|
)
|
||||||
|
ON uid = stream_id
|
||||||
|
|
||||||
|
LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state )
|
||||||
|
ON uid = stream_id_alias
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getStatistics(): Flowable<MutableList<StreamStatisticsEntry>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = SearchHistoryEntry.TABLE_NAME,
|
||||||
|
indices = [Index(value = [SearchHistoryEntry.SEARCH])]
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
val id: Long = 0
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
fun hasEqualValues(otherEntry: SearchHistoryEntry): Boolean {
|
||||||
|
return serviceId == otherEntry.serviceId && search == otherEntry.search
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ID = "id"
|
||||||
|
const val TABLE_NAME = "search_history"
|
||||||
|
const val SERVICE_ID = "service_id"
|
||||||
|
const val CREATION_DATE = "creation_date"
|
||||||
|
const val SEARCH = "search"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.schabi.newpipe.database.history.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
data class StreamHistoryEntry(
|
||||||
|
@Embedded
|
||||||
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||||
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_ACCESS_DATE)
|
||||||
|
val accessDate: OffsetDateTime,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.STREAM_REPEAT_COUNT)
|
||||||
|
val repeatCount: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun toStreamHistoryEntity(): StreamHistoryEntity {
|
||||||
|
return StreamHistoryEntity(streamId, accessDate, repeatCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasEqualValues(other: StreamHistoryEntry): Boolean {
|
||||||
|
return this.streamEntity.uid == other.streamEntity.uid && streamId == other.streamId &&
|
||||||
|
accessDate.isEqual(other.accessDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem = StreamInfoItem(
|
||||||
|
streamEntity.serviceId,
|
||||||
|
streamEntity.url,
|
||||||
|
streamEntity.title,
|
||||||
|
streamEntity.streamType
|
||||||
|
).apply {
|
||||||
|
duration = streamEntity.duration
|
||||||
|
uploaderName = streamEntity.uploader
|
||||||
|
uploaderUrl = streamEntity.uploaderUrl
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(streamEntity.thumbnailUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class adds a field to [PlaylistMetadataEntry] that contains an integer representing
|
||||||
|
* how many times a specific stream is already contained inside a local playlist. Used to be able
|
||||||
|
* to grey out playlists which already contain the current stream in the playlist append dialog.
|
||||||
|
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager.getPlaylistDuplicates
|
||||||
|
*/
|
||||||
|
data class PlaylistDuplicatesEntry(
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_ID)
|
||||||
|
override val uid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_URL)
|
||||||
|
override val thumbnailUrl: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
override val isThumbnailPermanent: Boolean?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
override val thumbnailStreamId: Long?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_DISPLAY_INDEX)
|
||||||
|
override var displayIndex: Long?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
|
override val streamCount: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_NAME)
|
||||||
|
override val orderingName: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
|
||||||
|
val timesStreamIsContained: Long
|
||||||
|
) : PlaylistMetadataEntry(
|
||||||
|
uid = uid,
|
||||||
|
orderingName = orderingName,
|
||||||
|
thumbnailUrl = thumbnailUrl,
|
||||||
|
isThumbnailPermanent = isThumbnailPermanent,
|
||||||
|
thumbnailStreamId = thumbnailStreamId,
|
||||||
|
displayIndex = displayIndex,
|
||||||
|
streamCount = streamCount
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val PLAYLIST_TIMES_STREAM_IS_CONTAINED: String = "timesStreamIsContained"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
|
import org.schabi.newpipe.database.LocalItem
|
||||||
|
|
||||||
|
interface PlaylistLocalItem : LocalItem {
|
||||||
|
val orderingName: String?
|
||||||
|
val displayIndex: Long?
|
||||||
|
val uid: Long
|
||||||
|
val thumbnailUrl: String?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import org.schabi.newpipe.database.LocalItem.LocalItemType
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
|
||||||
|
open class PlaylistMetadataEntry(
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_ID)
|
||||||
|
override val uid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_NAME)
|
||||||
|
override val orderingName: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_URL)
|
||||||
|
override val thumbnailUrl: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_DISPLAY_INDEX)
|
||||||
|
override var displayIndex: Long?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
open val isThumbnailPermanent: Boolean?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistEntity.PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
open val thumbnailStreamId: Long?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_STREAM_COUNT)
|
||||||
|
open val streamCount: Long
|
||||||
|
) : PlaylistLocalItem {
|
||||||
|
|
||||||
|
override val localItemType: LocalItemType
|
||||||
|
get() = LocalItemType.PLAYLIST_LOCAL_ITEM
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PLAYLIST_STREAM_COUNT: String = "streamCount"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020-2023 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import org.schabi.newpipe.database.LocalItem
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
data class PlaylistStreamEntry(
|
||||||
|
@Embedded
|
||||||
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||||
|
val progressMillis: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
|
||||||
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PlaylistStreamEntity.JOIN_INDEX)
|
||||||
|
val joinIndex: Int
|
||||||
|
) : LocalItem {
|
||||||
|
|
||||||
|
override val localItemType: LocalItem.LocalItemType
|
||||||
|
get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||||
|
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2022 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PlaylistDAO : BasicDAO<PlaylistEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlists")
|
||||||
|
override fun getAll(): Flowable<List<PlaylistEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlists")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<PlaylistEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlists WHERE uid = :playlistId")
|
||||||
|
fun getPlaylist(playlistId: Long): Flowable<MutableList<PlaylistEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlists WHERE uid = :playlistId")
|
||||||
|
fun deletePlaylist(playlistId: Long): Int
|
||||||
|
|
||||||
|
@get:Query("SELECT COUNT(*) FROM playlists")
|
||||||
|
val count: Flowable<Long>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun upsertPlaylist(playlist: PlaylistEntity): Long {
|
||||||
|
if (playlist.uid == -1L) {
|
||||||
|
// This situation is probably impossible.
|
||||||
|
return insert(playlist)
|
||||||
|
} else {
|
||||||
|
update(playlist)
|
||||||
|
return playlist.uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PlaylistRemoteDAO : BasicDAO<PlaylistRemoteEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists")
|
||||||
|
override fun getAll(): Flowable<List<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM remote_playlists")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists WHERE service_id = :serviceId")
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists WHERE uid = :playlistId")
|
||||||
|
fun getPlaylist(playlistId: Long): Flowable<PlaylistRemoteEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM remote_playlists WHERE url = :url AND service_id = :serviceId")
|
||||||
|
fun getPlaylist(serviceId: Long, url: String?): Flowable<MutableList<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@get:Query("SELECT * FROM remote_playlists ORDER BY display_index")
|
||||||
|
val playlists: Flowable<MutableList<PlaylistRemoteEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT uid FROM remote_playlists WHERE url = :url AND service_id = :serviceId")
|
||||||
|
fun getPlaylistIdInternal(serviceId: Long, url: String?): Long?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun upsert(playlist: PlaylistRemoteEntity): Long {
|
||||||
|
val playlistId = getPlaylistIdInternal(playlist.serviceId.toLong(), playlist.url)
|
||||||
|
|
||||||
|
if (playlistId == null) {
|
||||||
|
return insert(playlist)
|
||||||
|
} else {
|
||||||
|
playlist.uid = playlistId
|
||||||
|
update(playlist)
|
||||||
|
return playlistId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM remote_playlists WHERE uid = :playlistId")
|
||||||
|
fun deletePlaylist(playlistId: Long): Int
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.DEFAULT_THUMBNAIL_ID
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface PlaylistStreamDAO : BasicDAO<PlaylistStreamEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM playlist_stream_join")
|
||||||
|
override fun getAll(): Flowable<List<PlaylistStreamEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlist_stream_join")
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<PlaylistStreamEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("DELETE FROM playlist_stream_join WHERE playlist_id = :playlistId")
|
||||||
|
fun deleteBatch(playlistId: Long)
|
||||||
|
|
||||||
|
@Query("SELECT COALESCE(MAX(join_index), -1) FROM playlist_stream_join WHERE playlist_id = :playlistId")
|
||||||
|
fun getMaximumIndexOf(playlistId: Long): Flowable<Int>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT CASE WHEN COUNT(*) != 0 then stream_id ELSE $DEFAULT_THUMBNAIL_ID END
|
||||||
|
FROM streams
|
||||||
|
|
||||||
|
LEFT JOIN playlist_stream_join
|
||||||
|
ON uid = stream_id
|
||||||
|
|
||||||
|
WHERE playlist_id = :playlistId LIMIT 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getAutomaticThumbnailStreamId(playlistId: Long): Flowable<Long>
|
||||||
|
|
||||||
|
// get ids of streams of the given playlist then merge with the stream metadata
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM streams
|
||||||
|
|
||||||
|
INNER JOIN (SELECT stream_id, join_index FROM playlist_stream_join WHERE playlist_id = :playlistId)
|
||||||
|
ON uid = stream_id
|
||||||
|
|
||||||
|
LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state )
|
||||||
|
ON uid = stream_id_alias
|
||||||
|
|
||||||
|
ORDER BY join_index ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getOrderedStreamsOf(playlistId: Long): Flowable<MutableList<PlaylistStreamEntry>>
|
||||||
|
|
||||||
|
// If a playlist has no streams, there won’t be any rows in the **playlist_stream_join** table
|
||||||
|
// that have a foreign key to that playlist. Thus, the **playlist_id** will not have a
|
||||||
|
// corresponding value in any rows of the join table. So, if you group by the **playlist_id**,
|
||||||
|
// only playlists that contain videos are grouped and displayed. Look at #9642 #13055
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT uid, name, is_thumbnail_permanent, thumbnail_stream_id, display_index,
|
||||||
|
(SELECT thumbnail_url FROM streams WHERE streams.uid = thumbnail_stream_id) AS thumbnail_url,
|
||||||
|
|
||||||
|
COALESCE(COUNT(playlist_id), 0) AS streamCount FROM playlists
|
||||||
|
|
||||||
|
LEFT JOIN playlist_stream_join
|
||||||
|
ON playlists.uid = playlist_id
|
||||||
|
|
||||||
|
GROUP BY uid
|
||||||
|
ORDER BY display_index
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getPlaylistMetadata(): Flowable<MutableList<PlaylistMetadataEntry>>
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT *, MIN(join_index) FROM streams
|
||||||
|
|
||||||
|
INNER JOIN (SELECT stream_id, join_index FROM playlist_stream_join WHERE playlist_id = :playlistId)
|
||||||
|
ON uid = stream_id
|
||||||
|
|
||||||
|
LEFT JOIN (SELECT stream_id AS stream_id_alias, progress_time FROM stream_state )
|
||||||
|
ON uid = stream_id_alias
|
||||||
|
|
||||||
|
GROUP BY uid
|
||||||
|
ORDER BY MIN(join_index) ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getStreamsWithoutDuplicates(playlistId: Long): Flowable<MutableList<PlaylistStreamEntry>>
|
||||||
|
|
||||||
|
// If a playlist has no streams, there won’t be any rows in the **playlist_stream_join** table
|
||||||
|
// that have a foreign key to that playlist. Thus, the **playlist_id** will not have a
|
||||||
|
// corresponding value in any rows of the join table. So, if you group by the **playlist_id**,
|
||||||
|
// only playlists that contain videos are grouped and displayed. Look at #9642 #13055
|
||||||
|
|
||||||
|
@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 playlists.uid
|
||||||
|
ORDER BY display_index, name
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun getPlaylistDuplicatesMetadata(streamUrl: String): Flowable<MutableList<PlaylistDuplicatesEntry>>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry
|
||||||
|
|
||||||
|
@Entity(tableName = PlaylistEntity.Companion.PLAYLIST_TABLE)
|
||||||
|
data class PlaylistEntity @JvmOverloads constructor(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = PLAYLIST_ID)
|
||||||
|
var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_NAME)
|
||||||
|
var name: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_PERMANENT)
|
||||||
|
var isThumbnailPermanent: Boolean,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_THUMBNAIL_STREAM_ID)
|
||||||
|
var thumbnailStreamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = PLAYLIST_DISPLAY_INDEX)
|
||||||
|
var displayIndex: Long
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(item: PlaylistMetadataEntry) : this(
|
||||||
|
uid = item.uid,
|
||||||
|
name = item.orderingName,
|
||||||
|
isThumbnailPermanent = item.isThumbnailPermanent!!,
|
||||||
|
thumbnailStreamId = item.thumbnailStreamId!!,
|
||||||
|
displayIndex = item.displayIndex!!
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val DEFAULT_THUMBNAIL_ID = -1L
|
||||||
|
|
||||||
|
const val PLAYLIST_TABLE = "playlists"
|
||||||
|
const val PLAYLIST_ID = "uid"
|
||||||
|
const val PLAYLIST_NAME = "name"
|
||||||
|
const val PLAYLIST_THUMBNAIL_URL = "thumbnail_url"
|
||||||
|
const val PLAYLIST_DISPLAY_INDEX = "display_index"
|
||||||
|
const val PLAYLIST_THUMBNAIL_PERMANENT = "is_thumbnail_permanent"
|
||||||
|
const val PLAYLIST_THUMBNAIL_STREAM_ID = "thumbnail_stream_id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist.model
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.schabi.newpipe.database.LocalItem.LocalItemType
|
||||||
|
import org.schabi.newpipe.database.playlist.PlaylistLocalItem
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_TABLE
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistRemoteEntity.Companion.REMOTE_PLAYLIST_URL
|
||||||
|
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
||||||
|
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = REMOTE_PLAYLIST_TABLE,
|
||||||
|
indices = [
|
||||||
|
Index(
|
||||||
|
value = [REMOTE_PLAYLIST_SERVICE_ID, REMOTE_PLAYLIST_URL],
|
||||||
|
unique = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class PlaylistRemoteEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_ID)
|
||||||
|
override var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_SERVICE_ID)
|
||||||
|
val serviceId: Int = NO_SERVICE_ID,
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_NAME)
|
||||||
|
override val orderingName: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_URL)
|
||||||
|
val url: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_THUMBNAIL_URL)
|
||||||
|
override val thumbnailUrl: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_UPLOADER_NAME)
|
||||||
|
val uploader: String?,
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_DISPLAY_INDEX)
|
||||||
|
override var displayIndex: Long = -1, // Make sure the new item is on the top
|
||||||
|
|
||||||
|
@ColumnInfo(name = REMOTE_PLAYLIST_STREAM_COUNT)
|
||||||
|
val streamCount: Long?
|
||||||
|
) : PlaylistLocalItem {
|
||||||
|
|
||||||
|
constructor(playlistInfo: PlaylistInfo) : this(
|
||||||
|
serviceId = playlistInfo.serviceId,
|
||||||
|
orderingName = playlistInfo.name,
|
||||||
|
url = playlistInfo.url,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
||||||
|
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
|
||||||
|
),
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2020 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.playlist.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import androidx.room.Index
|
||||||
|
import org.schabi.newpipe.database.LocalItem
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistEntity.Companion.PLAYLIST_ID
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_INDEX
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_PLAYLIST_ID
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.JOIN_STREAM_ID
|
||||||
|
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.Companion.PLAYLIST_STREAM_JOIN_TABLE
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = PLAYLIST_STREAM_JOIN_TABLE,
|
||||||
|
primaryKeys = [JOIN_PLAYLIST_ID, JOIN_INDEX],
|
||||||
|
indices = [
|
||||||
|
Index(value = [JOIN_PLAYLIST_ID, JOIN_INDEX], unique = true),
|
||||||
|
Index(value = [JOIN_STREAM_ID])
|
||||||
|
],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = PlaylistEntity::class,
|
||||||
|
parentColumns = arrayOf(PLAYLIST_ID),
|
||||||
|
childColumns = arrayOf(JOIN_PLAYLIST_ID),
|
||||||
|
onDelete = CASCADE,
|
||||||
|
onUpdate = CASCADE,
|
||||||
|
deferred = true
|
||||||
|
),
|
||||||
|
ForeignKey(
|
||||||
|
entity = StreamEntity::class,
|
||||||
|
parentColumns = arrayOf(StreamEntity.STREAM_ID),
|
||||||
|
childColumns = arrayOf(JOIN_STREAM_ID),
|
||||||
|
onDelete = CASCADE,
|
||||||
|
onUpdate = CASCADE,
|
||||||
|
deferred = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class PlaylistStreamEntity(
|
||||||
|
@ColumnInfo(name = JOIN_PLAYLIST_ID)
|
||||||
|
val playlistUid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
|
val streamUid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = JOIN_INDEX)
|
||||||
|
val index: Int
|
||||||
|
) : LocalItem {
|
||||||
|
|
||||||
|
override val localItemType: LocalItem.LocalItemType
|
||||||
|
get() = LocalItem.LocalItemType.PLAYLIST_STREAM_ITEM
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val PLAYLIST_STREAM_JOIN_TABLE = "playlist_stream_join"
|
||||||
|
const val JOIN_PLAYLIST_ID = "playlist_id"
|
||||||
|
const val JOIN_STREAM_ID = "stream_id"
|
||||||
|
const val JOIN_INDEX = "join_index"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2020-2023 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.stream
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
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.Companion.STREAM_PROGRESS_MILLIS
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
data class StreamStatisticsEntry(
|
||||||
|
@Embedded
|
||||||
|
val streamEntity: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0")
|
||||||
|
val progressMillis: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
|
||||||
|
val streamId: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_LATEST_DATE)
|
||||||
|
val latestAccessDate: OffsetDateTime,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_WATCH_COUNT)
|
||||||
|
val watchCount: Long
|
||||||
|
) : LocalItem {
|
||||||
|
|
||||||
|
override val localItemType: LocalItem.LocalItemType
|
||||||
|
get() = LocalItem.LocalItemType.STATISTIC_STREAM_ITEM
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
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 {
|
||||||
|
const val STREAM_LATEST_DATE = "latestAccess"
|
||||||
|
const val STREAM_WATCH_COUNT = "watchCount"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.schabi.newpipe.database.stream
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Embedded
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
|
||||||
|
data class StreamWithState(
|
||||||
|
@Embedded
|
||||||
|
val stream: StreamEntity,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS)
|
||||||
|
val stateProgressMillis: Long?
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
package org.schabi.newpipe.database.stream.dao
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
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.Completable
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.util.StreamTypeUtil
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class StreamDAO : BasicDAO<StreamEntity> {
|
||||||
|
@Query("SELECT * FROM streams")
|
||||||
|
abstract override fun getAll(): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM streams")
|
||||||
|
abstract override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("SELECT * FROM streams WHERE service_id = :serviceId")
|
||||||
|
abstract override fun listByService(serviceId: Int): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||||
|
abstract fun getStream(serviceId: Long, url: String): Flowable<List<StreamEntity>>
|
||||||
|
|
||||||
|
@Query("UPDATE streams SET uploader_url = :uploaderUrl WHERE url = :url AND service_id = :serviceId")
|
||||||
|
abstract fun setUploaderUrl(serviceId: Long, url: String, uploaderUrl: String): Completable
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun silentInsertInternal(stream: StreamEntity): Long
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun silentInsertAllInternal(streams: List<StreamEntity>): List<Long>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) != 0 FROM streams WHERE url = :url AND service_id = :serviceId")
|
||||||
|
internal abstract fun exists(serviceId: Int, url: String): Boolean
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT uid, stream_type, textual_upload_date, upload_date, is_upload_date_approximation, duration
|
||||||
|
FROM streams WHERE url = :url AND service_id = :serviceId
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
internal abstract fun getMinimalStreamForCompare(serviceId: Int, url: String): StreamCompareFeed?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun upsert(newerStream: StreamEntity): Long {
|
||||||
|
val uid = silentInsertInternal(newerStream)
|
||||||
|
|
||||||
|
if (uid != -1L) {
|
||||||
|
newerStream.uid = uid
|
||||||
|
return uid
|
||||||
|
}
|
||||||
|
|
||||||
|
compareAndUpdateStream(newerStream)
|
||||||
|
|
||||||
|
update(newerStream)
|
||||||
|
return newerStream.uid
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun upsertAll(streams: List<StreamEntity>): List<Long> {
|
||||||
|
val insertUidList = silentInsertAllInternal(streams)
|
||||||
|
|
||||||
|
val streamIds = ArrayList<Long>(streams.size)
|
||||||
|
for ((index, uid) in insertUidList.withIndex()) {
|
||||||
|
val newerStream = streams[index]
|
||||||
|
if (uid != -1L) {
|
||||||
|
streamIds.add(uid)
|
||||||
|
newerStream.uid = uid
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
compareAndUpdateStream(newerStream)
|
||||||
|
streamIds.add(newerStream.uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(streams)
|
||||||
|
return streamIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun compareAndUpdateStream(newerStream: StreamEntity) {
|
||||||
|
val existentMinimalStream = getMinimalStreamForCompare(newerStream.serviceId, newerStream.url)
|
||||||
|
?: error("Stream cannot be null just after insertion.")
|
||||||
|
newerStream.uid = existentMinimalStream.uid
|
||||||
|
|
||||||
|
if (!StreamTypeUtil.isLiveStream(newerStream.streamType)) {
|
||||||
|
// Use the existent upload date if the newer stream does not have a better precision
|
||||||
|
// (i.e. is an approximation). This is done to prevent unnecessary changes.
|
||||||
|
val hasBetterPrecision =
|
||||||
|
newerStream.uploadDate != null && newerStream.isUploadDateApproximation != true
|
||||||
|
if (existentMinimalStream.uploadDate != null && !hasBetterPrecision) {
|
||||||
|
newerStream.uploadDate = existentMinimalStream.uploadDate
|
||||||
|
newerStream.textualUploadDate = existentMinimalStream.textualUploadDate
|
||||||
|
newerStream.isUploadDateApproximation = existentMinimalStream.isUploadDateApproximation
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existentMinimalStream.duration > 0 && newerStream.duration < 0) {
|
||||||
|
newerStream.duration = existentMinimalStream.duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
DELETE FROM streams WHERE
|
||||||
|
|
||||||
|
NOT EXISTS (SELECT 1 FROM stream_history sh
|
||||||
|
WHERE sh.stream_id = streams.uid)
|
||||||
|
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM playlist_stream_join ps
|
||||||
|
WHERE ps.stream_id = streams.uid)
|
||||||
|
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM feed f
|
||||||
|
WHERE f.stream_id = streams.uid)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun deleteOrphans(): Int
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal entry class used when comparing/updating an existent stream.
|
||||||
|
*/
|
||||||
|
internal data class StreamCompareFeed(
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TYPE)
|
||||||
|
var streamType: StreamType,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_TEXTUAL_UPLOAD_DATE)
|
||||||
|
var textualUploadDate: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_UPLOAD_DATE)
|
||||||
|
var uploadDate: OffsetDateTime? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||||
|
var isUploadDateApproximation: Boolean? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = StreamEntity.STREAM_DURATION)
|
||||||
|
var duration: Long
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2021 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.stream.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface StreamStateDAO : BasicDAO<StreamStateEntity> {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE)
|
||||||
|
override fun getAll(): Flowable<List<StreamStateEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE)
|
||||||
|
override fun deleteAll(): Int
|
||||||
|
|
||||||
|
override fun listByService(serviceId: Int): Flowable<List<StreamStateEntity>> {
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("SELECT * FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId")
|
||||||
|
fun getState(streamId: Long): Flowable<MutableList<StreamStateEntity>>
|
||||||
|
|
||||||
|
@Query("DELETE FROM " + StreamStateEntity.STREAM_STATE_TABLE + " WHERE " + StreamStateEntity.JOIN_STREAM_ID + " = :streamId")
|
||||||
|
fun deleteState(streamId: Long): Int
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.Companion.IGNORE)
|
||||||
|
fun silentInsertInternal(streamState: StreamStateEntity)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun upsert(stream: StreamStateEntity): Long {
|
||||||
|
silentInsertInternal(stream)
|
||||||
|
return update(stream).toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
package org.schabi.newpipe.database.stream.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.io.Serializable
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_TABLE
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_URL
|
||||||
|
import org.schabi.newpipe.extractor.localization.DateWrapper
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType
|
||||||
|
import org.schabi.newpipe.player.playqueue.PlayQueueItem
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = STREAM_TABLE,
|
||||||
|
indices = [
|
||||||
|
Index(value = [STREAM_SERVICE_ID, STREAM_URL], unique = true)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class StreamEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
@ColumnInfo(name = STREAM_ID)
|
||||||
|
var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_SERVICE_ID)
|
||||||
|
var serviceId: Int,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_URL)
|
||||||
|
var url: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TITLE)
|
||||||
|
var title: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TYPE)
|
||||||
|
var streamType: StreamType,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_DURATION)
|
||||||
|
var duration: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOADER)
|
||||||
|
var uploader: String,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOADER_URL)
|
||||||
|
var uploaderUrl: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_THUMBNAIL_URL)
|
||||||
|
var thumbnailUrl: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_VIEWS)
|
||||||
|
var viewCount: Long? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_TEXTUAL_UPLOAD_DATE)
|
||||||
|
var textualUploadDate: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_UPLOAD_DATE)
|
||||||
|
var uploadDate: OffsetDateTime? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_IS_UPLOAD_DATE_APPROXIMATION)
|
||||||
|
var isUploadDateApproximation: Boolean? = null
|
||||||
|
) : Serializable {
|
||||||
|
@Ignore
|
||||||
|
constructor(item: StreamInfoItem) : this(
|
||||||
|
serviceId = item.serviceId, url = item.url, title = item.name,
|
||||||
|
streamType = item.streamType, duration = item.duration, uploader = item.uploaderName,
|
||||||
|
uploaderUrl = item.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails), viewCount = item.viewCount,
|
||||||
|
textualUploadDate = item.textualUploadDate, uploadDate = item.uploadDate?.offsetDateTime(),
|
||||||
|
isUploadDateApproximation = item.uploadDate?.isApproximation
|
||||||
|
)
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(info: StreamInfo) : this(
|
||||||
|
serviceId = info.serviceId, url = info.url, title = info.name,
|
||||||
|
streamType = info.streamType, duration = info.duration, uploader = info.uploaderName,
|
||||||
|
uploaderUrl = info.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(info.thumbnails), viewCount = info.viewCount,
|
||||||
|
textualUploadDate = info.textualUploadDate, uploadDate = info.uploadDate?.offsetDateTime(),
|
||||||
|
isUploadDateApproximation = info.uploadDate?.isApproximation
|
||||||
|
)
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
constructor(item: PlayQueueItem) : this(
|
||||||
|
serviceId = item.serviceId,
|
||||||
|
url = item.url,
|
||||||
|
title = item.title,
|
||||||
|
streamType = item.streamType,
|
||||||
|
duration = item.duration,
|
||||||
|
uploader = item.uploader,
|
||||||
|
uploaderUrl = item.uploaderUrl,
|
||||||
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(item.thumbnails)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun toStreamInfoItem(): StreamInfoItem {
|
||||||
|
val item = StreamInfoItem(serviceId, url, title, streamType)
|
||||||
|
item.duration = duration
|
||||||
|
item.uploaderName = uploader
|
||||||
|
item.uploaderUrl = uploaderUrl
|
||||||
|
item.thumbnails = ImageStrategy.dbUrlToImageList(thumbnailUrl)
|
||||||
|
|
||||||
|
if (viewCount != null) item.viewCount = viewCount as Long
|
||||||
|
item.textualUploadDate = textualUploadDate
|
||||||
|
item.uploadDate = uploadDate?.let {
|
||||||
|
DateWrapper(it, isUploadDateApproximation ?: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STREAM_TABLE = "streams"
|
||||||
|
const val STREAM_ID = "uid"
|
||||||
|
const val STREAM_SERVICE_ID = "service_id"
|
||||||
|
const val STREAM_URL = "url"
|
||||||
|
const val STREAM_TITLE = "title"
|
||||||
|
const val STREAM_TYPE = "stream_type"
|
||||||
|
const val STREAM_DURATION = "duration"
|
||||||
|
const val STREAM_UPLOADER = "uploader"
|
||||||
|
const val STREAM_UPLOADER_URL = "uploader_url"
|
||||||
|
const val STREAM_THUMBNAIL_URL = "thumbnail_url"
|
||||||
|
|
||||||
|
const val STREAM_VIEWS = "view_count"
|
||||||
|
const val STREAM_TEXTUAL_UPLOAD_DATE = "textual_upload_date"
|
||||||
|
const val STREAM_UPLOAD_DATE = "upload_date"
|
||||||
|
const val STREAM_IS_UPLOAD_DATE_APPROXIMATION = "is_upload_date_approximation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2018-2023 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.stream.model
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.ForeignKey
|
||||||
|
import androidx.room.ForeignKey.Companion.CASCADE
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamEntity.Companion.STREAM_ID
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.JOIN_STREAM_ID
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.PLAYBACK_FINISHED_END_MILLISECONDS
|
||||||
|
import org.schabi.newpipe.database.stream.model.StreamStateEntity.Companion.STREAM_STATE_TABLE
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = STREAM_STATE_TABLE,
|
||||||
|
primaryKeys = [JOIN_STREAM_ID],
|
||||||
|
foreignKeys = [
|
||||||
|
ForeignKey(
|
||||||
|
entity = StreamEntity::class,
|
||||||
|
parentColumns = arrayOf(STREAM_ID),
|
||||||
|
childColumns = arrayOf(JOIN_STREAM_ID),
|
||||||
|
onDelete = CASCADE,
|
||||||
|
onUpdate = CASCADE
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class StreamStateEntity(
|
||||||
|
@ColumnInfo(name = JOIN_STREAM_ID)
|
||||||
|
val streamUid: Long,
|
||||||
|
|
||||||
|
@ColumnInfo(name = STREAM_PROGRESS_MILLIS)
|
||||||
|
val progressMillis: Long
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* The state will be considered valid, and thus be saved, if the progress is more than
|
||||||
|
* [PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS] or at least 1/4 of the video length.
|
||||||
|
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||||
|
* @return whether this stream state entity should be saved or not
|
||||||
|
*/
|
||||||
|
fun isValid(durationInSeconds: Long): Boolean {
|
||||||
|
return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS ||
|
||||||
|
progressMillis > durationInSeconds * 1000 / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The video will be considered as finished, if the time left is less than
|
||||||
|
* [PLAYBACK_FINISHED_END_MILLISECONDS] and the progress is at least 3/4 of the video length.
|
||||||
|
* The state will be saved anyway, so that it can be shown under stream info items, but the
|
||||||
|
* player will not resume if a state is considered as finished. Finished streams are also the
|
||||||
|
* ones that can be filtered out in the feed fragment.
|
||||||
|
* @param durationInSeconds the duration of the stream connected with this state, in seconds
|
||||||
|
* @return whether the stream is finished or not
|
||||||
|
*/
|
||||||
|
fun isFinished(durationInSeconds: Long): Boolean {
|
||||||
|
return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS &&
|
||||||
|
progressMillis >= durationInSeconds * 1000 * 3 / 4
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STREAM_STATE_TABLE = "stream_state"
|
||||||
|
const val JOIN_STREAM_ID = "stream_id"
|
||||||
|
|
||||||
|
// This additional field is required for the SQL query because 'stream_id' is used
|
||||||
|
// for some other joins already
|
||||||
|
const val JOIN_STREAM_ID_ALIAS = "stream_id_alias"
|
||||||
|
const val STREAM_PROGRESS_MILLIS = "progress_time"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Playback state will not be saved, if playback time is less than this threshold
|
||||||
|
* (5000ms = 5s).
|
||||||
|
*/
|
||||||
|
const val PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS = 5000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream will be considered finished if the playback time left exceeds this threshold
|
||||||
|
* (60000ms = 60s).
|
||||||
|
* @see org.schabi.newpipe.database.stream.model.StreamStateEntity.isFinished
|
||||||
|
*/
|
||||||
|
const val PLAYBACK_FINISHED_END_MILLISECONDS = 60000L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.subscription
|
||||||
|
|
||||||
|
import androidx.annotation.IntDef
|
||||||
|
|
||||||
|
@IntDef(NotificationMode.Companion.DISABLED, NotificationMode.Companion.ENABLED)
|
||||||
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
|
annotation class NotificationMode {
|
||||||
|
companion object {
|
||||||
|
const val DISABLED = 0
|
||||||
|
const val ENABLED = 1 // other values reserved for the future
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
package org.schabi.newpipe.database.subscription
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.RewriteQueriesToDropUnusedColumns
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
|
import io.reactivex.rxjava3.core.Maybe
|
||||||
|
import org.schabi.newpipe.database.BasicDAO
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class SubscriptionDAO : BasicDAO<SubscriptionEntity> {
|
||||||
|
@Query("SELECT COUNT(*) FROM subscriptions")
|
||||||
|
abstract fun rowCount(): Flowable<Long>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE service_id = :serviceId")
|
||||||
|
abstract override fun listByService(serviceId: Int): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions ORDER BY name COLLATE NOCASE ASC")
|
||||||
|
abstract override fun getAll(): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM subscriptions
|
||||||
|
|
||||||
|
WHERE name LIKE '%' || :filter || '%'
|
||||||
|
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getSubscriptionsFiltered(filter: String): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id
|
||||||
|
|
||||||
|
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||||
|
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getSubscriptionsOnlyUngrouped(
|
||||||
|
currentGroupId: Long
|
||||||
|
): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT * FROM subscriptions s
|
||||||
|
|
||||||
|
LEFT JOIN feed_group_subscription_join fgs
|
||||||
|
ON s.uid = fgs.subscription_id
|
||||||
|
|
||||||
|
WHERE (fgs.subscription_id IS NULL OR fgs.group_id = :currentGroupId)
|
||||||
|
AND s.name LIKE '%' || :filter || '%'
|
||||||
|
|
||||||
|
ORDER BY name COLLATE NOCASE ASC
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
abstract fun getSubscriptionsOnlyUngroupedFiltered(
|
||||||
|
currentGroupId: Long,
|
||||||
|
filter: String
|
||||||
|
): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
abstract fun getSubscriptionFlowable(serviceId: Int, url: String): Flowable<List<SubscriptionEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
abstract fun getSubscription(serviceId: Int, url: String): Maybe<SubscriptionEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM subscriptions WHERE uid = :subscriptionId")
|
||||||
|
abstract fun getSubscription(subscriptionId: Long): SubscriptionEntity
|
||||||
|
|
||||||
|
@Query("DELETE FROM subscriptions")
|
||||||
|
abstract override fun deleteAll(): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
abstract fun deleteSubscription(serviceId: Int, url: String): Int
|
||||||
|
|
||||||
|
@Query("SELECT uid FROM subscriptions WHERE url LIKE :url AND service_id = :serviceId")
|
||||||
|
internal abstract fun getSubscriptionIdInternal(serviceId: Int, url: String): Long?
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
internal abstract fun silentInsertAllInternal(entities: List<SubscriptionEntity>): List<Long>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
open fun upsertAll(entities: List<SubscriptionEntity>): List<SubscriptionEntity> {
|
||||||
|
val insertUidList = silentInsertAllInternal(entities)
|
||||||
|
|
||||||
|
insertUidList.forEachIndexed { index: Int, uidFromInsert: Long ->
|
||||||
|
val entity = entities[index]
|
||||||
|
|
||||||
|
if (uidFromInsert != -1L) {
|
||||||
|
entity.uid = uidFromInsert
|
||||||
|
} else {
|
||||||
|
val subscriptionIdFromDb = getSubscriptionIdInternal(entity.serviceId, entity.url!!)
|
||||||
|
?: error("Subscription cannot be null just after insertion.")
|
||||||
|
entity.uid = subscriptionIdFromDb
|
||||||
|
|
||||||
|
update(entity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2024 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-FileCopyrightText: 2025 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.database.subscription
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.Ignore
|
||||||
|
import androidx.room.Index
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||||
|
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||||
|
import org.schabi.newpipe.util.NO_SERVICE_ID
|
||||||
|
import org.schabi.newpipe.util.image.ImageStrategy
|
||||||
|
|
||||||
|
@Entity(
|
||||||
|
tableName = SubscriptionEntity.Companion.SUBSCRIPTION_TABLE,
|
||||||
|
indices = [
|
||||||
|
Index(
|
||||||
|
value = [SubscriptionEntity.Companion.SUBSCRIPTION_SERVICE_ID, SubscriptionEntity.Companion.SUBSCRIPTION_URL],
|
||||||
|
unique = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
data class SubscriptionEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
var uid: Long = 0,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_SERVICE_ID)
|
||||||
|
var serviceId: Int = NO_SERVICE_ID,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_URL)
|
||||||
|
var url: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_NAME)
|
||||||
|
var name: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_AVATAR_URL)
|
||||||
|
var avatarUrl: String? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_SUBSCRIBER_COUNT)
|
||||||
|
var subscriberCount: Long? = null,
|
||||||
|
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_DESCRIPTION)
|
||||||
|
var description: String? = null,
|
||||||
|
|
||||||
|
@get:NotificationMode
|
||||||
|
@ColumnInfo(name = SUBSCRIPTION_NOTIFICATION_MODE)
|
||||||
|
var notificationMode: Int = 0
|
||||||
|
) {
|
||||||
|
@Ignore
|
||||||
|
fun toChannelInfoItem(): ChannelInfoItem {
|
||||||
|
return ChannelInfoItem(this.serviceId, this.url, this.name).apply {
|
||||||
|
thumbnails = ImageStrategy.dbUrlToImageList(this@SubscriptionEntity.avatarUrl)
|
||||||
|
subscriberCount = this@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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
package org.schabi.newpipe.download;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.ActivityDownloaderBinding;
|
||||||
|
import org.schabi.newpipe.util.DeviceUtils;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.views.FocusOverlayView;
|
||||||
|
|
||||||
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
|
import us.shandian.giga.ui.fragment.MissionsFragment;
|
||||||
|
|
||||||
|
public class DownloadActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
private static final String MISSIONS_FRAGMENT_TAG = "fragment_tag";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
// Service
|
||||||
|
final Intent i = new Intent();
|
||||||
|
i.setClass(this, DownloadManagerService.class);
|
||||||
|
startService(i);
|
||||||
|
|
||||||
|
ThemeHelper.setTheme(this);
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
final ActivityDownloaderBinding downloaderBinding =
|
||||||
|
ActivityDownloaderBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(downloaderBinding.getRoot());
|
||||||
|
|
||||||
|
setSupportActionBar(downloaderBinding.toolbarLayout.toolbar);
|
||||||
|
|
||||||
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||||
|
actionBar.setTitle(R.string.downloads_title);
|
||||||
|
actionBar.setDisplayShowTitleEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getWindow().getDecorView().getViewTreeObserver()
|
||||||
|
.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||||
|
@Override
|
||||||
|
public void onGlobalLayout() {
|
||||||
|
updateFragments();
|
||||||
|
getWindow().getDecorView().getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (DeviceUtils.isTv(this)) {
|
||||||
|
FocusOverlayView.setupFocusObserver(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFragments() {
|
||||||
|
final MissionsFragment fragment = new MissionsFragment();
|
||||||
|
|
||||||
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.frame, fragment, MISSIONS_FRAGMENT_TAG)
|
||||||
|
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
|
||||||
|
.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
super.onCreateOptionsMenu(menu);
|
||||||
|
final MenuInflater inflater = getMenuInflater();
|
||||||
|
|
||||||
|
inflater.inflate(R.menu.download_menu, menu);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case android.R.id.home:
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1127
app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
Normal file
1127
app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,87 @@
|
||||||
|
package org.schabi.newpipe.download;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.DownloadLoadingDialogBinding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains a dialog which shows a loading indicator and has a customizable title.
|
||||||
|
*/
|
||||||
|
public class LoadingDialog extends DialogFragment {
|
||||||
|
private static final String TAG = "LoadingDialog";
|
||||||
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
private DownloadLoadingDialogBinding dialogLoadingBinding;
|
||||||
|
private final @StringRes int title;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new LoadingDialog.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The dialog contains a loading indicator and has a customizable title.
|
||||||
|
* <br/>
|
||||||
|
* Use {@code show()} to display the dialog to the user.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param title an informative title shown in the dialog's toolbar
|
||||||
|
*/
|
||||||
|
public LoadingDialog(final @StringRes int title) {
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreate() called with: "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
this.setCancelable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(
|
||||||
|
@NonNull final LayoutInflater inflater,
|
||||||
|
final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "onCreateView() called with: "
|
||||||
|
+ "inflater = [" + inflater + "], container = [" + container + "], "
|
||||||
|
+ "savedInstanceState = [" + savedInstanceState + "]");
|
||||||
|
}
|
||||||
|
return inflater.inflate(R.layout.download_loading_dialog, container);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
dialogLoadingBinding = DownloadLoadingDialogBinding.bind(view);
|
||||||
|
initToolbar(dialogLoadingBinding.toolbarLayout.toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initToolbar(final Toolbar toolbar) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG, "initToolbar() called with: toolbar = [" + toolbar + "]");
|
||||||
|
}
|
||||||
|
toolbar.setTitle(requireContext().getString(title));
|
||||||
|
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
dialogLoadingBinding = null;
|
||||||
|
super.onDestroyView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
package org.schabi.newpipe.error;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.acra.ReportField;
|
||||||
|
import org.acra.data.CrashReportData;
|
||||||
|
import org.acra.sender.ReportSender;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 13.09.16.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
|
* AcraReportSender.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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class AcraReportSender implements ReportSender {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(@NonNull final Context context, @NonNull final CrashReportData report) {
|
||||||
|
ErrorUtil.openActivity(context, new ErrorInfo(
|
||||||
|
new String[]{report.getString(ReportField.STACK_TRACE)},
|
||||||
|
UserAction.UI_ERROR,
|
||||||
|
"ACRA report",
|
||||||
|
null,
|
||||||
|
R.string.app_ui_crash));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package org.schabi.newpipe.error;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.auto.service.AutoService;
|
||||||
|
|
||||||
|
import org.acra.config.CoreConfiguration;
|
||||||
|
import org.acra.sender.ReportSender;
|
||||||
|
import org.acra.sender.ReportSenderFactory;
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by Christian Schabesberger on 13.09.16.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
|
* AcraReportSenderFactory.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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used by ACRA in {@link App}.initAcra() as the factory for report senders.
|
||||||
|
*/
|
||||||
|
@AutoService(ReportSenderFactory.class)
|
||||||
|
public class AcraReportSenderFactory implements ReportSenderFactory {
|
||||||
|
@NonNull
|
||||||
|
public ReportSender create(@NonNull final Context context,
|
||||||
|
@NonNull final CoreConfiguration config) {
|
||||||
|
return new AcraReportSender();
|
||||||
|
}
|
||||||
|
}
|
||||||
282
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
282
app/src/main/java/org/schabi/newpipe/error/ErrorActivity.kt
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.error
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.content.IntentCompat
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.grack.nanojson.JsonWriter
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import org.schabi.newpipe.BuildConfig
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.databinding.ActivityErrorBinding
|
||||||
|
import org.schabi.newpipe.util.Localization
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
import org.schabi.newpipe.util.text.setTextWithLinks
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This activity is used to show error details and allow reporting them in various ways.
|
||||||
|
* Use [ErrorUtil.openActivity] to correctly open this activity.
|
||||||
|
*/
|
||||||
|
class ErrorActivity : AppCompatActivity() {
|
||||||
|
private lateinit var errorInfo: ErrorInfo
|
||||||
|
private lateinit var currentTimeStamp: String
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityErrorBinding
|
||||||
|
|
||||||
|
private val contentCountryString: String
|
||||||
|
get() = Localization.getPreferredContentCountry(this).countryCode
|
||||||
|
|
||||||
|
private val contentLanguageString: String
|
||||||
|
get() = Localization.getPreferredLocalization(this).localizationCode
|
||||||
|
|
||||||
|
private val appLanguage: String
|
||||||
|
get() = Localization.getAppLocale().toString()
|
||||||
|
|
||||||
|
private val osString: String
|
||||||
|
get() {
|
||||||
|
val name = System.getProperty("os.name")!!
|
||||||
|
val osBase = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
Build.VERSION.BASE_OS.ifEmpty { "Android" }
|
||||||
|
} else {
|
||||||
|
"Android"
|
||||||
|
}
|
||||||
|
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val errorEmailSubject: String
|
||||||
|
get() = "$ERROR_EMAIL_SUBJECT ${getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
|
// /////////////////////////////////////////////////////////////////////
|
||||||
|
// Activity lifecycle
|
||||||
|
// /////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
ThemeHelper.setDayNightMode(this)
|
||||||
|
ThemeHelper.setTheme(this)
|
||||||
|
|
||||||
|
binding = ActivityErrorBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.getRoot())
|
||||||
|
|
||||||
|
setSupportActionBar(binding.toolbarLayout.toolbar)
|
||||||
|
supportActionBar?.apply {
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setTitle(R.string.error_report_title)
|
||||||
|
setDisplayShowTitleEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
errorInfo = IntentCompat.getParcelableExtra(intent, ERROR_INFO, ErrorInfo::class.java)!!
|
||||||
|
|
||||||
|
// important add guru meditation
|
||||||
|
addGuruMeditation()
|
||||||
|
// print current time, as zoned ISO8601 timestamp
|
||||||
|
currentTimeStamp = ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
|
||||||
|
|
||||||
|
binding.errorReportEmailButton.setOnClickListener { _ ->
|
||||||
|
openPrivacyPolicyDialog(this, "EMAIL")
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorReportCopyButton.setOnClickListener { _ ->
|
||||||
|
ShareUtils.copyToClipboard(this, buildMarkdown())
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.errorReportGitHubButton.setOnClickListener { _ ->
|
||||||
|
openPrivacyPolicyDialog(this, "GITHUB")
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal bugreport
|
||||||
|
buildInfo(errorInfo)
|
||||||
|
binding.errorMessageView.setTextWithLinks(errorInfo.getMessage(this))
|
||||||
|
binding.errorView.text = formErrorText(errorInfo.stackTraces)
|
||||||
|
|
||||||
|
// print stack trace once again for debugging:
|
||||||
|
errorInfo.stackTraces.forEach { Log.e(TAG, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.error_menu, menu)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
return when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.menu_item_share_error -> {
|
||||||
|
ShareUtils.shareText(
|
||||||
|
applicationContext,
|
||||||
|
getString(R.string.error_report_title),
|
||||||
|
buildJson()
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPrivacyPolicyDialog(context: Context, action: String) {
|
||||||
|
AlertDialog.Builder(context)
|
||||||
|
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setTitle(R.string.privacy_policy_title)
|
||||||
|
.setMessage(R.string.start_accept_privacy_policy)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setNeutralButton(R.string.read_privacy_policy) { _, _ ->
|
||||||
|
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
|
||||||
|
}
|
||||||
|
.setPositiveButton(R.string.accept) { _, _ ->
|
||||||
|
if (action == "EMAIL") { // send on email
|
||||||
|
val intent = Intent(Intent.ACTION_SENDTO)
|
||||||
|
.setData("mailto:".toUri()) // only email apps should handle this
|
||||||
|
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
|
||||||
|
.putExtra(Intent.EXTRA_SUBJECT, errorEmailSubject)
|
||||||
|
.putExtra(Intent.EXTRA_TEXT, buildJson())
|
||||||
|
ShareUtils.openIntentInApp(context, intent)
|
||||||
|
} else if (action == "GITHUB") { // open the NewPipe issue page on GitHub
|
||||||
|
ShareUtils.openUrlInApp(this, ERROR_GITHUB_ISSUE_URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.decline, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formErrorText(stacktrace: Array<String>): String {
|
||||||
|
val separator = "-------------------------------------"
|
||||||
|
return stacktrace.joinToString(separator + "\n", separator + "\n", separator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildInfo(info: ErrorInfo) {
|
||||||
|
binding.errorInfoLabelsView.text = getString(R.string.info_labels)
|
||||||
|
|
||||||
|
val text = info.userAction.message + "\n" +
|
||||||
|
info.request + "\n" +
|
||||||
|
contentLanguageString + "\n" +
|
||||||
|
contentCountryString + "\n" +
|
||||||
|
appLanguage + "\n" +
|
||||||
|
info.getServiceName() + "\n" +
|
||||||
|
currentTimeStamp + "\n" +
|
||||||
|
packageName + "\n" +
|
||||||
|
BuildConfig.VERSION_NAME + "\n" +
|
||||||
|
osString
|
||||||
|
|
||||||
|
binding.errorInfosView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildJson(): String {
|
||||||
|
try {
|
||||||
|
return JsonWriter.string()
|
||||||
|
.`object`()
|
||||||
|
.value("user_action", errorInfo.userAction.message)
|
||||||
|
.value("request", errorInfo.request)
|
||||||
|
.value("content_language", contentLanguageString)
|
||||||
|
.value("content_country", contentCountryString)
|
||||||
|
.value("app_language", appLanguage)
|
||||||
|
.value("service", errorInfo.getServiceName())
|
||||||
|
.value("package", packageName)
|
||||||
|
.value("version", BuildConfig.VERSION_NAME)
|
||||||
|
.value("os", osString)
|
||||||
|
.value("time", currentTimeStamp)
|
||||||
|
.array("exceptions", errorInfo.stackTraces.toList())
|
||||||
|
.value("user_comment", binding.errorCommentBox.getText().toString())
|
||||||
|
.end()
|
||||||
|
.done()
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Error while erroring: Could not build json", exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMarkdown(): String {
|
||||||
|
try {
|
||||||
|
return buildString(1024) {
|
||||||
|
val userComment = binding.errorCommentBox.text.toString()
|
||||||
|
if (userComment.isNotEmpty()) {
|
||||||
|
appendLine(userComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic error info
|
||||||
|
appendLine("## Exception")
|
||||||
|
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
|
||||||
|
appendLine("* __Request:__ ${errorInfo.request}")
|
||||||
|
appendLine("* __Content Country:__ $contentCountryString")
|
||||||
|
appendLine("* __Content Language:__ $contentLanguageString")
|
||||||
|
appendLine("* __App Language:__ $appLanguage")
|
||||||
|
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
|
||||||
|
appendLine("* __Timestamp:__ $currentTimeStamp")
|
||||||
|
appendLine("* __Package:__ $packageName")
|
||||||
|
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
|
||||||
|
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
|
||||||
|
appendLine("* __OS:__ $osString")
|
||||||
|
|
||||||
|
// Collapse all logs to a single paragraph when there are more than one
|
||||||
|
// to keep the GitHub issue clean.
|
||||||
|
if (errorInfo.stackTraces.size > 1) {
|
||||||
|
append("<details><summary><b>Exceptions (")
|
||||||
|
append(errorInfo.stackTraces.size)
|
||||||
|
append(")</b></summary><p>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the logs
|
||||||
|
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
|
||||||
|
append("<details><summary><b>Crash log ")
|
||||||
|
if (errorInfo.stackTraces.size > 1) {
|
||||||
|
append(index + 1)
|
||||||
|
}
|
||||||
|
append("</b>")
|
||||||
|
append("</summary><p>\n")
|
||||||
|
append("\n```\n${stacktrace}\n```\n")
|
||||||
|
append("</details>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure to close everything
|
||||||
|
if (errorInfo.stackTraces.size > 1) {
|
||||||
|
append("</p></details>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
append("<hr>\n")
|
||||||
|
}
|
||||||
|
} catch (exception: Exception) {
|
||||||
|
Log.e(TAG, "Error while erroring: Could not build markdown", exception)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addGuruMeditation() {
|
||||||
|
// just an easter egg
|
||||||
|
var text = binding.errorSorryView.text.toString()
|
||||||
|
text += "\n" + getString(R.string.guru_meditation)
|
||||||
|
binding.errorSorryView.text = text
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// LOG TAGS
|
||||||
|
private val TAG = ErrorActivity::class.java.toString()
|
||||||
|
|
||||||
|
// BUNDLE TAGS
|
||||||
|
const val ERROR_INFO = "error_info"
|
||||||
|
|
||||||
|
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
|
||||||
|
private const val ERROR_EMAIL_SUBJECT = "Exception in "
|
||||||
|
|
||||||
|
private const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
|
||||||
|
}
|
||||||
|
}
|
||||||
364
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Normal file
364
app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
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 com.google.android.exoplayer2.upstream.HttpDataSource
|
||||||
|
import com.google.android.exoplayer2.upstream.Loader
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
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.player.mediasource.FailedMediaSource
|
||||||
|
import org.schabi.newpipe.player.resolver.PlaybackResolver
|
||||||
|
import org.schabi.newpipe.util.text.getText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 private constructor(
|
||||||
|
val stackTraces: Array<String>,
|
||||||
|
val userAction: UserAction,
|
||||||
|
val request: String,
|
||||||
|
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 {
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
throwable: Throwable,
|
||||||
|
userAction: UserAction,
|
||||||
|
request: String,
|
||||||
|
serviceId: Int? = null,
|
||||||
|
openInBrowserUrl: String? = null
|
||||||
|
) : this(
|
||||||
|
throwableToStringList(throwable),
|
||||||
|
userAction,
|
||||||
|
request,
|
||||||
|
serviceId,
|
||||||
|
getMessage(throwable, userAction, serviceId),
|
||||||
|
isReportable(throwable),
|
||||||
|
isRetryable(throwable),
|
||||||
|
(throwable as? ReCaptchaException)?.url,
|
||||||
|
openInBrowserUrl
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(
|
||||||
|
throwables: List<Throwable>,
|
||||||
|
userAction: UserAction,
|
||||||
|
request: String,
|
||||||
|
serviceId: Int? = null,
|
||||||
|
openInBrowserUrl: String? = null
|
||||||
|
) : this(
|
||||||
|
throwableListToStringList(throwables),
|
||||||
|
userAction,
|
||||||
|
request,
|
||||||
|
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<String>,
|
||||||
|
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<Throwable>,
|
||||||
|
userAction: UserAction,
|
||||||
|
request: String,
|
||||||
|
info: Info?
|
||||||
|
) :
|
||||||
|
this(throwables, userAction, request, info?.serviceId, info?.url)
|
||||||
|
|
||||||
|
fun getServiceName(): String {
|
||||||
|
return getServiceName(serviceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getMessage(context: Context): CharSequence {
|
||||||
|
return message.getText(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Parcelize
|
||||||
|
class ErrorMessage(
|
||||||
|
@StringRes
|
||||||
|
private val stringRes: Int,
|
||||||
|
private vararg val formatArgs: String
|
||||||
|
) : Parcelable {
|
||||||
|
fun getText(context: Context): CharSequence {
|
||||||
|
// Ensure locale aware context via ContextCompat.getContextForLanguage() (just in case context is not AppCompatActivity)
|
||||||
|
val ctx = ContextCompat.getContextForLanguage(context)
|
||||||
|
return if (formatArgs.isEmpty()) {
|
||||||
|
ctx.getText(stringRes)
|
||||||
|
} else {
|
||||||
|
// ContextCompat.getString() with formatArgs does not exist, so we just
|
||||||
|
// replicate its source code but with formatArgs
|
||||||
|
ctx.resources.getText(stringRes, *formatArgs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val SERVICE_NONE = "<unknown_service>"
|
||||||
|
|
||||||
|
const val YOUTUBE_IP_BAN_FAQ_URL = "https://newpipe.net/FAQ/#ip-banned-youtube"
|
||||||
|
|
||||||
|
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<Throwable>) = throwableList.map { it.stackTraceToString() }.toTypedArray()
|
||||||
|
|
||||||
|
fun getMessage(
|
||||||
|
throwable: Throwable?,
|
||||||
|
action: UserAction?,
|
||||||
|
serviceId: Int?
|
||||||
|
): ErrorMessage {
|
||||||
|
return when {
|
||||||
|
// player exceptions
|
||||||
|
// some may be IOException, so do these checks before isNetworkRelated!
|
||||||
|
throwable is ExoPlaybackException -> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
YOUTUBE_IP_BAN_FAQ_URL
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// if 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 -> !isContentSurelyNotAvailable(throwable)
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// if we know the content is surely not available, retrying won't help
|
||||||
|
is ContentNotAvailableException -> !isContentSurelyNotAvailable(throwable)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfortunately sometimes [ContentNotAvailableException] may not indicate that the content
|
||||||
|
* is blocked/deleted/paid, but may just indicate that we could not extract it. This is an
|
||||||
|
* inconsistency in the exceptions thrown by the extractor, but until it is fixed, this
|
||||||
|
* function will distinguish between the two types.
|
||||||
|
* @return `true` if the content is not available because of a limitation imposed by the
|
||||||
|
* service or the owner, `false` if the extractor could not extract info about it
|
||||||
|
*/
|
||||||
|
fun isContentSurelyNotAvailable(e: ContentNotAvailableException): Boolean {
|
||||||
|
return when (e) {
|
||||||
|
is AccountTerminatedException,
|
||||||
|
is AgeRestrictedContentException,
|
||||||
|
is GeographicRestrictionException,
|
||||||
|
is PaidContentException,
|
||||||
|
is PrivateContentException,
|
||||||
|
is SoundCloudGoPlusContentException,
|
||||||
|
is UnsupportedContentInCountryException,
|
||||||
|
is YoutubeMusicPremiumContentException -> true
|
||||||
|
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
141
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Normal file
141
app/src/main/java/org/schabi/newpipe/error/ErrorPanelHelper.kt
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
package org.schabi.newpipe.error
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.jakewharton.rxbinding4.view.clicks
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
import org.schabi.newpipe.ktx.animate
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils
|
||||||
|
import org.schabi.newpipe.util.text.setTextWithLinks
|
||||||
|
|
||||||
|
class ErrorPanelHelper(
|
||||||
|
private val fragment: Fragment,
|
||||||
|
rootView: View,
|
||||||
|
onRetry: Runnable?
|
||||||
|
) {
|
||||||
|
private val context: Context = rootView.context!!
|
||||||
|
|
||||||
|
private val errorPanelRoot: View = rootView.findViewById(R.id.error_panel)
|
||||||
|
|
||||||
|
// the only element that is visible by default
|
||||||
|
private val errorTextView: TextView =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_message_view)
|
||||||
|
private val errorServiceInfoTextView: TextView =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_message_service_info_view)
|
||||||
|
private val errorServiceExplanationTextView: TextView =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_message_service_explanation_view)
|
||||||
|
private val errorActionButton: Button =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_action_button)
|
||||||
|
private val errorRetryButton: Button =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_retry_button)
|
||||||
|
private val errorOpenInBrowserButton: Button =
|
||||||
|
errorPanelRoot.findViewById(R.id.error_open_in_browser)
|
||||||
|
|
||||||
|
private var errorDisposable: Disposable? = null
|
||||||
|
private var retryShouldBeShown: Boolean = (onRetry != null)
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (onRetry != null) {
|
||||||
|
errorDisposable = errorRetryButton.clicks()
|
||||||
|
.debounce(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { onRetry.run() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureDefaultVisibility() {
|
||||||
|
errorTextView.isVisible = true
|
||||||
|
|
||||||
|
errorServiceInfoTextView.isVisible = false
|
||||||
|
errorServiceExplanationTextView.isVisible = false
|
||||||
|
errorActionButton.isVisible = false
|
||||||
|
errorRetryButton.isVisible = false
|
||||||
|
errorOpenInBrowserButton.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showError(errorInfo: ErrorInfo) {
|
||||||
|
ensureDefaultVisibility()
|
||||||
|
errorTextView.setTextWithLinks(errorInfo.getMessage(context))
|
||||||
|
|
||||||
|
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.recaptchaUrl)
|
||||||
|
fragment.startActivityForResult(intent, ReCaptchaActivity.RECAPTCHA_REQUEST)
|
||||||
|
errorActionButton.setOnClickListener(null)
|
||||||
|
}
|
||||||
|
} else if (errorInfo.isReportable) {
|
||||||
|
showAndSetErrorButtonAction(R.string.error_snackbar_action) {
|
||||||
|
ErrorUtil.openActivity(context, errorInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorInfo.isRetryable) {
|
||||||
|
errorRetryButton.isVisible = retryShouldBeShown
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorInfo.openInBrowserUrl != null) {
|
||||||
|
errorOpenInBrowserButton.isVisible = true
|
||||||
|
errorOpenInBrowserButton.setOnClickListener {
|
||||||
|
ShareUtils.openUrlInBrowser(context, errorInfo.openInBrowserUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRootVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the errorButtonAction, sets a text into it and sets the click listener.
|
||||||
|
*/
|
||||||
|
private fun showAndSetErrorButtonAction(
|
||||||
|
@StringRes resid: Int,
|
||||||
|
listener: View.OnClickListener
|
||||||
|
) {
|
||||||
|
errorActionButton.isVisible = true
|
||||||
|
errorActionButton.setText(resid)
|
||||||
|
errorActionButton.setOnClickListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showTextError(errorString: String) {
|
||||||
|
ensureDefaultVisibility()
|
||||||
|
|
||||||
|
errorTextView.setTextWithLinks(errorString)
|
||||||
|
|
||||||
|
setRootVisible()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setRootVisible() {
|
||||||
|
errorPanelRoot.animate(true, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hide() {
|
||||||
|
errorActionButton.setOnClickListener(null)
|
||||||
|
errorPanelRoot.animate(false, 150)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isVisible(): Boolean {
|
||||||
|
return errorPanelRoot.isVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
errorActionButton.setOnClickListener(null)
|
||||||
|
errorRetryButton.setOnClickListener(null)
|
||||||
|
errorDisposable?.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG: String = ErrorPanelHelper::class.simpleName!!
|
||||||
|
val DEBUG: Boolean = MainActivity.DEBUG
|
||||||
|
}
|
||||||
|
}
|
||||||
170
app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
Normal file
170
app/src/main/java/org/schabi/newpipe/error/ErrorUtil.kt
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
package org.schabi.newpipe.error
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.view.View
|
||||||
|
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
|
||||||
|
import org.schabi.newpipe.MainActivity
|
||||||
|
import org.schabi.newpipe.R
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class contains all of the methods that should be used to let the user know that an error has
|
||||||
|
* occurred in the least intrusive way possible for each case. This class is for unexpected errors,
|
||||||
|
* for handled errors (e.g. network errors) use e.g. [ErrorPanelHelper] instead.
|
||||||
|
* - Use a snackbar if the exception is not critical and it happens in a place where a root view
|
||||||
|
* is available.
|
||||||
|
* - Use a notification if the exception happens inside a background service (player, subscription
|
||||||
|
* import, ...) or there is no activity/fragment from which to extract a root view.
|
||||||
|
* - Finally use the error activity only as a last resort in case the exception is critical and
|
||||||
|
* happens in an open activity (since the workflow would be interrupted anyway in that case).
|
||||||
|
*/
|
||||||
|
class ErrorUtil {
|
||||||
|
companion object {
|
||||||
|
private const val ERROR_REPORT_NOTIFICATION_ID = 5340681
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a new error activity allowing the user to report the provided error. Only use this
|
||||||
|
* method directly as a last resort in case the exception is critical and happens in an open
|
||||||
|
* activity (since the workflow would be interrupted anyway in that case). So never use this
|
||||||
|
* for background services.
|
||||||
|
*
|
||||||
|
* If the crashed occurred while the app was in the background open a notification instead
|
||||||
|
*
|
||||||
|
* @param context the context to use to start the new activity
|
||||||
|
* @param errorInfo the error info to be reported
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun openActivity(context: Context, errorInfo: ErrorInfo) {
|
||||||
|
if (PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(MainActivity.KEY_IS_IN_BACKGROUND, true)
|
||||||
|
) {
|
||||||
|
createNotification(context, errorInfo)
|
||||||
|
} else {
|
||||||
|
context.startActivity(getErrorActivityIntent(context, errorInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a bottom snackbar to the user, with a report button that opens the error activity.
|
||||||
|
* Use this method if the exception is not critical and it happens in a place where a root
|
||||||
|
* view is available.
|
||||||
|
*
|
||||||
|
* @param context will be used to obtain the root view if it is an [Activity]; if no root
|
||||||
|
* view can be found an error notification is shown instead
|
||||||
|
* @param errorInfo the error info to be reported
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun showSnackbar(context: Context, errorInfo: ErrorInfo) {
|
||||||
|
val rootView = (context as? Activity)?.findViewById<View>(android.R.id.content)
|
||||||
|
showSnackbar(context, rootView, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a bottom snackbar to the user, with a report button that opens the error activity.
|
||||||
|
* Use this method if the exception is not critical and it happens in a place where a root
|
||||||
|
* view is available.
|
||||||
|
*
|
||||||
|
* @param fragment will be used to obtain the root view if it has a connected [Activity]; if
|
||||||
|
* no root view can be found an error notification is shown instead
|
||||||
|
* @param errorInfo the error info to be reported
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun showSnackbar(fragment: Fragment, errorInfo: ErrorInfo) {
|
||||||
|
var rootView = fragment.view
|
||||||
|
if (rootView == null && fragment.activity != null) {
|
||||||
|
rootView = fragment.requireActivity().findViewById(android.R.id.content)
|
||||||
|
}
|
||||||
|
showSnackbar(fragment.requireContext(), rootView, errorInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun showUiErrorSnackbar(context: Context, request: String, throwable: Throwable) {
|
||||||
|
showSnackbar(context, ErrorInfo(throwable, UserAction.UI_ERROR, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut to calling [showSnackbar] with an [ErrorInfo] of type [UserAction.UI_ERROR]
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun showUiErrorSnackbar(fragment: Fragment, request: String, throwable: Throwable) {
|
||||||
|
showSnackbar(fragment, ErrorInfo(throwable, UserAction.UI_ERROR, request))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an error notification. Tapping on the notification opens the error activity. Use
|
||||||
|
* this method if the exception happens inside a background service (player, subscription
|
||||||
|
* import, ...) or there is no activity/fragment from which to extract a root view.
|
||||||
|
*
|
||||||
|
* @param context the context to use to show the notification
|
||||||
|
* @param errorInfo the error info to be reported; the error message
|
||||||
|
* [ErrorInfo.messageStringId] will be shown in the notification
|
||||||
|
* description
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun createNotification(context: Context, errorInfo: ErrorInfo) {
|
||||||
|
val notificationBuilder: NotificationCompat.Builder =
|
||||||
|
NotificationCompat.Builder(
|
||||||
|
context,
|
||||||
|
context.getString(R.string.error_report_channel_id)
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.ic_bug_report)
|
||||||
|
.setContentTitle(context.getString(R.string.error_report_notification_title))
|
||||||
|
.setContentText(errorInfo.getMessage(context))
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(
|
||||||
|
PendingIntentCompat.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
getErrorActivityIntent(context, errorInfo),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager
|
||||||
|
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
val intent = Intent(context, ErrorActivity::class.java)
|
||||||
|
intent.putExtra(ErrorActivity.ERROR_INFO, errorInfo)
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSnackbar(context: Context, rootView: View?, errorInfo: ErrorInfo) {
|
||||||
|
if (rootView == null) {
|
||||||
|
// fallback to showing a notification if no root view is available
|
||||||
|
createNotification(context, errorInfo)
|
||||||
|
} else {
|
||||||
|
Snackbar.make(rootView, errorInfo.getMessage(context), Snackbar.LENGTH_LONG)
|
||||||
|
.setActionTextColor(Color.YELLOW)
|
||||||
|
.setAction(context.getString(R.string.error_snackbar_action).uppercase()) {
|
||||||
|
context.startActivity(getErrorActivityIntent(context, errorInfo))
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
package org.schabi.newpipe.error;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.webkit.CookieManager;
|
||||||
|
import android.webkit.WebResourceRequest;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
import android.webkit.WebViewClient;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.app.NavUtils;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.DownloaderImpl;
|
||||||
|
import org.schabi.newpipe.MainActivity;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.ActivityRecaptchaBinding;
|
||||||
|
import org.schabi.newpipe.extractor.utils.Utils;
|
||||||
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Created by beneth <bmauduit@beneth.fr> on 06.12.16.
|
||||||
|
*
|
||||||
|
* Copyright (C) Christian Schabesberger 2015 <chris.schabesberger@mailbox.org>
|
||||||
|
* ReCaptchaActivity.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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
public class ReCaptchaActivity extends AppCompatActivity {
|
||||||
|
public static final int RECAPTCHA_REQUEST = 10;
|
||||||
|
public static final String RECAPTCHA_URL_EXTRA = "recaptcha_url_extra";
|
||||||
|
public static final String TAG = ReCaptchaActivity.class.toString();
|
||||||
|
public static final String YT_URL = "https://www.youtube.com";
|
||||||
|
public static final String RECAPTCHA_COOKIES_KEY = "recaptcha_cookies";
|
||||||
|
|
||||||
|
public static String sanitizeRecaptchaUrl(@Nullable final String url) {
|
||||||
|
if (url == null || url.trim().isEmpty()) {
|
||||||
|
return YT_URL; // YouTube is the most likely service to have thrown a recaptcha
|
||||||
|
} else {
|
||||||
|
// remove "pbj=1" parameter from YouYube urls, as it makes the page JSON and not HTML
|
||||||
|
return url.replace("&pbj=1", "").replace("pbj=1&", "").replace("?pbj=1", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActivityRecaptchaBinding recaptchaBinding;
|
||||||
|
private String foundCookies = "";
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
ThemeHelper.setTheme(this);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
recaptchaBinding = ActivityRecaptchaBinding.inflate(getLayoutInflater());
|
||||||
|
setContentView(recaptchaBinding.getRoot());
|
||||||
|
setSupportActionBar(recaptchaBinding.toolbar);
|
||||||
|
|
||||||
|
final String url = sanitizeRecaptchaUrl(getIntent().getStringExtra(RECAPTCHA_URL_EXTRA));
|
||||||
|
// set return to Cancel by default
|
||||||
|
setResult(RESULT_CANCELED);
|
||||||
|
|
||||||
|
// enable Javascript
|
||||||
|
final WebSettings webSettings = recaptchaBinding.reCaptchaWebView.getSettings();
|
||||||
|
webSettings.setJavaScriptEnabled(true);
|
||||||
|
webSettings.setUserAgentString(DownloaderImpl.USER_AGENT);
|
||||||
|
|
||||||
|
recaptchaBinding.reCaptchaWebView.setWebViewClient(new WebViewClient() {
|
||||||
|
@Override
|
||||||
|
public boolean shouldOverrideUrlLoading(final WebView view,
|
||||||
|
final WebResourceRequest request) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "shouldOverrideUrlLoading: url=" + request.getUrl().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCookiesFromUrl(request.getUrl().toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPageFinished(final WebView view, final String url) {
|
||||||
|
super.onPageFinished(view, url);
|
||||||
|
handleCookiesFromUrl(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// cleaning cache, history and cookies from webView
|
||||||
|
recaptchaBinding.reCaptchaWebView.clearCache(true);
|
||||||
|
recaptchaBinding.reCaptchaWebView.clearHistory();
|
||||||
|
CookieManager.getInstance().removeAllCookies(null);
|
||||||
|
|
||||||
|
recaptchaBinding.reCaptchaWebView.loadUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.menu_recaptcha, menu);
|
||||||
|
|
||||||
|
final ActionBar actionBar = getSupportActionBar();
|
||||||
|
if (actionBar != null) {
|
||||||
|
actionBar.setDisplayHomeAsUpEnabled(false);
|
||||||
|
actionBar.setTitle(R.string.title_activity_recaptcha);
|
||||||
|
actionBar.setSubtitle(R.string.subtitle_activity_recaptcha);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressLint("MissingSuperCall") // saveCookiesAndFinish method handles back navigation
|
||||||
|
public void onBackPressed() {
|
||||||
|
saveCookiesAndFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
if (item.getItemId() == R.id.menu_item_done) {
|
||||||
|
saveCookiesAndFinish();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCookiesAndFinish() {
|
||||||
|
// try to get cookies of unclosed page
|
||||||
|
handleCookiesFromUrl(recaptchaBinding.reCaptchaWebView.getUrl());
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "saveCookiesAndFinish: foundCookies=" + foundCookies);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundCookies.isEmpty()) {
|
||||||
|
// save cookies to preferences
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
|
||||||
|
getApplicationContext());
|
||||||
|
final String key = getApplicationContext().getString(R.string.recaptcha_cookies_key);
|
||||||
|
prefs.edit().putString(key, foundCookies).apply();
|
||||||
|
|
||||||
|
// give cookies to Downloader class
|
||||||
|
DownloaderImpl.getInstance().setCookie(RECAPTCHA_COOKIES_KEY, foundCookies);
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to blank page (unloads youtube to prevent background playback)
|
||||||
|
recaptchaBinding.reCaptchaWebView.loadUrl("about:blank");
|
||||||
|
|
||||||
|
final Intent intent = new Intent(this, org.schabi.newpipe.MainActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
NavUtils.navigateUpTo(this, intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void handleCookiesFromUrl(@Nullable final String url) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "handleCookiesFromUrl: url=" + (url == null ? "null" : url));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String cookies = CookieManager.getInstance().getCookie(url);
|
||||||
|
handleCookies(cookies);
|
||||||
|
|
||||||
|
// sometimes cookies are inside the url
|
||||||
|
final int abuseStart = url.indexOf("google_abuse=");
|
||||||
|
if (abuseStart != -1) {
|
||||||
|
final int abuseEnd = url.indexOf("+path");
|
||||||
|
|
||||||
|
try {
|
||||||
|
handleCookies(Utils.decodeUrlUtf8(url.substring(abuseStart + 13, abuseEnd)));
|
||||||
|
} catch (final StringIndexOutOfBoundsException e) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.e(TAG, "handleCookiesFromUrl: invalid google abuse starting at "
|
||||||
|
+ abuseStart + " and ending at " + abuseEnd + " for url " + url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCookies(@Nullable final String cookies) {
|
||||||
|
if (MainActivity.DEBUG) {
|
||||||
|
Log.d(TAG, "handleCookies: cookies=" + (cookies == null ? "null" : cookies));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cookies == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addYoutubeCookies(cookies);
|
||||||
|
// add here methods to extract cookies for other services
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addYoutubeCookies(@NonNull final String cookies) {
|
||||||
|
if (cookies.contains("s_gl=") || cookies.contains("goojf=")
|
||||||
|
|| cookies.contains("VISITOR_INFO1_LIVE=")
|
||||||
|
|| cookies.contains("GOOGLE_ABUSE_EXEMPTION=")) {
|
||||||
|
// youtube seems to also need the other cookies:
|
||||||
|
addCookie(cookies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addCookie(final String cookie) {
|
||||||
|
if (foundCookies.contains(cookie)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundCookies.isEmpty() || foundCookies.endsWith("; ")) {
|
||||||
|
foundCookies += cookie;
|
||||||
|
} else if (foundCookies.endsWith(";")) {
|
||||||
|
foundCookies += " " + cookie;
|
||||||
|
} else {
|
||||||
|
foundCookies += "; " + cookie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/src/main/java/org/schabi/newpipe/error/UserAction.kt
Normal file
44
app/src/main/java/org/schabi/newpipe/error/UserAction.kt
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2017-2025 NewPipe contributors <https://newpipe.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.error
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user actions that can cause an error.
|
||||||
|
*/
|
||||||
|
enum class UserAction(val message: String) {
|
||||||
|
USER_REPORT("user report"),
|
||||||
|
UI_ERROR("ui error"),
|
||||||
|
DATABASE_IMPORT_EXPORT("database import or export"),
|
||||||
|
SUBSCRIPTION_CHANGE("subscription change"),
|
||||||
|
SUBSCRIPTION_UPDATE("subscription update"),
|
||||||
|
SUBSCRIPTION_GET("get subscription"),
|
||||||
|
SUBSCRIPTION_IMPORT_EXPORT("subscription import or export"),
|
||||||
|
LOAD_IMAGE("load image"),
|
||||||
|
SOMETHING_ELSE("something else"),
|
||||||
|
SEARCHED("searched"),
|
||||||
|
GET_SUGGESTIONS("get suggestions"),
|
||||||
|
REQUESTED_STREAM("requested stream"),
|
||||||
|
REQUESTED_CHANNEL("requested channel"),
|
||||||
|
REQUESTED_PLAYLIST("requested playlist"),
|
||||||
|
REQUESTED_KIOSK("requested kiosk"),
|
||||||
|
REQUESTED_COMMENTS("requested comments"),
|
||||||
|
REQUESTED_COMMENT_REPLIES("requested comment replies"),
|
||||||
|
REQUESTED_FEED("requested feed"),
|
||||||
|
REQUESTED_BOOKMARK("bookmark"),
|
||||||
|
DELETE_FROM_HISTORY("delete from history"),
|
||||||
|
PLAY_STREAM("play stream"),
|
||||||
|
DOWNLOAD_OPEN_DIALOG("download open dialog"),
|
||||||
|
DOWNLOAD_POSTPROCESSING("download post-processing"),
|
||||||
|
DOWNLOAD_FAILED("download failed"),
|
||||||
|
NEW_STREAMS_NOTIFICATIONS("new streams notifications"),
|
||||||
|
PREFERENCES_MIGRATION("migration of preferences"),
|
||||||
|
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"),
|
||||||
|
PLAY_ON_POPUP("play on popup"),
|
||||||
|
SUBSCRIPTIONS("loading subscriptions")
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue