diff --git a/.github/locales.py b/.github/locales.py
index 6127d9d80..a74d72588 100644
--- a/.github/locales.py
+++ b/.github/locales.py
@@ -1,13 +1,14 @@
import re
import glob
import requests
+import os
import lxml.etree as ET # builtin library doesn't preserve comments
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */"
-XML_NAME = "app/src/main/res/values-b+"
+XML_NAME = "app/src/main/res/values-"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
INDENT = " "*4
@@ -20,29 +21,29 @@ rest, after_src = rest.split(END_MARKER)
# Load already added langs
languages = {}
-for lang in re.finditer(r'Pair\("(.*)", "(.*)"\)', rest):
- name, iso = lang.groups()
- languages[iso] = name
+for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
+ flag, name, iso = lang.groups()
+ languages[iso] = (flag, name)
# Add not yet added langs
for folder in glob.glob(f"{XML_NAME}*"):
- iso = folder[len(XML_NAME):].replace("+", "-")
+ iso = folder[len(XML_NAME):]
if iso not in languages.keys():
- entry = iso_map.get(iso.lower(), {'nativeName':iso}) # fallback to iso code if not found
- languages[iso] = entry['nativeName'].split(',')[0] # first name if there are multiple
+ entry = iso_map.get(iso.lower(),{'nativeName':iso})
+ languages[iso] = ("", entry['nativeName'].split(',')[0])
-# Create pairs
-pairs = []
-for iso in sorted(languages, key=lambda iso: languages[iso].lower()): # sort by language name
- name = languages[iso]
- pairs.append(f'{INDENT}Pair("{name}", "{iso}"),')
+# Create triples
+triples = []
+for iso in sorted(languages.keys()):
+ flag, name = languages[iso]
+ triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
# Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
before_src +
START_MARKER +
"\n" +
- "\n".join(pairs) +
+ "\n".join(triples) +
"\n" +
END_MARKER +
after_src
@@ -61,5 +62,8 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
with open(file, 'wb') as fp:
fp.write(b'\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
+ # Remove trailing new line to be consistent with weblate
+ fp.seek(-1, os.SEEK_END)
+ fp.truncate()
except ET.ParseError as ex:
print(f"[{file}] {ex}")
diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml
index 30bedcc1b..e84bb08b0 100644
--- a/.github/workflows/build_to_archive.yml
+++ b/.github/workflows/build_to_archive.yml
@@ -1,93 +1,78 @@
-name: Archive build
-
-on:
- push:
- branches: [ master ]
- paths-ignore:
- - '*.md'
- - '*.json'
- - '**/wcokey.txt'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-concurrency:
- group: "Archive-build"
- cancel-in-progress: true
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Generate access token
- id: generate_token
- uses: tibdex/github-app-token@v2
- with:
- app_id: ${{ secrets.GH_APP_ID }}
- private_key: ${{ secrets.GH_APP_KEY }}
- repository: "recloudstream/secrets"
-
- - name: Generate access token (archive)
- id: generate_archive_token
- uses: tibdex/github-app-token@v2
- with:
- app_id: ${{ secrets.GH_APP_ID }}
- private_key: ${{ secrets.GH_APP_KEY }}
- repository: "recloudstream/cloudstream-archive"
-
- - uses: actions/checkout@v6
-
- - name: Set up JDK 17
- uses: actions/setup-java@v5
- with:
- distribution: temurin
- java-version: 17
-
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
-
- - name: Fetch keystore
- id: fetch_keystore
- run: |
- TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
- mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
- curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
- curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
- KEY_PWD="$(cat keystore_password.txt)"
- echo "::add-mask::${KEY_PWD}"
- echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
-
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v5
- with:
- cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
-
- - name: Run Gradle
- run: ./gradlew assemblePrereleaseRelease
- env:
- SIGNING_KEY_ALIAS: "key0"
- SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
- SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
- TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
- MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
-
- - uses: actions/checkout@v6
- with:
- repository: "recloudstream/cloudstream-archive"
- token: ${{ steps.generate_archive_token.outputs.token }}
- path: "archive"
-
- - name: Move build
- run: cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
-
- - name: Push archive
- run: |
- cd $GITHUB_WORKSPACE/archive
- git config --local user.email "actions@github.com"
- git config --local user.name "GitHub Actions"
- git add .
- git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
- git push --force
+name: Archive build
+
+on:
+ push:
+ branches: [ master ]
+ paths-ignore:
+ - '*.md'
+ - '*.json'
+ - '**/wcokey.txt'
+ workflow_dispatch:
+
+concurrency:
+ group: "Archive-build"
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate access token
+ id: generate_token
+ uses: tibdex/github-app-token@v2
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ repository: "recloudstream/secrets"
+ - name: Generate access token (archive)
+ id: generate_archive_token
+ uses: tibdex/github-app-token@v2
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ repository: "recloudstream/cloudstream-archive"
+ - uses: actions/checkout@v4
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'adopt'
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Fetch keystore
+ id: fetch_keystore
+ run: |
+ TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
+ mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
+ curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
+ curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
+ KEY_PWD="$(cat keystore_password.txt)"
+ echo "::add-mask::${KEY_PWD}"
+ echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
+ - name: Run Gradle
+ run: |
+ ./gradlew assemblePrerelease
+ env:
+ SIGNING_KEY_ALIAS: "key0"
+ SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
+ SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
+ SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
+ SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
+ - uses: actions/checkout@v4
+ with:
+ repository: "recloudstream/cloudstream-archive"
+ token: ${{ steps.generate_archive_token.outputs.token }}
+ path: "archive"
+
+ - name: Move build
+ run: |
+ cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
+
+ - name: Push archive
+ run: |
+ cd $GITHUB_WORKSPACE/archive
+ git config --local user.email "actions@github.com"
+ git config --local user.name "GitHub Actions"
+ git add .
+ git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
+ git push --force
\ No newline at end of file
diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml
index d67b8a519..ec50743ae 100644
--- a/.github/workflows/generate_dokka.yml
+++ b/.github/workflows/generate_dokka.yml
@@ -1,18 +1,19 @@
name: Dokka
+# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
+concurrency:
+ group: "dokka"
+ cancel-in-progress: true
+
on:
push:
- branches: [ master ]
+ branches:
+ # choose your default branch
+ - master
+ - main
paths-ignore:
- '*.md'
-permissions:
- contents: read
-
-concurrency:
- group: "dokka"
- cancel-in-progress: true
-
jobs:
build:
runs-on: ubuntu-latest
@@ -24,44 +25,41 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/dokka"
-
- name: Checkout
- uses: actions/checkout@v6
+ uses: actions/checkout@master
with:
path: "src"
- name: Checkout dokka
- uses: actions/checkout@v6
+ uses: actions/checkout@master
with:
repository: "recloudstream/dokka"
path: "dokka"
token: ${{ steps.generate_token.outputs.token }}
-
+
- name: Clean old builds
run: |
cd $GITHUB_WORKSPACE/dokka/
- rm -rf "./app"
- rm -rf "./library"
+ rm -rf "./-cloudstream"
- - name: Set up JDK 17
- uses: actions/setup-java@v5
+ - name: Setup JDK 17
+ uses: actions/setup-java@v4
with:
- distribution: temurin
java-version: 17
+ distribution: 'adopt'
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v5
- with:
- cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v3
- name: Generate Dokka
run: |
cd $GITHUB_WORKSPACE/src/
chmod +x gradlew
- ./gradlew docs:dokkaGeneratePublicationHtml
+ ./gradlew docs:dokkaHtml
- name: Copy Dokka
- run: cp -r $GITHUB_WORKSPACE/src/docs/build/dokka/html/* $GITHUB_WORKSPACE/dokka/
+ run: |
+ cp -r $GITHUB_WORKSPACE/src/docs/build/dokka/html/* $GITHUB_WORKSPACE/dokka/
- name: Push builds
run: |
diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml
new file mode 100644
index 000000000..88ab3656c
--- /dev/null
+++ b/.github/workflows/issue_action.yml
@@ -0,0 +1,88 @@
+name: Issue automatic actions
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ issue-moderator:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate access token
+ id: generate_token
+ uses: tibdex/github-app-token@v2
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ - name: Similarity analysis
+ id: similarity
+ uses: actions-cool/issues-similarity-analysis@v1
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ filter-threshold: 0.60
+ title-excludes: ''
+ comment-title: |
+ ### Your issue looks similar to these issues:
+ Please close if duplicate.
+ comment-body: '${index}. ${similarity} #${number}'
+ - name: Label if possible duplicate
+ if: steps.similarity.outputs.similar-issues-found =='true'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ steps.generate_token.outputs.token }}
+ script: |
+ github.rest.issues.addLabels({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: ["possible duplicate"]
+ })
+ - uses: actions/checkout@v4
+ - name: Automatically close issues that dont follow the issue template
+ uses: lucasbento/auto-close-issues@v1.0.2
+ with:
+ github-token: ${{ steps.generate_token.outputs.token }}
+ issue-close-message: |
+ @${issue.user.login}: hello! :wave:
+ This issue is being automatically closed because it does not follow the issue template."
+ closed-issues-label: "invalid"
+ - name: Check if issue mentions a provider
+ id: provider_check
+ env:
+ GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
+ run: |
+ wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
+ pip3 install httpx
+ RES="$(python3 ./check_issue.py)"
+ echo "name=${RES}" >> $GITHUB_OUTPUT
+ - name: Comment if issue mentions a provider
+ if: steps.provider_check.outputs.name != 'none'
+ uses: actions-cool/issues-helper@v3
+ with:
+ actions: 'create-comment'
+ token: ${{ steps.generate_token.outputs.token }}
+ body: |
+ Hello ${{ github.event.issue.user.login }}.
+ Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
+
+ Found provider name: `${{ steps.provider_check.outputs.name }}`
+ - name: Label if mentions provider
+ if: steps.provider_check.outputs.name != 'none'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ steps.generate_token.outputs.token }}
+ script: |
+ github.rest.issues.addLabels({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ labels: ["possible provider issue"]
+ })
+ - name: Add eyes reaction to all issues
+ uses: actions-cool/emoji-helper@v1.0.0
+ with:
+ type: 'issue'
+ token: ${{ steps.generate_token.outputs.token }}
+ emoji: 'eyes'
+
+
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index b5b17ba6a..f35cd58c5 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -8,13 +8,10 @@ on:
- '*.json'
- '**/wcokey.txt'
-concurrency:
+concurrency:
group: "pre-release"
cancel-in-progress: true
-permissions:
- contents: write
-
jobs:
build:
runs-on: ubuntu-latest
@@ -26,18 +23,14 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
-
- - uses: actions/checkout@v6
-
+ - uses: actions/checkout@v4
- name: Set up JDK 17
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v4
with:
- distribution: temurin
- java-version: 17
-
+ java-version: '17'
+ distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
-
- name: Fetch keystore
id: fetch_keystore
run: |
@@ -48,25 +41,18 @@ jobs:
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
-
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v5
- with:
- cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
-
- name: Run Gradle
- run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar
+ run: |
+ ./gradlew assemblePrerelease build androidSourcesJar
+ ./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
- TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
- MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
-
- name: Create pre-release
- uses: marvinpinto/action-automatic-releases@latest
+ uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "pre-release"
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 8f5c62866..7f6dd4123 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -2,35 +2,22 @@ name: Artifact Build
on: [pull_request]
-permissions:
- contents: read
-
jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
-
+ - uses: actions/checkout@v4
- name: Set up JDK 17
- uses: actions/setup-java@v5
+ uses: actions/setup-java@v4
with:
- distribution: temurin
- java-version: 17
-
+ java-version: '17'
+ distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
-
- - name: Setup Gradle
- uses: gradle/actions/setup-gradle@v5
- with:
- cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- cache-read-only: false
-
- name: Run Gradle
- run: ./gradlew assemblePrereleaseDebug lint check
-
+ run: ./gradlew assemblePrereleaseDebug
- name: Upload Artifact
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@v4
with:
name: pull-request-build
path: "app/build/outputs/apk/prerelease/debug/*.apk"
diff --git a/.github/workflows/update_locales.yml b/.github/workflows/update_locales.yml
index 0a538d5d4..ce140e559 100644
--- a/.github/workflows/update_locales.yml
+++ b/.github/workflows/update_locales.yml
@@ -1,19 +1,17 @@
name: Fix locale issues
on:
+ workflow_dispatch:
push:
- branches: [ master ]
paths:
- '**.xml'
- workflow_dispatch:
+ branches:
+ - master
-concurrency:
+concurrency:
group: "locale"
cancel-in-progress: true
-permissions:
- contents: read
-
jobs:
create:
runs-on: ubuntu-latest
@@ -25,17 +23,15 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream"
-
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
with:
token: ${{ steps.generate_token.outputs.token }}
-
- name: Install dependencies
- run: pip3 install lxml requests
-
+ run: |
+ pip3 install lxml
- name: Edit files
- run: python3 .github/locales.py
-
+ run: |
+ python3 .github/locales.py
- name: Commit to the repo
run: |
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
diff --git a/.gitignore b/.gitignore
index 5fc9f0870..2ac6c9695 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+*.iml
+.gradle
/local.properties
/.idea/caches
/.idea/misc.xml
@@ -9,220 +11,6 @@
.DS_Store
/build
/captures
-.cxx
-.kotlin/*
-
-# Created by https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,visualstudiocode
-# Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,java,android,androidstudio,visualstudiocode
-
-### Android ###
-# Gradle files
-.gradle/
-build/
-
-# Local configuration file (sdk path, etc)
-local.properties
-
-# Log/OS Files
-*.log
-
-# Android Studio generated files and folders
-captures/
-.externalNativeBuild/
-.cxx/
-*.apk
-output.json
-
-# IntelliJ
-*.iml
-.idea/
-misc.xml
-deploymentTargetDropDown.xml
-render.experimental.xml
-
-# Keystore files
-*.jks
-*.keystore
-
-# Google Services (e.g. APIs or Firebase)
-google-services.json
-
-# Android Profiling
-*.hprof
-
-### Android Patch ###
-gen-external-apklibs
-
-# Replacement of .externalNativeBuild directories introduced
-# with Android Studio 3.5.
-
-### Java ###
-# Compiled class file
-*.class
-
-# Log file
-
-# BlueJ files
-*.ctxt
-
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-
-# Package Files #
-*.jar
-*.war
-*.nar
-*.ear
-*.zip
-*.tar.gz
-*.rar
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-replay_pid*
-
-### Kotlin ###
-# Compiled class file
-
-# Log file
-
-# BlueJ files
-
-# Mobile Tools for Java (J2ME)
-
-# Package Files #
-
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-
-### VisualStudioCode ###
-.vscode/*
-
-# Local History for Visual Studio Code
-.history/
-
-# Built Visual Studio Code Extensions
-*.vsix
-
-### VisualStudioCode Patch ###
-# Ignore all local history of files
-.history
-.ionide
-
-### AndroidStudio ###
-# Covers files to be ignored for android development using Android Studio.
-
-# Built application files
-*.ap_
-*.aab
-
-# Files for the ART/Dalvik VM
-*.dex
-
-# Java class files
-
-# Generated files
-bin/
-gen/
-out/
-
-# Gradle files
-.gradle
-
-# Signing files
-.signing/
-
-# Local configuration file (sdk path, etc)
-
-# Proguard folder generated by Eclipse
-proguard/
-
-# Log Files
-
-# Android Studio
-/*/build/
-/*/local.properties
-/*/out
-/*/*/build
-/*/*/production
-.navigation/
-*.ipr
-*~
-*.swp
-
-# Keystore files
-
-# Google Services (e.g. APIs or Firebase)
-# google-services.json
-
-# Android Patch
-
-# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
-
-# NDK
-obj/
-
-# IntelliJ IDEA
-*.iws
-/out/
-
-# User-specific configurations
-.idea/caches/
-.idea/libraries/
-.idea/shelf/
-.idea/workspace.xml
-.idea/tasks.xml
-.idea/.name
-.idea/compiler.xml
-.idea/copyright/profiles_settings.xml
-.idea/encodings.xml
-.idea/misc.xml
-.idea/modules.xml
-.idea/scopes/scope_settings.xml
-.idea/dictionaries
-.idea/vcs.xml
-.idea/jsLibraryMappings.xml
-.idea/datasources.xml
-.idea/dataSources.ids
-.idea/sqlDataSources.xml
-.idea/dynamic.xml
-.idea/uiDesigner.xml
-.idea/assetWizardSettings.xml
-.idea/gradle.xml
-.idea/jarRepositories.xml
-.idea/navEditor.xml
-
-# Legacy Eclipse project files
-.classpath
-.project
-.cproject
-.settings/
-
-# Mobile Tools for Java (J2ME)
-
-# Package Files #
-
-# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
-
-## Plugin-specific files:
-
-# mpeltonen/sbt-idea plugin
-.idea_modules/
-
-# JIRA plugin
-atlassian-ide-plugin.xml
-
-# Mongo Explorer plugin
-.idea/mongoSettings.xml
-
-# Crashlytics plugin (for Android Studio and IntelliJ)
-com_crashlytics_export_strings.xml
-crashlytics.properties
-crashlytics-build.properties
-fabric.properties
-
-### AndroidStudio Patch ###
-
-!/gradle/wrapper/gradle-wrapper.jar
-
-# End of https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,visualstudiocode
+.cxx
+local.properties
diff --git a/.idea/.name b/.idea/.name
new file mode 100644
index 000000000..1eb497a93
--- /dev/null
+++ b/.idea/.name
@@ -0,0 +1 @@
+CloudStream
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 000000000..7643783a8
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 000000000..79ee123c2
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 000000000..b589d56e9
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/discord.xml b/.idea/discord.xml
new file mode 100644
index 000000000..d8e956166
--- /dev/null
+++ b/.idea/discord.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
new file mode 100644
index 000000000..db202a929
--- /dev/null
+++ b/.idea/gradle.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 000000000..333d49373
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/studiobot.xml b/.idea/studiobot.xml
new file mode 100644
index 000000000..9298202cb
--- /dev/null
+++ b/.idea/studiobot.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 000000000..35eb1ddfb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..7282979ad
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,6 @@
+{
+ "githubPullRequests.ignoredPullRequestBranches": [
+ "master"
+ ],
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
\ No newline at end of file
diff --git a/AI-POLICY.md b/AI-POLICY.md
deleted file mode 100644
index 5409393fb..000000000
--- a/AI-POLICY.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# AI Policy
-
-AI is a great tool. However, we want you to follow these rules regarding usage of AI in order to ensure the quality of both code and discussions.
-
-1. Always state any AI usage in pull requests and issues.
-
-2. Always test code before making a pull request. We do not want to test your AI generated code.
-
-3. Listen to humans over computers. Contributors to CloudStream know this codebase better than an AI.
-
-4. You should be able to explain and fix any code you submit. We do in-depth reviews and will reject low effort contributions.
diff --git a/README.md b/README.md
index c2492c5d8..8949304e9 100644
--- a/README.md
+++ b/README.md
@@ -1,46 +1,11 @@
# CloudStream
-**⚠️ Warning: By default, this app doesn't provide any video sources; you have to install extensions to add functionality to the app.**
+**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
+
[](https://discord.gg/5Hus6fM)
-
-## Table of Contents:
-+ [About Us:](#about_us)
-+ [Installation Steps:](#install_rules)
-+ [Contributing:](#contributing)
-+ [Issues:](#issues)
- + [Bugs Reports:](#bug_report)
- + [Enhancement:](#enhancment)
-+ [Extension Development:](#extensions)
-+ [Language Support:](#languages)
-+ [Further Sources](#contact_and_sources)
-
-
-
-
-## About us:
-
-**CloudStream is a media center that prioritizes and emphasizes complete freedom and flexibility for users and developers.**
-
-CloudStream is an extension-based multimedia player with tracking support. There are extensions to view videos from:
-
-+ [Librevox (audio-books)](https://librivox.org/)
-+ [Youtube](https://www.youtube.com/)
-+ [Twitch](https://www.twitch.tv/)
-+ [iptv-org (A collection of publicly available IPTV (Internet Protocol television) channels from all over the world.)](https://github.com/iptv-org/iptv)
-+ [nginx](https://nginx.org/)
-+ And more...
-
-
-**Please don't create illegal extensions or use any that host any copyrighted media.** For more details about our stance on the DMCA and EUCD, you can read about it on our organization: [reCloudStream](https://github.com/recloudstream)
-
-#### Important Copyright Note:
-
-Our documentation is unmaintained and open to contributions; therefore, apps and sources, extensions in recommended sources, and recommended apps are not officially moderated or endorsed by CloudStream; if you or another copyright owner identify an extension that breaches your copyright, please let us know.
-
-
-#### Features:
+### Features:
+ **AdFree**, No ads whatsoever
+ No tracking/analytics
+ Bookmarks
@@ -48,64 +13,7 @@ Our documentation is unmaintained and open to contributions; therefore, apps and
+ Chromecast
+ Extension system for personal customization
-
-
-
-## Installation:
-
-Our documentation provides the steps to install and configure CloudStream for your streaming needs.
-
-[Getting Started With CloudStream:](https://recloudstream.github.io/csdocs/)
-
-
-
-## Contributing:
-We **happily** accept any contributions to our project. To find out where you can start contributing towards the project, please look [at our issues tab](/cloudstream/issues)
-
-
-
-
-
-### Issues:
-While we **actively** accept issues and pull requests, we do require you fill out an [template](https://github.com/recloudstream/cloudstream/issues/new/choose) for issues. These include the following:
-
-
-
-- [Bug Report Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=bug&projects=&template=application-bug.yml)
- - For bug reports, we want as much info as possible, including your downloaded version of CloudeStream, device and updated version (if possible, current API),
- expected behavior of the program, and the actual behavior that the program did, most importantly we require clear, reproducible steps of the bug. If your bug can't be reproduced, it is unlikely we'll work on your issue.
-
-
-
-- [Feature Request Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=enhancement&projects=&template=feature-request.yml)
- - Before adding a feature request, please check to see if a feature request already has been requested.
-
-
-### Extensions:
-
-**Further details on creating extensions for CloudStream are found in our documentation.**
-
-[Guide: For Extension Developers](https://recloudstream.github.io/csdocs/devs/gettingstarted/)
-
-
-
-## Further Sources:
-
-As well as providing clear install steps, our [website](https://dweb.link/ipns/cloudstream.on.fleek.co/) includes a wide variety of other tools, such as:
-- [Troubleshooting](https://recloudstream.github.io/csdocs/troubleshooting/)
-- [Further CloudStream Repositories](https://recloudstream.github.io/csdocs/repositories/)
-- Set-Up for other devices, such as:
- - [Android TV](https://recloudstream.github.io/csdocs/other-devices/tv/)
- - [Windows](https://recloudstream.github.io/csdocs/other-devices/windows/)
- - [Linux](https://recloudstream.github.io/csdocs/other-devices/linux/)
-- And more...
-
-
-
### Supported languages:
-
-Even if you can't contribute to the code or documentation, we always look for those who can contribute to translation and language support. Your contribution is exceptionally appreciated; you can check our translation from the figure below.
-
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
new file mode 100644
index 000000000..7f7fd14c1
--- /dev/null
+++ b/app/CMakeLists.txt
@@ -0,0 +1,6 @@
+# Set this to the minimum version your project supports.
+cmake_minimum_required(VERSION 3.18)
+project(CrashHandler)
+find_library(log-lib log)
+add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
+target_link_libraries(native-lib ${log-lib})
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 02c1f99e8..48a28e89b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,96 +1,48 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform
-import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier
-import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
-import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
+import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import java.io.ByteArrayOutputStream
plugins {
- alias(libs.plugins.android.application)
- alias(libs.plugins.dokka)
- alias(libs.plugins.kotlin.serialization)
+ id("com.android.application")
+ id("com.google.devtools.ksp")
+ id("kotlin-android")
+ id("org.jetbrains.dokka")
}
-val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
+val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
+val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
-abstract class GenerateGitHashTask : DefaultTask() {
-
- @get:InputFile
- @get:PathSensitive(PathSensitivity.RELATIVE)
- abstract val headFile: RegularFileProperty
-
- @get:InputDirectory
- @get:PathSensitive(PathSensitivity.RELATIVE)
- abstract val headsDir: DirectoryProperty
-
- @get:OutputDirectory
- abstract val outputDir: DirectoryProperty
-
- @TaskAction
- fun generate() {
- val head = headFile.get().asFile
-
- val hash = try {
- if (head.exists()) {
- // Read the commit hash from .git/HEAD
- val headContent = head.readText().trim()
- if (headContent.startsWith("ref:")) {
- val refPath = headContent.substring(5) // e.g., refs/heads/main
- val commitFile = File(head.parentFile, refPath)
- if (commitFile.exists()) commitFile.readText().trim() else ""
- } else headContent // If it's a detached HEAD (commit hash directly)
- } else "" // If .git/HEAD doesn't exist
- } catch (_: Throwable) {
- "" // Just set to an empty string if any exception occurs
- }.take(7) // Get the short commit hash
-
- val outFile = outputDir.file("git-hash.txt").get().asFile
- outFile.parentFile.mkdirs()
- outFile.writeText(hash)
- }
-}
-
-val generateGitHash = tasks.register("generateGitHash") {
- val gitDir = layout.projectDirectory.dir("../.git")
-
- headFile.set(gitDir.file("HEAD"))
- headsDir.set(gitDir.dir("refs/heads"))
-
- outputDir.set(layout.buildDirectory.dir("generated/git"))
+fun String.execute() = ByteArrayOutputStream().use { baot ->
+ if (project.exec {
+ workingDir = projectDir
+ commandLine = this@execute.split(Regex("\\s"))
+ standardOutput = baot
+ }.exitValue == 0)
+ String(baot.toByteArray()).trim()
+ else null
}
android {
- @Suppress("UnstableApiUsage")
testOptions {
unitTests.isReturnDefaultValues = true
}
- // Looks like google likes to add metadata only they can read https://gitlab.com/IzzyOnDroid/repo/-/work_items/491
- dependenciesInfo {
- // Disables dependency metadata when building APKs.
- includeInApk = false
- // Disables dependency metadata when building Android App Bundles.
- includeInBundle = false
+ viewBinding {
+ enable = true
}
- androidComponents {
- onVariants { variant ->
- variant.sources.assets?.addGeneratedSourceDirectory(
- generateGitHash,
- GenerateGitHashTask::outputDir
- )
+ /* disable this for now
+ externalNativeBuild {
+ cmake {
+ path("CMakeLists.txt")
}
- }
+ }*/
signingConfigs {
- // We just use SIGNING_KEY_ALIAS here since it won't change
- // so won't kill the configuration cache.
- if (System.getenv("SIGNING_KEY_ALIAS") != null) {
+ if (prereleaseStoreFile != null) {
create("prerelease") {
- val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
- val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
-
- storeFile = prereleaseStoreFile?.let { file(it) }
+ storeFile = file(prereleaseStoreFile)
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
@@ -98,19 +50,23 @@ android {
}
}
- compileSdk = libs.versions.compileSdk.get().toInt()
+ compileSdk = 34
+ buildToolsVersion = "34.0.0"
defaultConfig {
applicationId = "com.lagradost.cloudstream3"
- minSdk = libs.versions.minSdk.get().toInt()
- targetSdk = libs.versions.targetSdk.get().toInt()
- versionCode = libs.versions.versionCode.get().toInt()
- versionName = libs.versions.versionName.get()
+ minSdk = 21
+ targetSdk = 33 /* Android 14 is Fu*ked
+ ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
+ versionCode = 64
+ versionName = "4.4.0"
- manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
+ resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
+ resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
+ resValue("bool", "is_prerelease", "false")
// Reads local.properties
- val localProperties = gradleLocalProperties(rootDir, project.providers)
+ val localProperties = gradleLocalProperties(rootDir)
buildConfigField(
"long",
@@ -128,6 +84,11 @@ android {
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ ksp {
+ arg("room.schemaLocation", "$projectDir/schemas")
+ arg("exportSchema", "true")
+ }
}
buildTypes {
@@ -154,9 +115,12 @@ android {
productFlavors {
create("stable") {
dimension = "state"
+ resValue("bool", "is_prerelease", "false")
}
create("prerelease") {
dimension = "state"
+ resValue("bool", "is_prerelease", "true")
+ buildConfigField("boolean", "BETA", "true")
applicationIdSuffix = ".prerelease"
if (signingConfigs.names.contains("prerelease")) {
signingConfig = signingConfigs.getByName("prerelease")
@@ -170,33 +134,17 @@ android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = JavaVersion.toVersion(javaTarget.target)
- targetCompatibility = JavaVersion.toVersion(javaTarget.target)
- }
-
- java {
- // Use Java 17 toolchain even if a higher JDK runs the build.
- // We still use Java 8 for now which higher JDKs have deprecated.
- toolchain {
- languageVersion.set(JavaLanguageVersion.of(libs.versions.jdkToolchain.get()))
- }
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
}
lint {
+ abortOnError = false
checkReleaseBuilds = false
}
buildFeatures {
buildConfig = true
- viewBinding = true
- }
-
- packaging {
- jniLibs {
- // Enables legacy JNI packaging to reduce APK size (similar to builds before minSdk 23).
- // Note: This may increase app startup time slightly.
- useLegacyPackaging = true
- }
}
namespace = "com.lagradost.cloudstream3"
@@ -204,89 +152,105 @@ android {
dependencies {
// Testing
- testImplementation(libs.junit)
- testImplementation(libs.json)
- androidTestImplementation(libs.core)
- androidTestImplementation(libs.espresso.core)
- androidTestImplementation(libs.ext.junit)
- androidTestImplementation(libs.instancio.core)
- androidTestImplementation(libs.junit.ktx)
- androidTestImplementation(libs.kotlin.test)
+ testImplementation("junit:junit:4.13.2")
+ testImplementation("org.json:json:20240303")
+ androidTestImplementation("androidx.test:core")
+ implementation("androidx.test.ext:junit-ktx:1.2.1")
+ androidTestImplementation("androidx.test.ext:junit:1.2.1")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
// Android Core & Lifecycle
- implementation(libs.core.ktx)
- implementation(libs.activity.ktx)
- implementation(libs.annotation)
- implementation(libs.appcompat)
- implementation(libs.fragment.ktx)
- implementation(libs.bundles.lifecycle)
- implementation(libs.bundles.navigation)
- implementation(libs.kotlinx.collections.immutable)
- implementation(libs.kotlinx.serialization.json) // JSON Parser
+ implementation("androidx.core:core-ktx:1.13.1")
+ implementation("androidx.appcompat:appcompat:1.7.0")
+ implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
// Design & UI
- implementation(libs.preference.ktx)
- implementation(libs.material)
- implementation(libs.constraintlayout)
+ implementation("jp.wasabeef:glide-transformations:4.3.0")
+ implementation("androidx.preference:preference-ktx:1.2.1")
+ implementation("com.google.android.material:material:1.12.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
- // Coil Image Loading
- implementation(libs.bundles.coil)
+ // Glide Module
+ ksp("com.github.bumptech.glide:ksp:4.16.0")
+ implementation("com.github.bumptech.glide:glide:4.16.0")
+ implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
+
+ // For KSP -> Official Annotation Processors are Not Yet Supported for KSP
+ ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
+ implementation("com.google.guava:guava:33.2.1-android")
+ implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
// Media 3 (ExoPlayer)
- implementation(libs.bundles.media3)
- implementation(libs.video)
-
- // FFmpeg Decoding
- implementation(libs.bundles.nextlib)
-
- // Anime-db for filler
- implementation(libs.anime.db)
+ implementation("androidx.media3:media3-ui:1.4.1")
+ implementation("androidx.media3:media3-cast:1.4.1")
+ implementation("androidx.media3:media3-common:1.4.1")
+ implementation("androidx.media3:media3-session:1.4.1")
+ implementation("androidx.media3:media3-exoplayer:1.4.1")
+ implementation("com.google.android.mediahome:video:1.0.0")
+ implementation("androidx.media3:media3-exoplayer-hls:1.4.1")
+ implementation("androidx.media3:media3-exoplayer-dash:1.4.1")
+ implementation("androidx.media3:media3-datasource-okhttp:1.4.1")
// PlayBack
- implementation(libs.colorpicker) // Subtitle Color Picker
- implementation(libs.newpipeextractor) // For Trailers
- implementation(libs.juniversalchardet) // Subtitle Decoding
+ implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
+ implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
+ implementation("com.github.teamnewpipe:NewPipeExtractor:v0.24.2") /* For Trailers
+ ^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
+ implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding
+
+ // Crash Reports (AcraApplication.kt)
+ implementation("ch.acra:acra-core:5.11.3")
+ implementation("ch.acra:acra-toast:5.11.3")
// UI Stuff
- implementation(libs.shimmer) // Shimmering Effect (Loading Skeleton)
- implementation(libs.palette.ktx) // Palette for Images -> Colors
- implementation(libs.tvprovider)
- implementation(libs.overlappingpanels) // Gestures
- implementation(libs.biometric) // Fingerprint Authentication
- implementation(libs.previewseekbar.media3) // SeekBar Preview
- implementation(libs.qrcode.kotlin) // QR Code for PIN Auth on TV
+ implementation("com.facebook.shimmer:shimmer:0.5.0") // Shimmering Effect (Loading Skeleton)
+ implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
+ implementation("androidx.tvprovider:tvprovider:1.0.0")
+ implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
+ implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
+ implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
+ implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV
// Extensions & Other Libs
- implementation(libs.jsoup) // HTML Parser
- implementation(libs.rhino) // Run JavaScript
- implementation(libs.safefile) // To Prevent the URI File Fu*kery
- coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
- implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9
- implementation(libs.jackson.module.kotlin) // JSON Parser
- implementation(libs.zipline)
-
- // Deprecated; will be removed once extensions have time to migrate from using it
- implementation("me.xdrop:fuzzywuzzy:1.4.0")
-
- // Torrent Support
- implementation(libs.torrentserver)
+ implementation("org.mozilla:rhino:1.7.15") // run JavaScript
+ implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
+ implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
+ implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
+ implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
+ ^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
+ Level 25 or Less. */
// Downloading & Networking
- implementation(libs.work.runtime.ktx)
- implementation(libs.nicehttp) // HTTP Lib
+ implementation("androidx.work:work-runtime:2.9.0")
+ implementation("androidx.work:work-runtime-ktx:2.9.0")
+ implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
- implementation(project(":library"))
+ implementation(project(":library") {
+ // There does not seem to be a good way of getting the android flavor.
+ val isDebug = gradle.startParameter.taskRequests.any { task ->
+ task.args.any { arg ->
+ arg.contains("debug", true)
+ }
+ }
+
+ this.extra.set("isDebug", isDebug)
+ })
}
tasks.register("androidSourcesJar") {
archiveClassifier.set("sources")
- from(android.sourceSets.getByName("main").java.directories) // Full Sources
+ from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
}
tasks.register("copyJar") {
- dependsOn("build", ":library:jvmJar")
from(
- "build/intermediates/compile_app_classes_jar/prereleaseDebug/bundlePrereleaseDebugClassesToCompileJar",
+ "build/intermediates/compile_app_classes_jar/prereleaseDebug",
"../library/build/libs"
)
into("build/app-classes")
@@ -305,39 +269,12 @@ tasks.register("makeJar") {
zipTree("build/app-classes/library-jvm.jar")
)
destinationDirectory.set(layout.buildDirectory)
- archiveBaseName = "classes"
+ archivesName = "classes"
}
-tasks.withType {
- compilerOptions {
- jvmTarget.set(javaTarget)
- jvmDefault.set(JvmDefaultMode.ENABLE)
- freeCompilerArgs.add("-Xannotation-default-target=param-property")
- optIn.addAll(
- "com.lagradost.cloudstream3.InternalAPI",
- "com.lagradost.cloudstream3.Prerelease",
- "kotlin.uuid.ExperimentalUuidApi",
- )
+tasks.withType {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
-}
-
-dokka {
- moduleName = "App"
- dokkaSourceSets {
- configureEach {
- suppress = name != "prereleaseDebug"
- analysisPlatform = KotlinPlatform.JVM
- displayName = "JVM"
- documentedVisibilities(
- VisibilityModifier.Public,
- VisibilityModifier.Protected
- )
-
- sourceLink {
- localDirectory = file("..")
- remoteUrl("https://github.com/recloudstream/cloudstream/tree/master")
- remoteLineSuffix = "#L"
- }
- }
- }
-}
+}
\ No newline at end of file
diff --git a/app/lint.xml b/app/lint.xml
deleted file mode 100644
index b2f5e8f2b..000000000
--- a/app/lint.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
index 4c5cdea5b..c7f02baff 100644
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
@@ -7,7 +7,6 @@ import android.view.LayoutInflater
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding
-import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
@@ -89,8 +88,6 @@ class ExampleInstrumentedTest {
// testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv)
//testAllLayouts(activity, R.layout.activity_main_tv)
- testAllLayouts(activity, R.layout.bottom_resultview_preview,R.layout.bottom_resultview_preview_tv)
-
testAllLayouts(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
testAllLayouts(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
@@ -136,14 +133,14 @@ class ExampleInstrumentedTest {
@Test
@Throws(AssertionError::class)
fun providerCorrectData() {
- val langTagsIETF = SubtitleHelper.languages.map { it.IETF_tag }
- Assert.assertFalse("IETFTagNames does not contain any languages", langTagsIETF.isNullOrEmpty())
+ val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
+ Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
for (api in getAllProviders()) {
Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE")
Assert.assertTrue("Api does not contain a name", api.name != "NONE")
Assert.assertTrue(
"Api ${api.name} does not contain a valid language code",
- langTagsIETF.contains(api.lang)
+ isoNames.contains(api.lang)
)
Assert.assertTrue(
"Api ${api.name} does not contain any supported types",
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt
deleted file mode 100644
index 80c7b49b0..000000000
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/SerializationClassTester.kt
+++ /dev/null
@@ -1,134 +0,0 @@
-package com.lagradost.cloudstream3
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import dalvik.system.DexFile
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.InternalSerializationApi
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.serializer
-import kotlinx.serialization.serializerOrNull
-import org.instancio.Instancio
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.reflect.KClass
-import kotlin.reflect.jvm.jvmName
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
-
-@RunWith(AndroidJUnit4::class)
-class SerializationClassTester {
- // Same as app, or using app reference
- val jacksonMapper = mapper
- val kotlinxMapper = json
-
- @Test
- fun isIdenticalSerialization() {
- val serializableClasses = findSerializableClasses("com.lagradost")
- println("Number of serializable classes: ${serializableClasses.size}")
-
- serializableClasses.forEach { kClass ->
- val instance = Instancio.create(kClass.java)
-
- val jacksonJson = jacksonMapper.writeValueAsString(instance)
- val kotlinxJson = serializeWithKotlinx(kClass, instance)
-
- assertEquals(
- jacksonJson,
- kotlinxJson,
- """
- Serialization mismatch for:
- ${kClass.qualifiedName}
-
- Jackson:
- $jacksonJson
-
- Kotlinx:
- $kotlinxJson
-
- """.trimIndent()
- )
- println("Identical serialization for: ${kClass.jvmName}")
- }
- }
-
- @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
- @Test
- fun isIdenticalDeserialization() {
- val serializableClasses = findSerializableClasses("com.lagradost")
- println("Number of serializable classes: ${serializableClasses.size}")
-
- serializableClasses.forEach { kClass ->
- val instance = Instancio.create(kClass.java)
- // Convert to JSON to get example JSON object
- // We prefer jackson here because the app may have many jackson JSON strings in local storage
- val originalJson = jacksonMapper.writeValueAsString(instance)
-
- // Create an object from the JSON using kotlinx
- val serializer =
- kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass)
- assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!")
- val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson)
-
- // Create an object from the JSON using jackson
- val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java)
-
-
- // Deep inspect both object using the mapper toJson function.
- // This deep equality check can be performed using other methods, but this just works.
- val jacksonJson = mapperDecoded.toJson()
- val kotlinxJson = kotlinxDecoded.toJson()
-
- assertEquals(
- jacksonJson,
- kotlinxJson,
- """
- Serialization mismatch for:
- ${kClass.qualifiedName}
-
- Jackson:
- $jacksonJson
-
- Kotlinx:
- $kotlinxJson
-
- """.trimIndent()
- )
- println("Identical deserialization for: ${kClass.jvmName}")
- }
- }
-
- // DEX files are the best solution to read all our classes dynamically.
- // classgraph could be used instead, but it only gives results on the JVM, not Android.
- @Suppress("DEPRECATION")
- private fun findSerializableClasses(packageName: String): List> {
- val context = InstrumentationRegistry
- .getInstrumentation()
- .targetContext
-
- val dexFile = DexFile(context.packageCodePath)
- return dexFile.entries()
- .toList()
- .filter { it.startsWith(packageName) }
- .mapNotNull {
- runCatching { Class.forName(it).kotlin }.getOrNull()
- }.filter { kClass ->
- // Not possible to use .hasAnnotation() on newer Android versions.
- kClass.java.annotations.any {
- it is Serializable
- }
- }
- }
-
- @OptIn(InternalSerializationApi::class)
- @Suppress("UNCHECKED_CAST")
- private fun serializeWithKotlinx(
- kClass: KClass<*>,
- value: Any
- ): String {
- val serializer = kClass.serializer() as KSerializer
- return kotlinxMapper.encodeToString(serializer, value)
- }
-}
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt
deleted file mode 100644
index 15ad532f8..000000000
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/utils/serializers/SerializerTest.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-package com.lagradost.cloudstream3.utils.serializers
-
-import android.net.Uri
-import com.lagradost.cloudstream3.utils.AppUtils.parseJson
-import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.KeepGeneratedSerializer
-import kotlinx.serialization.Serializable
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertNull
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-@OptIn(ExperimentalSerializationApi::class)
-@KeepGeneratedSerializer
-@Serializable(with = NonEmptyData.Serializer::class)
-data class NonEmptyData(
- val title: String = "",
- val tags: List = emptyList(),
- val meta: Map = emptyMap(),
- val name: String = "hello",
-) {
- object Serializer : NonEmptySerializer(NonEmptyData.generatedSerializer())
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-@KeepGeneratedSerializer
-@Serializable(with = WriteOnlyData.Serializer::class)
-data class WriteOnlyData(
- val fieldA: String = "",
- val fieldB: String = "",
-) {
- object Serializer : WriteOnlySerializer(
- WriteOnlyData.generatedSerializer(),
- setOf("fieldB"),
- )
-}
-
-@OptIn(ExperimentalSerializationApi::class)
-@KeepGeneratedSerializer
-@Serializable(with = MultiWriteOnly.Serializer::class)
-data class MultiWriteOnly(
- val fieldA: String = "",
- val fieldB: String = "",
- val fieldC: String = "",
-) {
- object Serializer : WriteOnlySerializer(
- MultiWriteOnly.generatedSerializer(),
- setOf("fieldB", "fieldC"),
- )
-}
-
-@Serializable
-data class UriData(
- @Serializable(with = UriSerializer::class)
- val uri: Uri = Uri.EMPTY,
-)
-
-class SerializerTest {
-
- @Test
- fun nonEmptySerializerOmitsEmptyStrings() {
- val data = NonEmptyData(title = "", name = "hello")
- val result = data.toJson()
- assertFalse(result.contains("title"))
- assertTrue(result.contains("name"))
- }
-
- @Test
- fun nonEmptySerializerOmitsEmptyLists() {
- val data = NonEmptyData(tags = emptyList(), name = "hello")
- val result = data.toJson()
- assertFalse(result.contains("tags"))
- }
-
- @Test
- fun nonEmptySerializerOmitsEmptyMaps() {
- val data = NonEmptyData(meta = emptyMap(), name = "hello")
- val result = data.toJson()
- assertFalse(result.contains("meta"))
- }
-
- @Test
- fun nonEmptySerializerKeepsNonEmptyFields() {
- val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v"))
- val result = data.toJson()
- assertTrue(result.contains("title"))
- assertTrue(result.contains("tags"))
- assertTrue(result.contains("meta"))
- }
-
- @Test
- fun nonEmptySerializerDoesNotAffectDeserialization() {
- val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}"""
- val result = parseJson(input)
- assertEquals("hello", result.title)
- assertEquals(listOf("a"), result.tags)
- assertEquals(mapOf("k" to "v"), result.meta)
- assertEquals("world", result.name)
- }
-
- @Test
- fun writeOnlySerializerOmitsFieldOnSerialize() {
- val data = WriteOnlyData(fieldA = "hello", fieldB = "secret")
- val result = data.toJson()
- assertTrue(result.contains("fieldA"))
- assertFalse(result.contains("fieldB"))
- }
-
- @Test
- fun writeOnlySerializerDeserializesNormally() {
- val input = """{"fieldA":"hello","fieldB":"secret"}"""
- val result = parseJson(input)
- assertEquals("hello", result.fieldA)
- assertEquals("secret", result.fieldB)
- }
-
- @Test
- fun writeOnlySerializerDeserializesMissingAsDefault() {
- val input = """{"fieldA":"hello"}"""
- val result = parseJson(input)
- assertEquals("hello", result.fieldA)
- assertEquals("", result.fieldB)
- }
-
- @Test
- fun writeOnlySerializerHandlesMultipleKeys() {
- val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2")
- val result = data.toJson()
- assertTrue(result.contains("fieldA"))
- assertFalse(result.contains("fieldB"))
- assertFalse(result.contains("fieldC"))
- }
-
- @Test
- fun uriSerializerSerializesUriToString() {
- val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
- val result = data.toJson()
- assertTrue(result.contains("https://example.com/path?query=1"))
- }
-
- @Test
- fun uriSerializerDeserializesStringToUri() {
- val input = """{"uri":"https://example.com/path?query=1"}"""
- val result = parseJson(input)
- assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
- }
-
- @Test
- fun uriSerializerRoundtripsCorrectly() {
- val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
- val encoded = data.toJson()
- val decoded = parseJson(encoded)
- assertEquals(data.uri, decoded.uri)
- }
-}
diff --git a/app/src/debug/res/drawable-v24/ic_banner_background.xml b/app/src/debug/res/drawable-v24/ic_banner_background.xml
index caed023d5..7b05b7111 100644
--- a/app/src/debug/res/drawable-v24/ic_banner_background.xml
+++ b/app/src/debug/res/drawable-v24/ic_banner_background.xml
@@ -25,8 +25,9 @@
android:endY="245.72"
android:endX="292.58"
android:type="linear">
-
-
+
+
+
@@ -39,8 +40,9 @@
android:endY="245.72"
android:endX="248.76"
android:type="linear">
-
-
+
+
+
@@ -53,45 +55,46 @@
android:endY="245.69"
android:endX="210.03"
android:type="linear">
-
-
+
+
+
+ android:fillColor="#2e24ff"/>
+ android:fillColor="#2e24ff"/>
+ android:fillColor="#2e24ff"/>
+ android:fillColor="#2e24ff"/>
+ android:fillColor="#2e24ff"/>
+ android:fillColor="#5252ff"/>
+ android:fillColor="#5252ff"/>
+ android:fillColor="#5252ff"/>
+ android:fillColor="#5252ff"/>
+ android:fillColor="#5252ff"/>
+ android:fillColor="#5252ff"/>
@@ -101,9 +104,9 @@
android:endY="252.3"
android:endX="373.57"
android:type="linear">
-
-
-
+
+
+
@@ -114,9 +117,9 @@
android:startX="400.11"
android:endX="900"
android:type="linear">
-
-
-
+
+
+
@@ -129,9 +132,9 @@
android:endY="252.3"
android:endX="373.57"
android:type="linear">
-
-
-
+
+
+
@@ -142,9 +145,9 @@
android:startX="700.11"
android:endX="900.57"
android:type="linear">
-
-
-
+
+
+
@@ -155,9 +158,9 @@
android:startX="400.11"
android:endX="800.57"
android:type="linear">
-
-
-
+
+
+
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ee4c978f2..a04504acd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,62 +7,21 @@
+
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ tools:targetApi="tiramisu">
+ android:launchMode="singleTask">
@@ -125,55 +83,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:supportsPictureInPicture="true">
@@ -200,14 +122,7 @@
-
-
-
-
-
-
-
@@ -231,7 +146,7 @@
-
+
@@ -244,6 +159,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+#include
+#include
+
+#define TAG "CloudStream Crash Handler"
+volatile sig_atomic_t gSignalStatus = 0;
+void handleNativeCrash(int signal) {
+ gSignalStatus = signal;
+}
+
+extern "C" JNIEXPORT void JNICALL
+Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) {
+ #define REGISTER_SIGNAL(X) signal(X, handleNativeCrash);
+ REGISTER_SIGNAL(SIGSEGV)
+ #undef REGISTER_SIGNAL
+}
+
+//extern "C" JNIEXPORT void JNICALL
+//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) {
+// int *p = nullptr;
+// *p = 0;
+//}
+
+extern "C" JNIEXPORT int JNICALL
+Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) {
+ //__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus);
+ return gSignalStatus;
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
index bbe7d97de..d6f978fe5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
@@ -1,78 +1,216 @@
package com.lagradost.cloudstream3
-/**
- * Deprecated alias for CloudStreamApp for backwards compatibility with plugins.
- * Use CloudStreamApp instead.
- */
-@Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp"),
- level = DeprecationLevel.WARNING
-)
-class AcraApplication {
- companion object {
+import android.app.Activity
+import android.app.Application
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.widget.Toast
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import com.lagradost.api.setContext
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
+import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
+import com.lagradost.cloudstream3.plugins.PluginManager
+import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
+import com.lagradost.cloudstream3.ui.settings.Globals.TV
+import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
+import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
+import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
+import com.lagradost.cloudstream3.utils.DataStore.getKey
+import com.lagradost.cloudstream3.utils.DataStore.getKeys
+import com.lagradost.cloudstream3.utils.DataStore.removeKey
+import com.lagradost.cloudstream3.utils.DataStore.removeKeys
+import com.lagradost.cloudstream3.utils.DataStore.setKey
+import kotlinx.coroutines.runBlocking
+import org.acra.ACRA
+import org.acra.ReportField
+import org.acra.config.CoreConfiguration
+import org.acra.data.CrashReportData
+import org.acra.data.StringFormat
+import org.acra.ktx.initAcra
+import org.acra.sender.ReportSender
+import org.acra.sender.ReportSenderFactory
+import java.io.File
+import java.io.FileNotFoundException
+import java.io.PrintStream
+import java.lang.ref.WeakReference
+import java.util.Locale
+import kotlin.concurrent.thread
+import kotlin.system.exitProcess
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.context"),
- level = DeprecationLevel.WARNING
- )
- val context get() = CloudStreamApp.context
+class CustomReportSender : ReportSender {
+ // Sends all your crashes to google forms
+ override fun send(context: Context, errorContent: CrashReportData) {
+ println("Sending report")
+ val url =
+ "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
+ val data = mapOf(
+ "entry.1993829403" to errorContent.toJSON()
+ )
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.removeKeys(folder)"),
- level = DeprecationLevel.WARNING
- )
- fun removeKeys(folder: String): Int? =
- CloudStreamApp.removeKeys(folder)
+ thread { // to not run it on main thread
+ runBlocking {
+ suspendSafeApiCall {
+ app.post(url, data = data)
+ //println("Report response: $post")
+ }
+ }
+ }
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(path, value)"),
- level = DeprecationLevel.WARNING
- )
- fun setKey(path: String, value: T) =
- CloudStreamApp.setKey(path, value)
-
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(folder, path, value)"),
- level = DeprecationLevel.WARNING
- )
- fun setKey(folder: String, path: String, value: T) =
- CloudStreamApp.setKey(folder, path, value)
-
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path, defVal)"),
- level = DeprecationLevel.WARNING
- )
- inline fun getKey(path: String, defVal: T?): T? =
- CloudStreamApp.getKey(path, defVal)
-
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path)"),
- level = DeprecationLevel.WARNING
- )
- inline fun getKey(path: String): T? =
- CloudStreamApp.getKey(path)
-
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path)"),
- level = DeprecationLevel.WARNING
- )
- inline fun getKey(folder: String, path: String): T? =
- CloudStreamApp.getKey(folder, path)
-
- @Deprecated(
- message = "AcraApplication is deprecated, use CloudStreamApp instead",
- replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path, defVal)"),
- level = DeprecationLevel.WARNING
- )
- inline fun getKey(folder: String, path: String, defVal: T?): T? =
- CloudStreamApp.getKey(folder, path, defVal)
- }
+ runOnMainThread { // to run it on main looper
+ normalSafeApiCall {
+ Toast.makeText(context, R.string.acra_report_toast, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+}
+
+class CustomSenderFactory : ReportSenderFactory {
+ override fun create(context: Context, config: CoreConfiguration): ReportSender {
+ return CustomReportSender()
+ }
+
+ override fun enabled(config: CoreConfiguration): Boolean {
+ return true
+ }
+}
+
+class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
+ Thread.UncaughtExceptionHandler {
+ override fun uncaughtException(thread: Thread, error: Throwable) {
+ ACRA.errorReporter.handleException(error)
+ try {
+ PrintStream(errorFile).use { ps ->
+ ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
+ ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
+ error.printStackTrace(ps)
+ }
+ } catch (ignored: FileNotFoundException) {
+ }
+ try {
+ onError.invoke()
+ } catch (ignored: Exception) {
+ }
+ exitProcess(1)
+ }
+
+}
+
+class AcraApplication : Application() {
+
+ override fun onCreate() {
+ super.onCreate()
+ ExceptionHandler(filesDir.resolve("last_error")) {
+ val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
+ startActivity(Intent.makeRestartActivityTask(intent!!.component))
+ }.also {
+ exceptionHandler = it
+ Thread.setDefaultUncaughtExceptionHandler(it)
+ }
+ }
+
+ override fun attachBaseContext(base: Context?) {
+ super.attachBaseContext(base)
+ context = base
+
+ initAcra {
+ //core configuration:
+ buildConfigClass = BuildConfig::class.java
+ reportFormat = StringFormat.JSON
+
+ reportContent = listOf(
+ ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
+ ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
+ ReportField.STACK_TRACE,
+ )
+
+ // removed this due to bug when starting the app, moved it to when it actually crashes
+ //each plugin you chose above can be configured in a block like this:
+ /*toast {
+ text = getString(R.string.acra_report_toast)
+ //opening this block automatically enables the plugin.
+ }*/
+ }
+ }
+
+ companion object {
+ var exceptionHandler: ExceptionHandler? = null
+
+ /** Use to get activity from Context */
+ tailrec fun Context.getActivity(): Activity? = this as? Activity
+ ?: (this as? ContextWrapper)?.baseContext?.getActivity()
+
+ private var _context: WeakReference? = null
+ var context
+ get() = _context?.get()
+ private set(value) {
+ _context = WeakReference(value)
+ setContext(WeakReference(value))
+ }
+
+ fun getKeyClass(path: String, valueType: Class): T? {
+ return context?.getKey(path, valueType)
+ }
+
+ fun setKeyClass(path: String, value: T) {
+ context?.setKey(path, value)
+ }
+
+ fun removeKeys(folder: String): Int? {
+ return context?.removeKeys(folder)
+ }
+
+ fun setKey(path: String, value: T) {
+ context?.setKey(path, value)
+ }
+
+ fun setKey(folder: String, path: String, value: T) {
+ context?.setKey(folder, path, value)
+ }
+
+ inline fun getKey(path: String, defVal: T?): T? {
+ return context?.getKey(path, defVal)
+ }
+
+ inline fun getKey(path: String): T? {
+ return context?.getKey(path)
+ }
+
+ inline fun getKey(folder: String, path: String): T? {
+ return context?.getKey(folder, path)
+ }
+
+ inline fun getKey(folder: String, path: String, defVal: T?): T? {
+ return context?.getKey(folder, path, defVal)
+ }
+
+ fun getKeys(folder: String): List? {
+ return context?.getKeys(folder)
+ }
+
+ fun removeKey(folder: String, path: String) {
+ context?.removeKey(folder, path)
+ }
+
+ fun removeKey(path: String) {
+ context?.removeKey(path)
+ }
+
+ /**
+ * If fallbackWebview is true and a fragment is supplied then it will open a webview with the url if the browser fails.
+ * */
+ fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
+ context?.openBrowser(url, fallbackWebview, fragment)
+ }
+
+ /** Will fallback to webview if in TV layout */
+ fun openBrowser(url: String, activity: FragmentActivity?) {
+ openBrowser(
+ url,
+ isLayout(TV or EMULATOR),
+ activity?.supportFragmentManager?.fragments?.lastOrNull()
+ )
+ }
+ }
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt b/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt
deleted file mode 100644
index a9cd9c01e..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/CloudStreamApp.kt
+++ /dev/null
@@ -1,181 +0,0 @@
-package com.lagradost.cloudstream3
-
-import android.app.Activity
-import android.app.Application
-import android.content.Context
-import android.content.ContextWrapper
-import android.content.Intent
-import android.os.Build
-import android.widget.Toast
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentActivity
-import coil3.ImageLoader
-import coil3.PlatformContext
-import coil3.SingletonImageLoader
-import com.lagradost.api.setContext
-import com.lagradost.cloudstream3.BuildConfig
-import com.lagradost.cloudstream3.mvvm.safe
-import com.lagradost.cloudstream3.mvvm.safeAsync
-import com.lagradost.cloudstream3.plugins.PluginManager
-import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
-import com.lagradost.cloudstream3.ui.settings.Globals.TV
-import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
-import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
-import com.lagradost.cloudstream3.utils.AppDebug
-import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
-import com.lagradost.cloudstream3.utils.DataStore.getKey
-import com.lagradost.cloudstream3.utils.DataStore.getKeys
-import com.lagradost.cloudstream3.utils.DataStore.removeKey
-import com.lagradost.cloudstream3.utils.DataStore.removeKeys
-import com.lagradost.cloudstream3.utils.DataStore.setKey
-import com.lagradost.cloudstream3.utils.ImageLoader.buildImageLoader
-import kotlinx.coroutines.runBlocking
-import java.io.File
-import java.io.FileNotFoundException
-import java.io.PrintStream
-import java.lang.ref.WeakReference
-import java.util.Locale
-import kotlin.concurrent.thread
-import kotlin.system.exitProcess
-
-class ExceptionHandler(
- val errorFile: File,
- val onError: (() -> Unit)
-) : Thread.UncaughtExceptionHandler {
-
- override fun uncaughtException(thread: Thread, error: Throwable) {
- try {
- val threadId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
- thread.threadId()
- } else {
- @Suppress("DEPRECATION")
- thread.id
- }
-
- PrintStream(errorFile).use { ps ->
- ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
- ps.println("Fatal exception on thread ${thread.name} ($threadId)")
- error.printStackTrace(ps)
- }
- } catch (_: FileNotFoundException) {
- }
- try {
- onError()
- } catch (_: Exception) {
- }
- exitProcess(1)
- }
-}
-
-class CloudStreamApp : Application(), SingletonImageLoader.Factory {
-
- override fun onCreate() {
- super.onCreate()
- // If we want to initialize Coil as early as possible, maybe when
- // loading an image or GIF in a splash screen activity.
- // buildImageLoader(applicationContext)
-
- ExceptionHandler(filesDir.resolve("last_error")) {
- val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
- startActivity(Intent.makeRestartActivityTask(intent!!.component))
- }.also {
- exceptionHandler = it
- Thread.setDefaultUncaughtExceptionHandler(it)
- }
-
- AppDebug.isDebug = BuildConfig.DEBUG
- }
-
- override fun attachBaseContext(base: Context?) {
- super.attachBaseContext(base)
- context = base
- }
-
- override fun newImageLoader(context: PlatformContext): ImageLoader {
- // Coil module will be initialized globally when first loadImage() is invoked.
- return buildImageLoader(applicationContext)
- }
-
- companion object {
- var exceptionHandler: ExceptionHandler? = null
-
- /** Use to get Activity from Context. */
- tailrec fun Context.getActivity(): Activity? {
- return when (this) {
- is Activity -> this
- is ContextWrapper -> baseContext.getActivity()
- else -> null
- }
- }
-
- private var _context: WeakReference? = null
- var context
- get() = _context?.get()
- private set(value) {
- _context = WeakReference(value)
- setContext(WeakReference(value))
- }
-
- fun getKeyClass(path: String, valueType: Class): T? {
- return context?.getKey(path, valueType)
- }
-
- fun setKeyClass(path: String, value: T) {
- context?.setKey(path, value)
- }
-
- fun removeKeys(folder: String): Int? {
- return context?.removeKeys(folder)
- }
-
- fun setKey(path: String, value: T) {
- context?.setKey(path, value)
- }
-
- fun setKey(folder: String, path: String, value: T) {
- context?.setKey(folder, path, value)
- }
-
- inline fun getKey(path: String, defVal: T?): T? {
- return context?.getKey(path, defVal)
- }
-
- inline fun getKey(path: String): T? {
- return context?.getKey(path)
- }
-
- inline fun getKey(folder: String, path: String): T? {
- return context?.getKey(folder, path)
- }
-
- inline fun getKey(folder: String, path: String, defVal: T?): T? {
- return context?.getKey(folder, path, defVal)
- }
-
- fun getKeys(folder: String): List? {
- return context?.getKeys(folder)
- }
-
- fun removeKey(folder: String, path: String) {
- context?.removeKey(folder, path)
- }
-
- fun removeKey(path: String) {
- context?.removeKey(path)
- }
-
- /** If fallbackWebView is true and a fragment is supplied then it will open a WebView with the URL if the browser fails. */
- fun openBrowser(url: String, fallbackWebView: Boolean = false, fragment: Fragment? = null) {
- context?.openBrowser(url, fallbackWebView, fragment)
- }
-
- /** Will fall back to WebView if in TV or emulator layout. */
- fun openBrowser(url: String, activity: FragmentActivity?) {
- openBrowser(
- url,
- isLayout(TV or EMULATOR),
- activity?.supportFragmentManager?.fragments?.lastOrNull()
- )
- }
- }
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index 4ce09bd44..50e6d8c98 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -1,16 +1,13 @@
package com.lagradost.cloudstream3
-import android.annotation.SuppressLint
+import android.Manifest
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
-import android.Manifest
import android.os.Build
-import android.os.Handler
-import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
@@ -27,41 +24,30 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.view.children
-import androidx.core.view.isNotEmpty
import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession
import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.syncproviders.AccountManager
-import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter
-import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
-import com.lagradost.cloudstream3.ui.player.PlayerPipHelper.isPIPPossible
-import com.lagradost.cloudstream3.ui.player.Torrent
-import com.lagradost.cloudstream3.ui.result.ActorAdaptor
-import com.lagradost.cloudstream3.ui.result.EpisodeAdapter
-import com.lagradost.cloudstream3.ui.result.ImageAdapter
-import com.lagradost.cloudstream3.ui.search.SearchAdapter
-import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
-import com.lagradost.cloudstream3.ui.settings.Globals.TV
+import com.lagradost.cloudstream3.ui.player.PlayerEventType
+import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
-import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
-import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Event
-import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
+import com.lagradost.cloudstream3.utils.UIHelper
+import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
+import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx
-import com.lagradost.cloudstream3.utils.UiText
+import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
-import org.schabi.newpipe.extractor.NewPipe
enum class FocusDirection {
Start,
@@ -101,24 +87,17 @@ object CommonActivity {
get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
- val screenWidthWithOrientation: Int
- get() {
- return displayMetrics.widthPixels
- }
- val screenHeightWithOrientation: Int
- get() {
- return displayMetrics.heightPixels
- }
- var isPipDesired: Boolean = false
+
+ var canEnterPipMode: Boolean = false
+ var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
val onColorSelectedEvent = Event>()
val onDialogDismissedEvent = Event()
+ var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair) -> Boolean)? = null
- var appliedTheme: Int = 0
- var appliedColor: Int = 0
private var currentToast: Toast? = null
@@ -185,41 +164,27 @@ object CommonActivity {
val toast = Toast(act)
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
- @Suppress("DEPRECATION")
- toast.view =
- binding.root // FIXME Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
+ toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
currentToast = toast
toast.show()
- val handler = Handler(Looper.getMainLooper())
- val ref = WeakReference(toast)
-
- /* Clean up activity leak */
- handler.postDelayed({
- if (ref.get() == currentToast) {
- currentToast = null
- }
- }, 10_000)
-
} catch (e: Exception) {
logError(e)
}
}
/**
- * Set locale
- * @param languageTag shall a IETF BCP 47 conformant tag.
- * Check [com.lagradost.cloudstream3.utils.SubtitleHelper].
- *
- * See locales on:
- * https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-core/availableLocales.json
- * https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
- * https://android.googlesource.com/platform/frameworks/base/+/android-16.0.0_r2/core/res/res/values/locale_config.xml
- * https://iso639-3.sil.org/code_tables/639/data/all
- */
- fun setLocale(context: Context?, languageTag: String?) {
- if (context == null || languageTag == null) return
- val locale = Locale.forLanguageTag(languageTag)
+ * Not all languages can be fetched from locale with a code.
+ * This map allows sidestepping the default Locale(languageCode)
+ * when setting the app language.
+ **/
+ val appLanguageExceptions = hashMapOf(
+ "zh-rTW" to Locale.TRADITIONAL_CHINESE
+ )
+
+ fun setLocale(context: Context?, languageCode: String?) {
+ if (context == null || languageCode == null) return
+ val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
val resources: Resources = context.resources
val config = resources.configuration
Locale.setDefault(locale)
@@ -227,12 +192,7 @@ object CommonActivity {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
context.createConfigurationContext(config)
-
- @Suppress("DEPRECATION")
- resources.updateConfiguration(
- config,
- resources.displayMetrics
- ) // FIXME this should be replaced
+ resources.updateConfiguration(config, resources.displayMetrics)
}
fun Context.updateLocale() {
@@ -243,27 +203,30 @@ object CommonActivity {
fun init(act: Activity) {
setActivityInstance(act)
- ioSafe { Torrent.deleteAllFiles() }
+
val componentActivity = activity as? ComponentActivity ?: return
+ //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
+ //https://developer.android.com/guide/topics/ui/picture-in-picture
+ canShowPipMode =
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
+ componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
+ componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
+
componentActivity.updateLocale()
componentActivity.updateTv()
- AccountManager.initMainAPI()
NewPipe.init(DownloaderTestImpl.getInstance())
- MainActivity.activityResultLauncher =
- componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- if (result.resultCode == AppCompatActivity.RESULT_OK) {
- val actionUid =
- getKey("last_click_action") ?: return@registerForActivityResult
- Log.d(TAG, "Loading action $actionUid result handler")
- val action = VideoClickActionHolder.getByUniqueId(actionUid) as? OpenInAppAction
- ?: return@registerForActivityResult
- action.onResultSafe(act, result.data)
- removeKey("last_click_action")
- removeKey("last_opened")
- }
+ MainActivity.activityResultLauncher = componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ if (result.resultCode == AppCompatActivity.RESULT_OK) {
+ val actionUid = getKey("last_click_action") ?: return@registerForActivityResult
+ Log.d(TAG, "Loading action $actionUid result handler")
+ val action = VideoClickActionHolder.getByUniqueId(actionUid) as? OpenInAppAction ?: return@registerForActivityResult
+ action.onResult(act, result.data)
+ removeKey("last_click_action")
+ removeKey("last_opened_id")
}
+ }
// Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
@@ -283,22 +246,17 @@ object CommonActivity {
}
}
- /** Enters pip mode if it is both possible and desired to do so*/
private fun Activity.enterPIPMode() {
- if (!isPipDesired || !this.isPIPPossible()) return
-
+ if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try {
enterPictureInPictureMode(PictureInPictureParams.Builder().build())
- } catch (_: Exception) {
- // Use fallback just in case
- @Suppress("DEPRECATION")
+ } catch (e: Exception) {
enterPictureInPictureMode()
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- @Suppress("DEPRECATION")
enterPictureInPictureMode()
}
}
@@ -307,18 +265,17 @@ object CommonActivity {
}
}
- fun onUserLeaveHint(act: Activity) {
- // On Android 12 and later we use setAutoEnterEnabled() instead.
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) return
- act.enterPIPMode()
+ fun onUserLeaveHint(act: Activity?) {
+ if (canEnterPipMode && canShowPipMode) {
+ act?.enterPIPMode()
+ }
}
fun updateTheme(act: Activity) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
if (settingsManager
- .getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
- ) {
+ .getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
loadThemes(act)
}
}
@@ -350,10 +307,6 @@ object CommonActivity {
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.MonetMode else R.style.AppTheme
- "Dracula" -> R.style.DraculaMode
- "Lavender" -> R.style.LavenderMode
- "SilentBlue" -> R.style.SilentBlueMode
-
else -> R.style.AppTheme
}
@@ -386,13 +339,9 @@ object CommonActivity {
else -> R.style.OverlayPrimaryColorNormal
}
-
act.theme.applyStyle(currentTheme, true)
act.theme.applyStyle(currentOverlayTheme, true)
- appliedTheme = currentTheme
- appliedColor = currentOverlayTheme
- act.updateTv()
- if (isLayout(TV)) act.theme.applyStyle(R.style.AppThemeTvOverlay, true)
+
act.theme.applyStyle(
R.style.LoadedStyle,
true
@@ -423,7 +372,8 @@ object CommonActivity {
private fun View.hasContent(): Boolean {
return isShown && when (this) {
- is ViewGroup -> this.isNotEmpty()
+ //is RecyclerView -> this.childCount > 0
+ is ViewGroup -> this.childCount > 0
else -> true
}
}
@@ -453,7 +403,7 @@ object CommonActivity {
// if cant focus but visible then break and let android decide
// the exception if is the view is a parent and has children that wants focus
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
- parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.isNotEmpty()
+ parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
} ?: false
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
@@ -531,8 +481,84 @@ object CommonActivity {
}
- fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?): Boolean? {
- return null
+ fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
+
+ // 149 keycode_numpad 5
+ when (keyCode) {
+ KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
+ PlayerEventType.SeekForward
+ }
+
+ KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
+ PlayerEventType.SeekBack
+ }
+
+ KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
+ PlayerEventType.NextEpisode
+ }
+
+ KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
+ PlayerEventType.PrevEpisode
+ }
+
+ KeyEvent.KEYCODE_MEDIA_PAUSE -> {
+ PlayerEventType.Pause
+ }
+
+ KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
+ PlayerEventType.Play
+ }
+
+ KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
+ PlayerEventType.Lock
+ }
+
+ KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
+ PlayerEventType.ToggleHide
+ }
+
+ KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
+ PlayerEventType.ToggleMute
+ }
+
+ KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
+ PlayerEventType.ShowMirrors
+ }
+ // OpenSubtitles shortcut
+ KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
+ PlayerEventType.SearchSubtitlesOnline
+ }
+
+ KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
+ PlayerEventType.ShowSpeed
+ }
+
+ KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
+ PlayerEventType.Resize
+ }
+
+ KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
+ PlayerEventType.SkipOp
+ }
+
+ KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
+ PlayerEventType.SkipCurrentChapter
+ }
+
+ KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
+ PlayerEventType.PlayPauseToggle
+ }
+
+ else -> null
+ }?.let { playerEvent ->
+ playerEventListener?.invoke(playerEvent)
+ }
+
+ //when (keyCode) {
+ // KeyEvent.KEYCODE_DPAD_CENTER -> {
+ // println("DPAD PRESSED")
+ // }
+ //}
}
/** overrides focus and custom key events */
@@ -569,7 +595,6 @@ object CommonActivity {
else -> null
}
-
// println("NEXT FOCUS : $nextView")
if (nextView != null) {
nextView.requestFocus()
@@ -577,15 +602,10 @@ object CommonActivity {
return true
}
- // TODO: Figure out why removing the check for SearchAutoComplete seems
- // to break focus on TV as it shouldn't need to be used.
- // Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote)
- // send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button.
- @SuppressLint("RestrictedApi")
- if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) &&
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) {
- showInputMethod(act.currentFocus?.findFocus())
+ UIHelper.showInputMethod(act.currentFocus?.findFocus())
}
//println("Keycode: $keyCode")
@@ -594,6 +614,7 @@ object CommonActivity {
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
+
}
// if someone else want to override the focus then don't handle the event as it is already
diff --git a/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt
new file mode 100644
index 000000000..045a7963a
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt
@@ -0,0 +1,11 @@
+package com.lagradost.cloudstream3
+
+import android.view.LayoutInflater
+import androidx.annotation.LayoutRes
+import androidx.recyclerview.widget.RecyclerView
+import com.lagradost.cloudstream3.ui.HeaderViewDecoration
+
+fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
+ val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
+ view.addItemDecoration(HeaderViewDecoration(headerView))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 90583011d..fa54545cf 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -1,38 +1,36 @@
package com.lagradost.cloudstream3
import android.animation.ValueAnimator
-import android.annotation.SuppressLint
-import android.app.Dialog
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Rect
+import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
-import android.view.Gravity
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
-import android.widget.CheckBox
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
-import androidx.core.content.edit
-import androidx.core.net.toUri
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.view.GravityCompat
import androidx.core.view.children
-import androidx.core.view.get
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
@@ -50,7 +48,6 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
-import androidx.viewpager2.widget.ViewPager2
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.Session
import com.google.android.gms.cast.framework.SessionManager
@@ -64,9 +61,10 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.initAll
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
+import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
@@ -76,34 +74,35 @@ import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.CommonActivity.updateTheme
-import com.lagradost.cloudstream3.actions.temp.fcast.FcastManager
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.safe
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
-import com.lagradost.cloudstream3.plugins.PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins
+import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
-import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SHARE
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
+import com.lagradost.cloudstream3.ui.account.AccountViewModel
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeViewModel
import com.lagradost.cloudstream3.ui.library.LibraryViewModel
@@ -114,12 +113,15 @@ import com.lagradost.cloudstream3.ui.result.LinearListLayout
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.SyncViewModel
+import com.lagradost.cloudstream3.ui.result.setImage
+import com.lagradost.cloudstream3.ui.result.setText
+import com.lagradost.cloudstream3.ui.result.setTextHtml
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
-import com.lagradost.cloudstream3.ui.settings.Globals.isLandscape
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
@@ -156,27 +158,22 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
-import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
-import com.lagradost.cloudstream3.utils.InAppUpdater.runAutoUpdate
+import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
-import com.lagradost.cloudstream3.utils.TvChannelUtils
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
+import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
-import com.lagradost.cloudstream3.utils.UIHelper.enableEdgeToEdgeCompat
-import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
-import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat
+import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
-import com.lagradost.cloudstream3.utils.setText
-import com.lagradost.cloudstream3.utils.setTextHtml
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.actions.temp.fcast.FcastManager
import com.lagradost.safefile.SafeFile
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -188,9 +185,6 @@ import java.nio.charset.Charset
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.system.exitProcess
-import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
companion object {
@@ -200,23 +194,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
const val ANIMATED_OUTLINE: Boolean = false
var lastError: String? = null
- /** Update lastError variable based on error file, to check if app crashed.
- * Can be called multiple times without changing the lastError variable changing.
- **/
- fun setLastError(context: Context) {
- if (lastError != null) return
-
- val errorFile = context.filesDir.resolve("last_error")
- if (errorFile.exists() && errorFile.isFile) {
- lastError = errorFile.readText(Charset.defaultCharset())
- errorFile.delete()
- } else {
- lastError = null
- }
- }
-
private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY"
- const val API_NAME_EXTRA_KEY = "API_NAME_EXTRA_KEY"
/**
* Transient files to delete on application exit.
@@ -279,14 +257,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
fun handleAppIntentUrl(
activity: FragmentActivity?,
str: String?,
- isWebview: Boolean,
- extraArgs: Bundle? = null
+ isWebview: Boolean
): Boolean =
with(activity) {
// TODO MUCH BETTER HANDLING
// Invalid URIs can crash
- fun safeURI(uri: String) = safe { URI(uri) }
+ fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
@@ -295,29 +272,28 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
loadRepository(realUrl)
return true
} else if (str.contains(APP_STRING)) {
- for (api in AccountManager.allApis) {
- if (api.isValidRedirectUrl(str)) {
+ for (api in OAuth2Apis) {
+ if (str.contains("/${api.redirectUrl}")) {
ioSafe {
Log.i(TAG, "handleAppIntent $str")
- try {
- val isSuccessful = api.login(str)
- if (isSuccessful) {
- Log.i(TAG, "authenticated ${api.name}")
- } else {
- Log.i(TAG, "failed to authenticate ${api.name}")
+ val isSuccessful = api.handleRedirect(str)
+
+ if (isSuccessful) {
+ Log.i(TAG, "authenticated ${api.name}")
+ } else {
+ Log.i(TAG, "failed to authenticate ${api.name}")
+ }
+
+ this@with.runOnUiThread {
+ try {
+ showToast(
+ getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
+ api.name
+ )
+ )
+ } catch (e: Exception) {
+ logError(e) // format might fail
}
- showToast(
- if (isSuccessful) {
- txt(R.string.authenticated_user, api.name)
- } else {
- txt(R.string.authenticated_user_fail, api.name)
- }
- )
- } catch (t: Throwable) {
- logError(t)
- showToast(
- txt(R.string.authenticated_user_fail, api.name)
- )
}
}
return true
@@ -326,11 +302,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
// This specific intent is used for the gradle deployWithAdb
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
if (str == "$APP_STRING:") {
- ioSafe {
- PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_hotReloadAllLocalPlugins(
- activity
- )
- }
+ PluginManager.hotReloadAllLocalPlugins(activity)
}
} else if (safeURI(str)?.scheme == APP_STRING_REPO) {
val url = str.replaceFirst(APP_STRING_REPO, "https")
@@ -352,7 +324,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
activity?.findViewById(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search
} else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
- val uri = str.toUri()
+ val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8")
@@ -362,8 +334,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
LinkGenerator(
listOf(BasicLink(url, name)),
extract = true,
- id = url.hashCode()
- ), 0
+ )
)
)
} else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
@@ -379,68 +350,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
START_ACTION_RESUME_LATEST
)
}
- } else if (str.startsWith(APP_STRING_SHARE)) {
- try {
- val data = str.substringAfter("$APP_STRING_SHARE:")
- val parts = data.split("?", limit = 2)
- loadResult(
- String(base64DecodeArray(parts[1]), Charsets.UTF_8),
- String(base64DecodeArray(parts[0]), Charsets.UTF_8),
- ""
- )
- return true
- } catch (e: Exception) {
- showToast("Invalid Uri", Toast.LENGTH_SHORT)
- return false
- }
} else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
return true
} else {
- val apiName = extraArgs?.getString(API_NAME_EXTRA_KEY)
- ?.takeIf { it.isNotBlank() }
- // if provided, try to match the api name instead of the api url
- // this is in order to also support providers that use JSON dataUrls
- // for example
- if (apiName != null) {
- loadResult(str, apiName, "")
- return true
- }
-
- val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull()
- if (matchedApi != null) {
- loadResult(str, matchedApi.name, "")
- return true
+ synchronized(apis) {
+ for (api in apis) {
+ if (str.startsWith(api.mainUrl)) {
+ loadResult(str, api.name)
+ return true
+ }
+ }
}
}
}
}
return false
}
-
-
- fun centerView(view: View?) {
- if (view == null) return
- try {
- Log.v(TAG, "centerView: $view")
- val r = Rect(0, 0, 0, 0)
- view.getDrawingRect(r)
- val x = r.centerX()
- val y = r.centerY()
- val dx = r.width() / 2 //screenWidth / 2
- val dy = screenHeight / 2
- val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
- view.requestRectangleOnScreen(r2, false)
- // TvFocus.current =TvFocus.current.copy(y=y.toFloat())
- } catch (_: Throwable) {
- }
- }
}
-
var lastPopup: SearchResponse? = null
- var lastPopupJob: Job? = null
fun loadPopup(result: SearchResponse, load: Boolean = true) {
lastPopup = result
val syncName = syncViewModel.syncName(result.apiName)
@@ -456,8 +386,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
syncViewModel.clear()
}
- lastPopupJob?.cancel()
- lastPopupJob = if (load) {
+ if (load) {
viewModel.load(
this, result.url, result.apiName, false, if (getApiDubstatusSettings()
.contains(DubStatus.Dubbed)
@@ -504,7 +433,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_downloads,
R.id.navigation_settings,
R.id.navigation_download_child,
- R.id.navigation_download_queue,
R.id.navigation_subtitles,
R.id.navigation_chrome_subtitles,
R.id.navigation_settings_player,
@@ -519,7 +447,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
).contains(destination.id)
- /*val dontPush = listOf(
+ val dontPush = listOf(
R.id.navigation_home,
R.id.navigation_search,
R.id.navigation_results_phone,
@@ -550,19 +478,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
layoutParams = params
- }*/
+ }
+
+ val landscape = when (resources.configuration.orientation) {
+ Configuration.ORIENTATION_LANDSCAPE -> {
+ true
+ }
+
+ Configuration.ORIENTATION_PORTRAIT -> {
+ isLayout(TV or EMULATOR)
+ }
+
+ else -> {
+ false
+ }
+ }
binding?.apply {
- navRailView.isVisible = isNavVisible && isLandscape()
- navView.isVisible = isNavVisible && !isLandscape()
- navHostFragment.apply {
- val marginPx = resources.getDimensionPixelSize(R.dimen.nav_rail_view_width)
- layoutParams =
- (navHostFragment.layoutParams as ViewGroup.MarginLayoutParams).apply {
- marginStart =
- if (isNavVisible && isLandscape() && isLayout(TV or EMULATOR)) marginPx else 0
- }
- }
+ navRailView.isVisible = isNavVisible && landscape
+ navView.isVisible = isNavVisible && !landscape
/**
* We need to make sure if we return to a sub-fragment,
@@ -570,15 +504,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
* highlight the wrong one in UI.
*/
when (destination.id) {
- in listOf(
- R.id.navigation_downloads,
- R.id.navigation_download_child,
- R.id.navigation_download_queue
- ) -> {
+ in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> {
navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true
navView.menu.findItem(R.id.navigation_downloads).isChecked = true
}
-
in listOf(
R.id.navigation_settings,
R.id.navigation_subtitles,
@@ -665,11 +594,18 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- override fun dispatchKeyEvent(event: KeyEvent): Boolean =
- CommonActivity.dispatchKeyEvent(this, event) ?: super.dispatchKeyEvent(event)
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ val response = CommonActivity.dispatchKeyEvent(this, event)
+ if (response != null)
+ return response
+ return super.dispatchKeyEvent(event)
+ }
- override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean =
- CommonActivity.onKeyDown(this, keyCode, event) ?: super.onKeyDown(keyCode, event)
+ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
+ CommonActivity.onKeyDown(this, keyCode, event)
+
+ return super.onKeyDown(keyCode, event)
+ }
override fun onUserLeaveHint() {
@@ -677,34 +613,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
onUserLeaveHint(this)
}
- @SuppressLint("ApplySharedPref") // commit since the op needs to be synchronous
- private fun showConfirmExitDialog(settingsManager: SharedPreferences) {
- val confirmBeforeExit = settingsManager.getInt(getString(R.string.confirm_exit_key), -1)
-
- if (confirmBeforeExit == 1 || (confirmBeforeExit == -1 && isLayout(PHONE))) {
- // finish() causes a bug on some TVs where player
- // may keep playing after closing the app.
- if (isLayout(TV)) exitProcess(0) else finish()
- return
- }
-
- val dialogView = layoutInflater.inflate(R.layout.confirm_exit_dialog, null)
- val dontShowAgainCheck: CheckBox = dialogView.findViewById(R.id.checkboxDontShowAgain)
+ private fun showConfirmExitDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
- builder.setView(dialogView)
- .setTitle(R.string.confirm_exit_dialog)
- .setNegativeButton(R.string.no) { _, _ -> /*NO-OP*/ }
- .setPositiveButton(R.string.yes) { _, _ ->
- if (dontShowAgainCheck.isChecked) {
- settingsManager.edit(commit = true) {
- putInt(getString(R.string.confirm_exit_key), 1)
- }
- }
- // finish() causes a bug on some TVs where player
- // may keep playing after closing the app.
- if (isLayout(TV)) exitProcess(0) else finish()
- }
-
+ builder.setTitle(R.string.confirm_exit_dialog)
+ builder.apply {
+ // Forceful exit since back button can actually go back to setup
+ setPositiveButton(R.string.yes) { _, _ -> exitProcess(0) }
+ setNegativeButton(R.string.no) { _, _ -> }
+ }
builder.show().setDefaultFocus()
}
@@ -723,11 +639,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
this.sendBroadcast(broadcastIntent)
afterPluginsLoadedEvent -= ::onAllPluginsLoaded
- detachBackPressedCallback("MainActivityDefault")
super.onDestroy()
}
- override fun onNewIntent(intent: Intent) {
+ override fun onNewIntent(intent: Intent?) {
handleAppIntent(intent)
super.onNewIntent(intent)
}
@@ -736,55 +651,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (intent == null) return
val str = intent.dataString
loadCache()
-
- handleAppIntentUrl(this, str, false, intent.extras)
+ handleAppIntentUrl(this, str, false)
}
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
- private var lastNavTime = 0L
private fun onNavDestinationSelected(item: MenuItem, navController: NavController): Boolean {
- val currentTime = System.currentTimeMillis()
- // safeDebounce: Check if a previous tap happened within the last 400ms
- if (currentTime - lastNavTime < 400) return false
- lastNavTime = currentTime
-
- val destinationId = item.itemId
-
- // Check if we are already at the selected destination
- if (navController.currentDestination?.id == destinationId) return false
-
- // Make all nav buttons focus on this specific view when nextFocusRightId
- val targetView = when (destinationId) {
- // Please note that if R.id.navigation_home is readded, then it will only take affect when
- // navigation to home for the second time as onNavDestinationSelected will not get called
- // when first loading up the app
-
- // R.id.navigation_home -> R.id.home_preview_change_api
- R.id.navigation_search -> R.id.main_search
- R.id.navigation_library -> R.id.main_search
- R.id.navigation_downloads -> R.id.download_appbar
- else -> null
- }
- if (targetView != null && isLayout(TV or EMULATOR)) {
- val fromView = binding?.navRailView
- if (fromView != null) {
- fromView.nextFocusRightId = targetView
-
- for (focusView in arrayOf(
- R.id.navigation_downloads,
- R.id.navigation_home,
- R.id.navigation_search,
- R.id.navigation_library,
- R.id.navigation_settings,
- )) {
- fromView.findViewById(focusView)?.nextFocusRightId = targetView
- }
- }
- }
-
-
val builder = NavOptions.Builder().setLaunchSingleTop(true).setRestoreState(true)
.setEnterAnim(R.anim.enter_anim)
.setExitAnim(R.anim.exit_anim)
@@ -797,11 +670,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
saveState = true
)
}
+ val options = builder.build()
return try {
- navController.navigate(destinationId, null, builder.build())
- navController.currentDestination?.matchDestination(destinationId) == true
+ navController.navigate(item.itemId, null, options)
+ navController.currentDestination?.matchDestination(item.itemId) == true
} catch (e: IllegalArgumentException) {
- Log.e("NavigationError", "Failed to navigate: ${e.message}")
false
}
}
@@ -810,21 +683,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
- allProviders.withLock {
+ synchronized(allProviders) {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey>(USER_PROVIDER_API)?.let { list ->
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
- allProviders.add(
- it.javaClass.getDeclaredConstructor().newInstance()
- .apply {
- name = custom.name
- lang = custom.lang
- mainUrl = custom.url.trimEnd('/')
- canBeOverridden = false
- })
+ allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
+ name = custom.name
+ lang = custom.lang
+ mainUrl = custom.url.trimEnd('/')
+ canBeOverridden = false
+ })
}
}
}
@@ -843,6 +714,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
lateinit var viewModel: ResultViewModel2
lateinit var syncViewModel: SyncViewModel
private var libraryViewModel: LibraryViewModel? = null
+ private var accountViewModel: AccountViewModel? = null
/** kinda dirty, however it signals that we should use the watch status as sync or not*/
var isLocalList: Boolean = false
@@ -856,37 +728,20 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
private fun hidePreviewPopupDialog() {
bottomPreviewPopup.dismissSafe(this)
- lastPopupJob?.cancel()
- lastPopupJob = null
bottomPreviewPopup = null
bottomPreviewBinding = null
}
- private var bottomPreviewPopup: Dialog? = null
+ private var bottomPreviewPopup: BottomSheetDialog? = null
private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
val ret = (bottomPreviewBinding ?: run {
-
- val builder: Dialog
- val layout: Int
-
- if (isLayout(PHONE)) {
- builder =
- BottomSheetDialog(this)
- layout = R.layout.bottom_resultview_preview
- } else {
- builder =
- Dialog(this, R.style.DialogHalfFullscreen)
- layout = R.layout.bottom_resultview_preview_tv
- // No way to do this in styles :(
- builder.window?.setGravity(Gravity.CENTER_VERTICAL or Gravity.END)
- }
-
- val root = layoutInflater.inflate(layout, null, false)
- val binding = BottomResultviewPreviewBinding.bind(root)
-
+ val builder =
+ BottomSheetDialog(this)
+ val binding: BottomResultviewPreviewBinding =
+ BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false)
bottomPreviewBinding = binding
- builder.setContentView(root)
+ builder.setContentView(binding.root)
builder.setOnDismissListener {
bottomPreviewPopup = null
bottomPreviewBinding = null
@@ -1177,14 +1032,34 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- override fun onCreate(savedInstanceState: Bundle?) {
- app.initClient(this, ignoreSSL = false)
- @OptIn(UnsafeSSL::class)
- insecureApp.initClient(this, ignoreSSL = true)
+ private fun centerView(view: View?) {
+ if (view == null) return
+ try {
+ Log.v(TAG, "centerView: $view")
+ val r = Rect(0, 0, 0, 0)
+ view.getDrawingRect(r)
+ val x = r.centerX()
+ val y = r.centerY()
+ val dx = r.width() / 2 //screenWidth / 2
+ val dy = screenHeight / 2
+ val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
+ view.requestRectangleOnScreen(r2, false)
+ // TvFocus.current =TvFocus.current.copy(y=y.toFloat())
+ } catch (_: Throwable) {
+ }
+ }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
- setLastError(this)
+ val errorFile = filesDir.resolve("last_error")
+ if (errorFile.exists() && errorFile.isFile) {
+ lastError = errorFile.readText(Charset.defaultCharset())
+ errorFile.delete()
+ } else {
+ lastError = null
+ }
val settingsForProvider = SettingsJson()
settingsForProvider.enableAdult =
@@ -1193,14 +1068,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
MainAPI.settingsForProvider = settingsForProvider
loadThemes(this)
- enableEdgeToEdgeCompat()
- setNavigationBarColorCompat(R.attr.primaryGrayBackground)
updateLocale()
super.onCreate(savedInstanceState)
try {
if (isCastApiAvailable()) {
- CastContext.getSharedInstance(this) { it.run() }
- .addOnSuccessListener { mSessionManager = it.sessionManager }
+ CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
}
} catch (t: Throwable) {
logError(t)
@@ -1210,17 +1082,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
updateTv()
// backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
- safe {
+ normalSafeApiCall {
val appVer = BuildConfig.VERSION_NAME
val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
- if (lastAppAutoBackup.isEmpty()) return@safe
-
- safe {
+ normalSafeApiCall {
backup(this)
}
- safe {
+ normalSafeApiCall {
// Recompile oat on new version
PluginManager.deleteAllOatFiles(this)
}
@@ -1248,7 +1118,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (isLayout(TV)) {
// Put here any button you don't want focusing it to center the view
val exceptionButtons = listOf(
- //R.id.home_preview_play_btt,
+ R.id.home_preview_play_btt,
R.id.home_preview_info_btt,
R.id.home_preview_hidden_next_focus,
R.id.home_preview_hidden_prev_focus,
@@ -1280,26 +1150,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
null
}
- binding?.apply {
- fixSystemBarsPadding(
- navView,
- heightResId = R.dimen.nav_view_height,
- padTop = false,
- overlayCutout = false
- )
-
- fixSystemBarsPadding(
- navRailView,
- widthResId = R.dimen.nav_rail_view_width,
- padRight = false,
- padTop = false
- )
- }
-
- // overscan
- val padding = settingsManager.getInt(getString(R.string.overscan_key), 0).toPx
- binding?.homeRoot?.setPadding(padding, padding, padding, padding)
-
changeStatusBarState(isLayout(EMULATOR))
/** Biometric stuff for users without accounts **/
@@ -1341,7 +1191,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe { SafeFile.check(this@MainActivity) }
if (PluginManager.checkSafeModeFile()) {
- safe {
+ normalSafeApiCall {
showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
@@ -1358,11 +1208,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
true
)
) {
- PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_updateAllOnlinePluginsAndLoadThem(
- this@MainActivity
- )
+ PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else {
- ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(this@MainActivity)
+ loadAllOnlinePlugins(this@MainActivity)
}
//Automatically download not existing plugins, using mode specified.
@@ -1373,7 +1221,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
)
) ?: AutoDownloadMode.Disable
if (autoDownloadPlugin != AutoDownloadMode.Disable) {
- PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_downloadNotExistingPluginsAndLoad(
+ PluginManager.downloadNotExistingPluginsAndLoad(
this@MainActivity,
autoDownloadPlugin
)
@@ -1381,14 +1229,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
ioSafe {
- PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(
- this@MainActivity,
- false
- )
+ PluginManager.loadAllLocalPlugins(this@MainActivity, false)
}
-
-// Add your channel creation here
-
}
} else {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
@@ -1513,17 +1355,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
resultviewPreviewMetaRating.setText(d.ratingText)
resultviewPreviewDescription.setTextHtml(d.plotText)
- if (isLayout(PHONE)) {
- resultviewPreviewPoster.loadImage(
- d.posterImage ?: d.posterBackgroundImage,
- headers = d.posterHeaders
- )
- } else {
- resultviewPreviewPoster.loadImage(
- d.posterBackgroundImage ?: d.posterImage,
- headers = d.posterHeaders
- )
- }
+ resultviewPreviewPoster.setImage(
+ d.posterImage ?: d.posterBackgroundImage
+ )
setUserData(syncViewModel.userData.value)
setWatchStatus(viewModel.watchStatus.value)
@@ -1626,6 +1460,18 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
// init accounts
ioSafe {
+ for (api in accountManagers) {
+ api.init()
+ }
+
+ inAppAuths.amap { api ->
+ try {
+ api.initialize()
+ } catch (e: Exception) {
+ logError(e)
+ }
+ }
+
// we need to run this after we init all apis, otherwise currentSyncApi will fuck itself
this@MainActivity.runOnUiThread {
// Change library icon with logo of current api in sync
@@ -1653,7 +1499,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
- apis = allProviders.distinctBy { it }
+ apis = synchronized(allProviders) {
+ allProviders.distinctBy { it }
+ }
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@@ -1673,11 +1521,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- if (navDestination.matchDestination(R.id.navigation_home)) {
- attachBackPressedCallback("MainActivity") {
- showConfirmExitDialog(settingsManager)
- }
- } else detachBackPressedCallback("MainActivity")
+ if (isLayout(TV or EMULATOR)) {
+ if (navDestination.matchDestination(R.id.navigation_home)) {
+ attachBackPressedCallback {
+ showConfirmExitDialog()
+ window?.navigationBarColor =
+ colorFromAttribute(R.attr.primaryGrayBackground)
+ updateLocale()
+ }
+ } else detachBackPressedCallback()
+ }
}
//val navController = findNavController(R.id.nav_host_fragment)
@@ -1703,27 +1556,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
navController
)
}
-
}
binding?.navRailView?.apply {
- if (isLayout(PHONE)) {
- itemRippleColor = rippleColor
- itemActiveIndicatorColor = rippleColor
- } else {
- val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.textColor, 1.0f))
- val rippleColorTransparent =
- ColorStateList.valueOf(getResourceColor(R.attr.textColor, 0.2f))
- itemSpacing = 12.toPx // expandedItemSpacing does not have an attr
- itemRippleColor = rippleColorTransparent
- itemActiveIndicatorColor = rippleColor
- }
+ itemRippleColor = rippleColor
+ itemActiveIndicatorColor = rippleColor
setupWithNavController(navController)
- /*if (isLayout(TV or EMULATOR)) {
+ if (isLayout(TV or EMULATOR)) {
background?.alpha = 200
} else {
background?.alpha = 255
- }*/
+ }
setOnItemSelectedListener { item ->
onNavDestinationSelected(
@@ -1732,7 +1575,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
)
}
-
fun noFocus(view: View) {
view.tag = view.context.getString(R.string.tv_no_focus_tag)
(view as? ViewGroup)?.let {
@@ -1758,7 +1600,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
observe(homeViewModel.currentAccount) { currentAccount ->
if (currentAccount != null) {
- navProfilePic?.loadImage(
+ navProfilePic?.setImage(
currentAccount.image
)
navProfileRoot.isVisible = true
@@ -1771,104 +1613,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- val rail = binding?.navRailView
- if (rail != null) {
- binding?.navRailView?.labelVisibilityMode =
- NavigationRailView.LABEL_VISIBILITY_UNLABELED
- //val focus = mutableSetOf()
-
- var prevId: Int? = null
- var prevView: View? = null
-
- // The genius engineers at google did not actually
- // write a nextFocus for the navrail
- rail.findViewById(R.id.navigation_settings)?.nextFocusDownId =
- R.id.nav_footer_profile_card
- for (id in arrayOf(
- R.id.navigation_home,
- R.id.navigation_search,
- R.id.navigation_library,
- R.id.navigation_downloads,
- R.id.navigation_settings
- )) {
- val view = rail.findViewById(id) ?: continue
- prevId?.let { view.nextFocusUpId = it }
- prevView?.nextFocusDownId = id
-
- prevView = view
- prevId = id
- // Uncomment for focus expand
- /*if (!isLayout(TV)) {
- view.onFocusChangeListener = null
- } else {
- view.onFocusChangeListener =
- View.OnFocusChangeListener { v, hasFocus ->
- if (hasFocus) {
- focus += id
- binding?.navRailView?.labelVisibilityMode =
- NavigationRailView.LABEL_VISIBILITY_LABELED
- binding?.navRailView?.expand()
- } else {
- focus -= id
- v.post {
- if (focus.isEmpty()) {
- binding?.navRailView?.labelVisibilityMode =
- NavigationRailView.LABEL_VISIBILITY_UNLABELED
- binding?.navRailView?.collapse()
- }
- }
- }
- }
- }*/
- }
- }
-
- // Navigation button long click functionality to scroll to top
- for (view in listOf(binding?.navView, binding?.navRailView)) {
- view?.findViewById(R.id.navigation_home)?.setOnLongClickListener {
- val recycler = binding?.root?.findViewById(R.id.home_master_recycler)
- recycler?.smoothScrollToPosition(0)
- return@setOnLongClickListener recycler != null
- }
-
- view?.findViewById(R.id.navigation_library)?.setOnLongClickListener {
- val viewPager = binding?.root?.findViewById(R.id.viewpager)
- ?: return@setOnLongClickListener false
- try {
- val children = (viewPager[0] as? RecyclerView)?.children
- ?: return@setOnLongClickListener false
- for (child in children) {
- child.findViewById(R.id.page_recyclerview)
- ?.smoothScrollToPosition(0)
- }
- } catch (_: IndexOutOfBoundsException) {
- } catch (t: Throwable) {
- logError(t)
- }
- return@setOnLongClickListener true
- }
-
- view?.findViewById(R.id.navigation_search)?.setOnLongClickListener {
- for (recyclerId in arrayOf(
- R.id.search_master_recycler,
- R.id.search_autofit_results,
- R.id.search_history_recycler
- )) {
- val recycler = binding?.root?.findViewById(recyclerId)
- ?: return@setOnLongClickListener false
- recycler.smoothScrollToPosition(0)
- }
- return@setOnLongClickListener true
- }
-
- view?.findViewById(R.id.navigation_downloads)?.setOnLongClickListener {
- val recycler: RecyclerView? = binding?.root?.findViewById(R.id.download_list)
- ?: binding?.root?.findViewById(R.id.download_child_list)
- recycler?.smoothScrollToPosition(0)
- return@setOnLongClickListener recycler != null
- }
- }
-
loadCache()
updateHasTrailers()
/*nav_view.setOnNavigationItemSelectedListener { item ->
@@ -1935,7 +1679,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
fun buildMediaQueueItem(video: String): MediaQueueItem {
// val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO)
//movieMetadata.putString(MediaMetadata.KEY_TITLE, "CloudStream")
- val mediaInfo = MediaInfo.Builder(video.toUri().toString())
+ val mediaInfo = MediaInfo.Builder(Uri.parse(video).toString())
.setStreamType(MediaInfo.STREAM_TYPE_NONE)
.setContentType(MimeTypes.IMAGE_JPEG)
// .setMetadata(movieMetadata).build()
@@ -1961,7 +1705,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
- allProviders.withLock {
+ synchronized(allProviders) {
for (api in allProviders) {
providersAndroidManifestString += "(USER_SELECTED_HOMEPAGE_API)?.let { homepage ->
DataStoreHelper.currentHomePage = homepage
removeKey(USER_SELECTED_HOMEPAGE_API)
@@ -2039,14 +1772,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
// }
// }
- attachBackPressedCallback("MainActivityDefault") {
- setNavigationBarColorCompat(R.attr.primaryGrayBackground)
- updateLocale()
- runDefault()
- }
+ onBackPressedDispatcher.addCallback(
+ this,
+ object : OnBackPressedCallback(true) {
+ override fun handleOnBackPressed() {
+ window?.navigationBarColor = colorFromAttribute(R.attr.primaryGrayBackground)
+ updateLocale()
- // Start the download queue
- DownloadQueueManager.init(this)
+ // If we don't disable we end up in a loop with default behavior calling
+ // this callback as well, so we disable it, run default behavior,
+ // then re-enable this callback so it can be used for next back press.
+ isEnabled = false
+ onBackPressedDispatcher.onBackPressed()
+ isEnabled = true
+ }
+ }
+ )
}
/** Biometric stuff **/
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/AlwaysAskAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/AlwaysAskAction.kt
deleted file mode 100644
index a3c4040b5..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/AlwaysAskAction.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.lagradost.cloudstream3.actions
-
-import android.content.Context
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
-
-class AlwaysAskAction : VideoClickAction() {
- override val name = txt(R.string.player_settings_always_ask)
- override val isPlayer = true
-
- // Only show in settings, not on a video
- override fun shouldShow(context: Context?, video: ResultEpisode?): Boolean = video == null
-
- override suspend fun runAction(
- context: Context?,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- // This is handled specially in ResultViewModel2.kt by detecting the AlwaysAskAction
- // and showing the player selection dialog instead of executing the action directly
- throw NotImplementedError("AlwaysAskAction is handled specially by the calling code")
- }
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/OpenInAppAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/OpenInAppAction.kt
index ac912cbeb..99c1ac38b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/OpenInAppAction.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/OpenInAppAction.kt
@@ -1,28 +1,34 @@
package com.lagradost.cloudstream3.actions
import android.app.Activity
+import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.widget.Toast
import androidx.core.content.FileProvider
import androidx.core.net.toUri
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
+import com.lagradost.cloudstream3.CommonActivity.showToast
+import com.lagradost.cloudstream3.MainActivity.Companion.activityResultLauncher
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.ResultFragment
-import com.lagradost.cloudstream3.utils.UiText
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.UiText
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppContextUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.DataStoreHelper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
import java.io.File
fun updateDurationAndPosition(position: Long, duration: Long) {
if (position <= 0 || duration <= 0) return
- val episode = getKey("last_opened") ?: return
- DataStoreHelper.setViewPosAndResume(episode.id, position, duration, episode, null)
+ DataStoreHelper.setViewPos(getKey("last_opened_id"), position, duration)
ResultFragment.updateUI()
}
@@ -33,13 +39,7 @@ fun updateDurationAndPosition(position: Long, duration: Long) {
fun makeTempM3U8Intent(
context: Context,
intent: Intent,
- result: LinkLoadingResult
-) {
- if (result.links.size == 1) {
- intent.setDataAndType(result.links.first().url.toUri(), "video/*")
- return
- }
-
+ result: LinkLoadingResult) {
intent.apply {
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
@@ -47,29 +47,37 @@ fun makeTempM3U8Intent(
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
- val outputFile = File.createTempFile("mirrorlist", ".m3u8", context.cacheDir)
- var text = "#EXTM3U\n#EXT-X-VERSION:3"
+ val outputDir = context.cacheDir
- result.links.forEach { link ->
- text += "\n#EXTINF:0,${link.name}\n${link.url}"
+ if (result.links.size == 1) {
+ intent.setDataAndType(result.links.first().url.toUri(), "video/*")
+ } else {
+ val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir)
+
+ var text = "#EXTM3U\n#EXT-X-VERSION:3"
+
+ result.links.forEachIndexed { index, link ->
+ text += "\n#EXTINF:$index,${link.name}\n${link.url}"
+ }
+
+ //With subtitles it doesn't work for no reason :(
+ /*for (sub in result.subs) {
+ val normalizedName = sub.name.replace("[^a-zA-Z0-9 ]".toRegex(), "")
+ text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${normalizedName}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.languageCode}\",URI=\"${sub.url}\""
+ }*/
+
+ text += "\n#EXT-X-ENDLIST"
+
+ outputFile.writeText(text)
+
+ intent.setDataAndType(
+ FileProvider.getUriForFile(
+ context,
+ context.applicationContext.packageName + ".provider",
+ outputFile
+ ), "application/x-mpegURL"
+ )
}
-
- //With subtitles it doesn't work for no reason :(
- /*for (sub in result.subs) {
- val normalizedName = sub.name.replace("[^a-zA-Z0-9 ]".toRegex(), "")
- text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${normalizedName}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.languageCode}\",URI=\"${sub.url}\""
- }*/
-
- text += "\n#EXT-X-ENDLIST"
- outputFile.writeText(text)
-
- intent.setDataAndType(
- FileProvider.getUriForFile(
- context,
- context.applicationContext.packageName + ".provider",
- outputFile
- ), "application/x-mpegURL"
- )
}
abstract class OpenInAppAction(
@@ -77,16 +85,15 @@ abstract class OpenInAppAction(
open val packageName: String,
private val intentClass: String? = null,
private val action: String = Intent.ACTION_VIEW
-) : VideoClickAction() {
+): VideoClickAction() {
override val name: UiText
get() = txt(R.string.episode_action_play_in_format, appName)
override val isPlayer = true
- override fun shouldShow(context: Context?, video: ResultEpisode?) =
- context?.isAppInstalled(packageName) != false
+ override fun shouldShow(context: Context?, video: ResultEpisode?) = context?.isAppInstalled(packageName) == true
- override suspend fun runAction(
+ override fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
@@ -99,37 +106,29 @@ abstract class OpenInAppAction(
intent.component = ComponentName(packageName, intentClass)
}
putExtra(context, intent, video, result, index)
- setKey("last_opened", video)
- launchResult(intent)
+ setKey("last_opened_id", video.id)
+ try {
+ CoroutineScope(Dispatchers.IO).launch {
+ activityResultLauncher?.launch(intent)
+ }
+ } catch (_: ActivityNotFoundException) {
+ showToast(R.string.app_not_found_error, Toast.LENGTH_LONG)
+ } catch (t: Throwable) {
+ logError(t)
+ showToast(t.toString(), Toast.LENGTH_LONG)
+ }
}
/**
* Before intent is sent, this function is called to put extra data into the intent.
* @see VideoClickAction.runAction
* */
- @Throws
- abstract suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- )
+ abstract fun putExtra(context: Context, intent: Intent, video: ResultEpisode, result: LinkLoadingResult, index: Int?)
/**
* This function is called when the app is opened again after the intent was sent.
* You can use it to for example update duration and position.
* @see updateDurationAndPosition
*/
- @Throws
abstract fun onResult(activity: Activity, intent: Intent?)
-
- /** Safe version of onResult, we don't trust extension devs to not crash the app */
- fun onResultSafe(activity: Activity, intent: Intent?) {
- try {
- onResult(activity, intent)
- } catch (t: Throwable) {
- logError(t)
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt
index a864b5fb7..f66ed74d9 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/VideoClickAction.kt
@@ -1,77 +1,32 @@
package com.lagradost.cloudstream3.actions
import android.app.Activity
-import android.content.ActivityNotFoundException
import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import android.widget.Toast
-import androidx.core.app.ActivityOptionsCompat
import com.lagradost.api.Log
-import com.lagradost.cloudstream3.CommonActivity
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.MainActivity
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.actions.temp.BiglyBTPackage
import com.lagradost.cloudstream3.actions.temp.CopyClipboardAction
-import com.lagradost.cloudstream3.actions.temp.JustPlayerPackage
-import com.lagradost.cloudstream3.actions.temp.LibreTorrentPackage
-import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage
-import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
-import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
-import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
-import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
-import com.lagradost.cloudstream3.actions.temp.VlcNightlyPackage
import com.lagradost.cloudstream3.actions.temp.VlcPackage
import com.lagradost.cloudstream3.actions.temp.WebVideoCastPackage
import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction
-import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
-import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
+import com.lagradost.cloudstream3.ui.result.UiText
+import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.UiText
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.util.concurrent.Callable
-import java.util.concurrent.FutureTask
import kotlin.reflect.jvm.jvmName
object VideoClickActionHolder {
- val allVideoClickActions = atomicListOf(
- // Default
- PlayInBrowserAction(),
- CopyClipboardAction(),
- ViewM3U8Action(),
- PlayMirrorAction(),
- // main support external apps
- VlcPackage(),
- MpvPackage(),
- MpvExPackage(),
- NextPlayerPackage(),
- JustPlayerPackage(),
- FcastAction(),
- LibreTorrentPackage(),
- BiglyBTPackage(),
- // forks/backup apps
- VlcNightlyPackage(),
- WebVideoCastPackage(),
- MpvYTDLPackage(),
- MpvKtPackage(),
- MpvKtPreviewPackage(),
- OnlyPlayer(),
- MpvRxPackage(),
- // Always Ask option
- AlwaysAskAction(),
- // added by plugins
- // ...
+ val allVideoClickActions = threadSafeListOf(
+ PlayInBrowserAction(), CopyClipboardAction(),
+ VlcPackage(), ViewM3U8Action(),
+ MpvPackage(), MpvYTDLPackage(),
+ WebVideoCastPackage(), MpvKtPackage(), MpvKtPreviewPackage(),
+ FcastAction()
)
init {
@@ -83,7 +38,7 @@ object VideoClickActionHolder {
fun makeOptionMap(activity: Activity?, video: ResultEpisode) = allVideoClickActions
// We need to have index before filtering
.mapIndexed { id, it -> it to id + ACTION_ID_OFFSET }
- .filter { it.first.shouldShowSafe(activity, video) }
+ .filter { it.first.shouldShow(activity, video) }
.map { it.first.name to it.second }
@@ -99,7 +54,7 @@ object VideoClickActionHolder {
?.second
}
- fun getPlayers(activity: Activity? = null) = allVideoClickActions.filter { it.isPlayer && it.shouldShowSafe(activity, null) }
+ fun getPlayers(activity: Activity? = null) = allVideoClickActions.filter { it.isPlayer && it.shouldShow(activity, null) }
}
abstract class VideoClickAction {
@@ -117,66 +72,10 @@ abstract class VideoClickAction {
/** Determines which plugin a given provider is from. This is the full path to the plugin. */
var sourcePlugin: String? = null
- /** Even if VideoClickAction should not run any UI code, startActivity requires it,
- * this is a wrapper for runOnUiThread in a suspended safe context that bubble up exceptions */
- @Throws
- suspend fun uiThread(callable : Callable) : T? {
- val future = FutureTask{
- try {
- Result.success(callable.call())
- } catch (t : Throwable) {
- Result.failure(t)
- }
- }
- CommonActivity.activity?.runOnUiThread(future) ?: throw ErrorLoadingException("No UI Activity, this should never happened")
- val result = withContext(Dispatchers.IO) {
- return@withContext future.get()
- }
- return result.getOrThrow()
- }
-
- /** Internally uses activityResultLauncher,
- * use this when the activity has a result like watched position */
- @Throws
- suspend fun launchResult(intent : Intent?, options : ActivityOptionsCompat? = null) {
- if (intent == null) {
- return
- }
-
- uiThread {
- MainActivity.activityResultLauncher?.launch(intent,options)
- }
- }
-
- /** Internally uses startActivity, use this when you don't
- * have any result that needs to be stored when exiting the activity */
- @Throws
- suspend fun launch(intent : Intent?, bundle : Bundle? = null) {
- if (intent == null) {
- return
- }
-
- uiThread {
- CommonActivity.activity?.startActivity(intent, bundle)
- }
- }
-
fun uniqueId() = "$sourcePlugin:${this::class.jvmName}"
- @Throws
abstract fun shouldShow(context: Context?, video: ResultEpisode?): Boolean
- /** Safe version of shouldShow, as we don't trust extension devs to handle exceptions,
- * however no dev *should* throw in shouldShow */
- fun shouldShowSafe(context: Context?, video: ResultEpisode?): Boolean {
- return try {
- shouldShow(context,video)
- } catch (t : Throwable) {
- logError(t)
- false
- }
- }
-
/**
* This function is called when the action is clicked.
* @param context The current activity
@@ -184,22 +83,5 @@ abstract class VideoClickAction {
* @param result The result of the link loading, contains video & subtitle links
* @param index if oneSource is true, this is the index of the selected source
*/
- @Throws
- abstract suspend fun runAction(context: Context?, video: ResultEpisode, result: LinkLoadingResult, index: Int?)
-
- /** Safe version of runAction, as we don't trust extension devs to handle exceptions */
- fun runActionSafe(context: Context?, video: ResultEpisode, result: LinkLoadingResult, index: Int?) = ioSafe {
- try {
- runAction(context, video, result, index)
- } catch (_ : NotImplementedError) {
- CommonActivity.showToast("runAction has not been implemented for ${name.asStringNull(context)}, please contact the extension developer of $sourcePlugin", Toast.LENGTH_LONG)
- } catch (error : ErrorLoadingException) {
- CommonActivity.showToast(error.message, Toast.LENGTH_LONG)
- } catch (_: ActivityNotFoundException) {
- CommonActivity.showToast(R.string.app_not_found_error, Toast.LENGTH_LONG)
- } catch (t : Throwable) {
- logError(t)
- CommonActivity.showToast(t.toString(), Toast.LENGTH_LONG)
- }
- }
-}
+ abstract fun runAction(context: Context?, video: ResultEpisode, result: LinkLoadingResult, index: Int?)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/Aria2Package.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/Aria2Package.kt
deleted file mode 100644
index a7401c2ff..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/Aria2Package.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/devgianlu/Aria2Android */
-@Suppress("unused")
-class Aria2Package : OpenInAppAction(
- appName = txt("Aria2"),
- packageName = "com.gianlu.aria2android",
- intentClass = "com.gianlu.aria2android.MainActivity"
-) {
- override val oneSource: Boolean = true
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- throw NotImplementedError("Aria2Android is missing getIntent, and onNewIntent, meaning it cant handle intents")
- }
-
- override fun onResult(activity: Activity, intent: Intent?) = Unit
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/BiglyBTPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/BiglyBTPackage.kt
deleted file mode 100644
index 3959bb9d3..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/BiglyBTPackage.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import androidx.core.net.toUri
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/BiglySoftware/BiglyBT-Android */
-class BiglyBTPackage : OpenInAppAction(
- appName = txt("BiglyBT"),
- packageName = "com.biglybt.android.client",
- intentClass = "com.biglybt.android.client.activity.IntentHandler"
-) {
- // Only torrents are supported by the app
- override val sourceTypes: Set =
- setOf(ExtractorLinkType.MAGNET, ExtractorLinkType.TORRENT)
-
- override val oneSource: Boolean = true
-
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- intent.data = result.links[index!!].url.toUri()
- }
-
- override fun onResult(activity: Activity, intent: Intent?) = Unit
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt
deleted file mode 100644
index d414b6117..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CloudStreamPackage.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.BuildConfig
-import com.lagradost.cloudstream3.ui.player.ExtractorUri
-import com.lagradost.cloudstream3.ui.player.SubtitleData
-import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
-import com.lagradost.cloudstream3.utils.DrmExtractorLink
-import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
-import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.newExtractorLink
-import com.lagradost.cloudstream3.utils.Qualities
-import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF
-import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF
-import com.lagradost.cloudstream3.utils.txt
-
-/**
- * If you want to support CloudStream 3 as an external player, then this shows how to play any video link
- * For basic interactions, just `intent.data = uri` works
- *
- * However for more advanced use, CloudStream 3 also supports playlists of MinimalVideoLink and MinimalSubtitleLink with a `String[]` of JSON
- * These are passed as LINKS_EXTRA and SUBTITLE_EXTRA respectively
- */
-@Suppress("Unused")
-class CloudStreamPackage : OpenInAppAction(
- appName = txt("CloudStream"),
- packageName = BuildConfig.APPLICATION_ID, //"com.lagradost.cloudstream3" or "com.lagradost.cloudstream3.prerelease"
- intentClass = "com.lagradost.cloudstream3.ui.player.DownloadedPlayerActivity"
-) {
- override val oneSource: Boolean = false
-
- companion object {
- const val SUBTITLE_EXTRA: String = "subs" // Json of an array of MinimalVideoLink
- const val LINKS_EXTRA: String = "links" // Json of an array of MinimalSubtitleLink
- const val TITLE_EXTRA: String = "title" // Unused (String)
- const val ID_EXTRA: String =
- "id" // Identification number for the video(s), used to store start time (Int)
- const val POSITION_EXTRA: String = "pos" // Start time in MS (Long)
- const val DURATION_EXTRA: String = "dur" // Duration time in MS (Long)
- }
-
- data class MinimalVideoLink(
- @JsonProperty("uri")
- val uri: Uri?,
- @JsonProperty("url")
- val url: String?,
- @JsonProperty("mimeType")
- val mimeType: String = "video/mp4",
- @JsonProperty("name")
- val name: String?,
- @JsonProperty("headers")
- var headers: Map = mapOf(),
- @JsonProperty("quality")
- val quality: Int?,
- ) {
- companion object {
- fun fromExtractor(link: ExtractorLink): MinimalVideoLink = MinimalVideoLink(
- uri = null,
- url = link.url,
- name = link.name,
- mimeType = link.type.getMimeType(),
- headers = if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer) + link.headers,
- quality = link.quality
- )
- }
-
- suspend fun toExtractorLink(): Pair =
- url?.let { url ->
- newExtractorLink(
- source = "NONE",
- name = name ?: "Unknown",
- url = url,
- type = ExtractorLinkType.entries.firstOrNull { ty -> ty.getMimeType() == mimeType }
- ?: ExtractorLinkType.VIDEO) {
-
- this@newExtractorLink.headers =
- this@MinimalVideoLink.headers
-
- this@newExtractorLink.quality =
- this@MinimalVideoLink.quality ?: Qualities.Unknown.value
- }
- } to uri?.let { uri ->
- ExtractorUri(
- uri = uri,
- name = name ?: "Unknown",
- )
- }
- }
-
-
- data class MinimalSubtitleLink(
- @JsonProperty("url")
- val url: String,
- @JsonProperty("mimeType")
- val mimeType: String = "text/vtt",
- @JsonProperty("name")
- val name: String?,
- @JsonProperty("headers")
- var headers: Map = mapOf(),
- ) {
- companion object {
- fun fromSubtitle(sub: SubtitleData): MinimalSubtitleLink = MinimalSubtitleLink(
- url = sub.url,
- mimeType = sub.mimeType,
- name = sub.originalName,
- headers = sub.headers,
- )
- }
-
- fun toSubtitleData(): SubtitleData = SubtitleData(
- url = url,
- nameSuffix = "",
- mimeType = mimeType,
- originalName = name ?: "Unknown",
- headers = headers,
- origin = SubtitleOrigin.URL,
- languageCode = fromCodeToLangTagIETF(name) ?:
- fromLanguageToTagIETF(name, true) ?:
- name,
- )
- }
-
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- intent.apply {
- val position = getViewPos(video.id)?.position
- if (position != null)
- putExtra(POSITION_EXTRA, position)
-
- putExtra(ID_EXTRA, video.id)
- putExtra(TITLE_EXTRA, video.name)
- putExtra(
- SUBTITLE_EXTRA,
- result.subs.map { MinimalSubtitleLink.fromSubtitle(it).toJson() }.toTypedArray()
- )
- putExtra(
- LINKS_EXTRA,
- result.links.filter { it !is ExtractorLinkPlayList && it !is DrmExtractorLink }
- .map { MinimalVideoLink.fromExtractor(it).toJson() }.toTypedArray()
- )
- }
- }
-
- override fun onResult(activity: Activity, intent: Intent?) {
- // No results yet
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CopyClipboardAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CopyClipboardAction.kt
index 7e89d7c8c..e054b5ce2 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CopyClipboardAction.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/CopyClipboardAction.kt
@@ -4,7 +4,7 @@ import android.content.Context
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
class CopyClipboardAction: VideoClickAction() {
@@ -14,7 +14,7 @@ class CopyClipboardAction: VideoClickAction() {
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
- override suspend fun runAction(
+ override fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/JustPlayerPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/JustPlayerPackage.kt
deleted file mode 100644
index 20eb843c7..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/JustPlayerPackage.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import androidx.core.net.toUri
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/moneytoo/Player/ */
-class JustPlayerPackage : OpenInAppAction(
- appName = txt("JustPlayer"),
- packageName = "com.brouken.player",
- intentClass = "com.brouken.player.PlayerActivity"
-) {
- override val sourceTypes: Set =
- setOf(ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8, ExtractorLinkType.DASH)
-
- override val oneSource: Boolean = true
-
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- // While JustPlayer has support for subs, it cant add both subs and links at the same time
- // See https://github.com/moneytoo/Player/blob/49d80eb8de7a7bfc662393fdf114788fed1ebb2e/app/src/main/java/com/brouken/player/PlayerActivity.java#L794
- intent.data = result.links[index!!].url.toUri()
- }
-
- override fun onResult(activity: Activity, intent: Intent?) = Unit
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/LibreTorrentPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/LibreTorrentPackage.kt
deleted file mode 100644
index 11d1858c6..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/LibreTorrentPackage.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import androidx.core.net.toUri
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/proninyaroslav/libretorrent */
-class LibreTorrentPackage : OpenInAppAction(
- appName = txt("LibreTorrent"),
- packageName = "org.proninyaroslav.libretorrent",
- intentClass = "org.proninyaroslav.libretorrent.ui.addtorrent.AddTorrentActivity"
-) {
- // Only torrents are supported by the app
- override val sourceTypes: Set =
- setOf(ExtractorLinkType.MAGNET, ExtractorLinkType.TORRENT)
-
- override val oneSource: Boolean = true
-
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- intent.data = result.links[index!!].url.toUri()
- }
-
- override fun onResult(activity: Activity, intent: Intent?) = Unit
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvKtPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvKtPackage.kt
index faae39212..f5ded49b8 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvKtPackage.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvKtPackage.kt
@@ -3,12 +3,13 @@ package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.net.Uri
import androidx.core.net.toUri
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLinkType
@@ -33,7 +34,7 @@ open class MpvKtPackage(
ExtractorLinkType.M3U8
)
- override suspend fun putExtra(
+ override fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
@@ -44,7 +45,7 @@ open class MpvKtPackage(
intent.apply {
putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray())
- setDataAndType(link.url.toUri(), "video/*")
+ setDataAndType(Uri.parse(link.url), "video/*")
// m3u8 plays, but changing sources feature is not available
// makeTempM3U8Intent(activity, this, result)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvPackage.kt
index cd49eb994..4c66d0450 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvPackage.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvPackage.kt
@@ -10,16 +10,13 @@ import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLinkType
// https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
// https://mpv-android.github.io/mpv-android/intent.html
-//https://github.com/marlboro-advance/mpvEx
-class MpvExPackage: MpvPackage("mpvEx","app.marlboroadvance.mpvex","app.marlboroadvance.mpvex.ui.player.PlayerActivity")
-
class MpvYTDLPackage : MpvPackage("MPV YTDL", "is.xyz.mpv.ytdl") {
override val sourceTypes = setOf(
ExtractorLinkType.VIDEO,
@@ -28,13 +25,13 @@ class MpvYTDLPackage : MpvPackage("MPV YTDL", "is.xyz.mpv.ytdl") {
)
}
-open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv",intentClass:String = "is.xyz.mpv.MPVActivity"): OpenInAppAction(
+open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv"): OpenInAppAction(
txt(appName),
packageName,
- intentClass
+ "is.xyz.mpv.MPVActivity"
) {
- override val oneSource = true // mpv has poor playlist support on TV
- override suspend fun putExtra(
+
+ override fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
@@ -45,11 +42,7 @@ open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv
putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray())
putExtra("title", video.name)
- if (index != null) {
- setDataAndType((result.links.getOrNull(index)?.url ?: return).toUri(), "video/*")
- } else {
- makeTempM3U8Intent(context, this, result)
- }
+ makeTempM3U8Intent(context, this, result)
val position = getViewPos(video.id)?.position
if (position != null)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt
deleted file mode 100644
index e8bb93a99..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/MpvRxPackage.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import androidx.core.net.toUri
-import com.lagradost.api.Log
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.actions.updateDurationAndPosition
-import com.lagradost.cloudstream3.isEpisodeBased
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/Riteshp2001/mpvRx
- *
- * https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132
- * https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56
- * */
-class MpvRxPackage : OpenInAppAction(
- appName = txt("mpvRx"),
- packageName = "app.gyrolet.mpvrx",
- intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity"
-) {
- override val oneSource = true
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- intent.apply {
- putExtra("title", video.name)
- val link = result.links[index!!]
- val headers = link.headers
-
- setData(link.url.toUri())
- if (headers.isNotEmpty()) {
- // PlayerActivity expects a flat array: [key1, value1, key2, value2, ...]
- val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray()
- intent.putExtra("headers", flat)
- }
- /*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146
- intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray())
- intent.putExtra(
- "subs.titles",
- subs.map { it.name }.toTypedArray(),
- )
- intent.putExtra(
- "subs.langs",
- subs.map { it.languageCode }.toTypedArray(),
- )
- val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri()
- intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf() )*/
-
- if (video.tvType.isEpisodeBased()) {
- video.season?.let { intent.putExtra("introdb_season", it) }
- video.episode.let { intent.putExtra("introdb_episode", it) }
- }
-
- val position = getViewPos(video.id)?.position
- if (position != null)
- putExtra("position", position.toInt())
- }
- }
-
- override fun onResult(activity: Activity, intent: Intent?) {
- val position = intent?.getIntExtra("position", -1) ?: -1
- val duration = intent?.getIntExtra("duration", -1) ?: -1
- Log.d("MPV", "Position: $position, Duration: $duration")
- updateDurationAndPosition(position.toLong(), duration.toLong())
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/NextPlayerPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/NextPlayerPackage.kt
deleted file mode 100644
index 5d0923b81..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/NextPlayerPackage.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import androidx.core.net.toUri
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/anilbeesetti/nextplayer */
-class NextPlayerPackage : OpenInAppAction(
- appName = txt("NextPlayer"),
- packageName = "dev.anilbeesetti.nextplayer",
- intentClass = "dev.anilbeesetti.nextplayer.feature.player.PlayerActivity"
-) {
- override val sourceTypes: Set =
- setOf(ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8, ExtractorLinkType.DASH)
-
- override val oneSource: Boolean = true
-
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- intent.data = result.links[index!!].url.toUri()
- }
-
- override fun onResult(activity: Activity, intent: Intent?) = Unit
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt
deleted file mode 100644
index 348be440a..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/OnlyPlayer.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.core.net.toUri
-import com.lagradost.cloudstream3.actions.OpenInAppAction
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
-
-/** https://github.com/Kindness-Kismet/only_player/tree/main
- * https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */
-class OnlyPlayer : OpenInAppAction(
- txt("Only Player"),
- "one.only.player",
- intentClass = "one.only.player.feature.player.PlayerActivity"
-) {
- override val oneSource = true
- override suspend fun putExtra(
- context: Context,
- intent: Intent,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- /** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */
- intent.apply {
- val link = result.links[index!!]
- setData(link.url.toUri())
-
- putExtra("headers", Bundle().apply {
- for ((key, value) in link.headers) {
- putExtra(key, value)
- }
- })
- }
- }
-
- override fun onResult(activity: Activity, intent: Intent?) {
- /* onResult does not get called */
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayInBrowserAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayInBrowserAction.kt
index bfd2926bf..de32bb4b3 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayInBrowserAction.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayInBrowserAction.kt
@@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.actions.temp
import android.content.Context
import android.content.Intent
-import androidx.core.net.toUri
+import android.net.Uri
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.VideoClickAction
+import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.ExtractorLinkType
class PlayInBrowserAction: VideoClickAction() {
@@ -25,15 +26,19 @@ class PlayInBrowserAction: VideoClickAction() {
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
- override suspend fun runAction(
+ override fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
val link = result.links.getOrNull(index ?: 0) ?: return
- val i = Intent(Intent.ACTION_VIEW)
- i.data = link.url.toUri()
- launch(i)
+ try {
+ val i = Intent(Intent.ACTION_VIEW)
+ i.data = Uri.parse(link.url)
+ context?.startActivity(i)
+ } catch (e: Exception) {
+ logError(e)
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayMirrorAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayMirrorAction.kt
deleted file mode 100644
index 56512377b..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/PlayMirrorAction.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.lagradost.cloudstream3.actions.temp
-
-import android.app.Activity
-import android.content.Context
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.actions.VideoClickAction
-import com.lagradost.cloudstream3.ui.player.ExtractorUri
-import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
-import com.lagradost.cloudstream3.ui.player.LOADTYPE_INAPP
-import com.lagradost.cloudstream3.ui.player.SubtitleData
-import com.lagradost.cloudstream3.ui.player.VideoGenerator
-import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
-import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.ExtractorLinkType
-import com.lagradost.cloudstream3.utils.UIHelper.navigate
-import com.lagradost.cloudstream3.utils.txt
-
-class PlayMirrorAction : VideoClickAction() {
- override val name = txt(R.string.episode_action_play_mirror)
-
- override val oneSource = true
-
- override val isPlayer = true
-
- override val sourceTypes: Set = LOADTYPE_INAPP
-
- override fun shouldShow(context: Context?, video: ResultEpisode?) = true
-
- override suspend fun runAction(
- context: Context?,
- video: ResultEpisode,
- result: LinkLoadingResult,
- index: Int?
- ) {
- //Implemented a generator to handle the single
- val activity = context as? Activity ?: return
- val link = index?.let { result.links[it] }
- val generatorMirror = object : VideoGenerator(listOf(video)) {
- override val hasCache: Boolean = false
- override val canSkipLoading: Boolean = false
- override fun getId(index: Int): Int = video.id
-
- override suspend fun generateLinks(
- clearCache: Boolean,
- sourceTypes: Set,
- callback: (Pair) -> Unit,
- subtitleCallback: (SubtitleData) -> Unit,
- offset: Int,
- isCasting: Boolean
- ): Boolean {
- index?.let { callback(link to null) }
- result.subs.forEach { subtitle -> subtitleCallback(subtitle) }
- return true
- }
- }
-
- activity.navigate(
- R.id.global_to_navigation_player,
- GeneratorPlayer.newInstance(
- generatorMirror, 0, result.syncData
- )
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/ViewM3U8Action.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/ViewM3U8Action.kt
index 791566862..c14168e96 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/ViewM3U8Action.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/ViewM3U8Action.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
class ViewM3U8Action: VideoClickAction() {
override val name = txt(R.string.episode_action_play_in_format, "m3u8 player")
@@ -16,7 +16,7 @@ class ViewM3U8Action: VideoClickAction() {
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
- override suspend fun runAction(
+ override fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
@@ -25,6 +25,6 @@ class ViewM3U8Action: VideoClickAction() {
if (context == null) return
val i = Intent(Intent.ACTION_VIEW)
makeTempM3U8Intent(context, i, result)
- launch(i)
+ context.startActivity(i)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt
index 46b46a2c2..ecb37fdc6 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/VlcPackage.kt
@@ -4,27 +4,21 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
-import androidx.core.net.toUri
import com.lagradost.api.Log
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
// https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
// https://wiki.videolan.org/Android_Player_Intents/
-class VlcNightlyPackage : VlcPackage() {
- override val packageName = "org.videolan.vlc.debug"
- override val appName = txt("VLC Nightly")
-}
-
-open class VlcPackage: OpenInAppAction(
+class VlcPackage: OpenInAppAction(
appName = txt("VLC"),
packageName = "org.videolan.vlc",
intentClass = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@@ -38,21 +32,18 @@ open class VlcPackage: OpenInAppAction(
Intent.ACTION_VIEW
}
) {
- // while VLC supports multi links, it has poor support, so we disable it for now
- override val oneSource = true
+ override val oneSource = false
- override suspend fun putExtra(
+ override fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
- if (index != null) {
- intent.setDataAndType(result.links[index].url.toUri(), "video/*")
- } else {
- makeTempM3U8Intent(context, intent, result)
- }
+
+ makeTempM3U8Intent(context, intent, result)
+
val position = getViewPos(video.id)?.position ?: 0L
intent.putExtra("from_start", false)
@@ -60,7 +51,7 @@ open class VlcPackage: OpenInAppAction(
intent.putExtra("secure_uri", true)
intent.putExtra("title", video.name)
- val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
+ val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
result.subs.firstOrNull {
subsLang == it.languageCode
}?.let {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/WebVideoCastPackage.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/WebVideoCastPackage.kt
index 963221bb3..f8419f63c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/WebVideoCastPackage.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/WebVideoCastPackage.kt
@@ -3,13 +3,14 @@ package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
+import android.net.Uri
import android.os.Bundle
import androidx.core.net.toUri
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.ExtractorLinkType
// https://www.webvideocaster.com/integrations
@@ -27,7 +28,7 @@ class WebVideoCastPackage: OpenInAppAction(
ExtractorLinkType.M3U8
)
- override suspend fun putExtra(
+ override fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
@@ -37,7 +38,7 @@ class WebVideoCastPackage: OpenInAppAction(
val link = result.links[index ?: 0]
intent.apply {
- setDataAndType(link.url.toUri(), "video/*")
+ setDataAndType(Uri.parse(link.url), "video/*")
val title = video.name ?: video.headerName
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastAction.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastAction.kt
index 1036a7055..c0f92e4df 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastAction.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastAction.kt
@@ -1,13 +1,13 @@
package com.lagradost.cloudstream3.actions.temp.fcast
import android.content.Context
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
+import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
@@ -26,7 +26,7 @@ class FcastAction: VideoClickAction() {
override fun shouldShow(context: Context?, video: ResultEpisode?) = FcastManager.currentDevices.isNotEmpty()
- override suspend fun runAction(
+ override fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
@@ -34,16 +34,14 @@ class FcastAction: VideoClickAction() {
) {
val link = result.links.getOrNull(index ?: 0) ?: return
val devices = FcastManager.currentDevices.toList()
- uiThread {
- context?.getActivity()?.showBottomDialog(
- devices.map { it.name },
- -1,
- txt(R.string.player_settings_select_cast_device).asString(context),
- false,
- {}) {
- val position = getViewPos(video.id)?.position
- castTo(devices.getOrNull(it), link, position)
- }
+ context?.getActivity()?.showBottomDialog(
+ devices.map { it.name },
+ -1,
+ txt(R.string.player_settings_select_cast_device).asString(context),
+ false,
+ {}) {
+ val position = getViewPos(video.id)?.position
+ castTo(devices.getOrNull(it), link, position)
}
}
@@ -55,7 +53,7 @@ class FcastAction: VideoClickAction() {
session.sendMessage(
Opcode.Play,
PlayMessage(
- link.type.getMimeType(),
+ "video/*",
link.url,
time = position?.let { it / 1000.0 },
headers = mapOf(
@@ -66,4 +64,4 @@ class FcastAction: VideoClickAction() {
)
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastManager.kt b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastManager.kt
index e2cf4f002..78682ca1c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/actions/temp/fcast/FcastManager.kt
@@ -5,9 +5,7 @@ import android.net.nsd.NsdManager
import android.net.nsd.NsdManager.ResolveListener
import android.net.nsd.NsdServiceInfo
import android.os.Build
-import android.os.ext.SdkExtensions
import android.util.Log
-import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
class FcastManager {
@@ -73,67 +71,24 @@ class FcastManager {
}
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
- // Safe here as, java.lang.NoClassDefFoundError: Failed resolution of: Landroid/net/nsd/NsdManager$ServiceInfoCallback
- safe {
- if (serviceInfo == null) return@safe
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(
- Build.VERSION_CODES.TIRAMISU
- ) >= 7
- ) {
- nsdManager?.registerServiceInfoCallback(
- serviceInfo,
- Runnable::run,
- object : NsdManager.ServiceInfoCallback {
- override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
- Log.e(tag, "Service registration failed: $errorCode")
- }
-
- override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
- Log.d(
- tag,
- "Service updated: ${serviceInfo.serviceName}," +
- "Net: ${serviceInfo.hostAddresses.firstOrNull()?.hostAddress}"
- )
- synchronized(_currentDevices) {
- _currentDevices.removeIf { it.rawName == serviceInfo.serviceName }
- _currentDevices.add(PublicDeviceInfo(serviceInfo))
- }
- }
-
- override fun onServiceLost() {
- Log.d(tag, "Service lost: ${serviceInfo.serviceName},")
- synchronized(_currentDevices) {
- _currentDevices.removeIf { it.rawName == serviceInfo.serviceName }
- }
- }
-
- override fun onServiceInfoCallbackUnregistered() {}
- })
- } else {
- @Suppress("DEPRECATION")
- nsdManager?.resolveService(serviceInfo, object : ResolveListener {
- override fun onResolveFailed(
- serviceInfo: NsdServiceInfo?,
- errorCode: Int
- ) {
- }
-
- override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
- if (serviceInfo == null) return
-
- synchronized(_currentDevices) {
- _currentDevices.add(PublicDeviceInfo(serviceInfo))
- }
-
- Log.d(
- tag,
- "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}"
- )
- }
- })
+ if (serviceInfo == null) return
+ nsdManager?.resolveService(serviceInfo, object : ResolveListener {
+ override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
}
- }
+
+ override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
+ if (serviceInfo == null) return
+
+ synchronized(_currentDevices) {
+ _currentDevices.add(PublicDeviceInfo(serviceInfo))
+ }
+
+ Log.d(
+ tag,
+ "Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}"
+ )
+ }
+ })
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
@@ -180,16 +135,6 @@ class FcastManager {
class PublicDeviceInfo(serviceInfo: NsdServiceInfo) {
val rawName: String = serviceInfo.serviceName
- val host: String? = if (
- Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
- SdkExtensions.getExtensionVersion(
- Build.VERSION_CODES.TIRAMISU
- ) >= 7
- ) {
- serviceInfo.hostAddresses.firstOrNull()?.hostAddress
- } else {
- @Suppress("DEPRECATION")
- serviceInfo.host.hostAddress
- }
+ val host: String? = serviceInfo.host.hostAddress
val name = rawName.replace("-", " ") + host?.let { " $it" }
-}
\ No newline at end of file
+}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
similarity index 83%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
rename to app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
index 7076e407f..5bbb4538b 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
@@ -1,18 +1,10 @@
package com.lagradost.cloudstream3.metaproviders
import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.LoadResponse
-import com.lagradost.cloudstream3.MovieLoadResponse
-import com.lagradost.cloudstream3.MovieSearchResponse
-import com.lagradost.cloudstream3.SearchResponseList
-import com.lagradost.cloudstream3.SubtitleFile
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.toNewSearchResponseList
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
@@ -30,9 +22,11 @@ class CrossTmdbProvider : TmdbProvider() {
}
private val validApis
- get() = apis.filter { it.lang == this.lang && it::class != this::class }
+ get() =
+ synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
//.distinctBy { it.uniqueId }
+
data class CrossMetaData(
@JsonProperty("isSuccess") val isSuccess: Boolean,
@JsonProperty("movies") val movies: List>? = null,
@@ -61,12 +55,8 @@ class CrossTmdbProvider : TmdbProvider() {
return false
}
- override suspend fun search(query: String, page: Int): SearchResponseList? {
- // TODO REMOVE
- return super.search(query, page)
- ?.items
- ?.filterIsInstance()
- ?.toNewSearchResponseList()
+ override suspend fun search(query: String): List? {
+ return super.search(query)?.filterIsInstance() // TODO REMOVE
}
override suspend fun load(url: String): LoadResponse? {
@@ -119,4 +109,4 @@ class CrossTmdbProvider : TmdbProvider() {
return base
}
-}
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
similarity index 94%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
rename to app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
index 2a8524e00..bc646a8d2 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
@@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
-import com.lagradost.cloudstream3.mvvm.safeAsync
+import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
@@ -44,7 +44,7 @@ object SyncRedirector {
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
if (providerApi.supportedSyncNames.contains(syncName)) {
syncRegex.find(url)?.value?.let {
- safeAsync {
+ suspendSafeApiCall {
providerApi.getLoadUrl(syncName, it)
}
}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
similarity index 68%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
rename to app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
index 89f935da3..c5b4d453d 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
@@ -1,51 +1,17 @@
package com.lagradost.cloudstream3.metaproviders
import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.Actor
-import com.lagradost.cloudstream3.Episode
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.HomePageList
-import com.lagradost.cloudstream3.HomePageResponse
-import com.lagradost.cloudstream3.LoadResponse
+import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addActors
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
-import com.lagradost.cloudstream3.MainAPI
-import com.lagradost.cloudstream3.MainPageRequest
-import com.lagradost.cloudstream3.MovieLoadResponse
-import com.lagradost.cloudstream3.MovieSearchResponse
-import com.lagradost.cloudstream3.ProviderType
-import com.lagradost.cloudstream3.Score
-import com.lagradost.cloudstream3.SearchResponseList
-import com.lagradost.cloudstream3.TvSeriesLoadResponse
-import com.lagradost.cloudstream3.TvSeriesSearchResponse
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.newEpisode
-import com.lagradost.cloudstream3.newHomePageResponse
-import com.lagradost.cloudstream3.newMovieLoadResponse
-import com.lagradost.cloudstream3.newMovieSearchResponse
-import com.lagradost.cloudstream3.newTvSeriesLoadResponse
-import com.lagradost.cloudstream3.newTvSeriesSearchResponse
-import com.lagradost.cloudstream3.runAllAsync
-import com.lagradost.cloudstream3.toNewSearchResponseList
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.uwetrottmann.tmdb2.Tmdb
-import com.uwetrottmann.tmdb2.entities.AppendToResponse
-import com.uwetrottmann.tmdb2.entities.BaseMovie
-import com.uwetrottmann.tmdb2.entities.BaseTvShow
-import com.uwetrottmann.tmdb2.entities.CastMember
-import com.uwetrottmann.tmdb2.entities.ContentRating
-import com.uwetrottmann.tmdb2.entities.Movie
-import com.uwetrottmann.tmdb2.entities.ReleaseDate
-import com.uwetrottmann.tmdb2.entities.ReleaseDatesResult
-import com.uwetrottmann.tmdb2.entities.TvSeason
-import com.uwetrottmann.tmdb2.entities.TvShow
-import com.uwetrottmann.tmdb2.entities.Videos
+import com.uwetrottmann.tmdb2.entities.*
import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem
import com.uwetrottmann.tmdb2.enumerations.VideoType
import retrofit2.awaitResponse
-import retrofit2.Response
-import java.util.Calendar
+import java.util.*
/**
* episode and season starting from 1
@@ -88,39 +54,36 @@ open class TmdbProvider : MainAPI() {
}
private fun BaseTvShow.toSearchResponse(): TvSeriesSearchResponse {
- return newTvSeriesSearchResponse(
- name = this.name ?: this.original_name,
- url = getUrl(id, true),
- type = TvType.TvSeries,
- fix = false
- ) {
- this.id = this@toSearchResponse.id
- this.posterUrl = getImageUrl(poster_path)
- this.score = Score.from10(vote_average)
- this.year = first_air_date?.let {
+ return TvSeriesSearchResponse(
+ this.name ?: this.original_name,
+ getUrl(id, true),
+ apiName,
+ TvType.TvSeries,
+ getImageUrl(this.poster_path),
+ this.first_air_date?.let {
Calendar.getInstance().apply {
time = it
}.get(Calendar.YEAR)
- }
- }
+ },
+ null,
+ this.id
+ )
}
private fun BaseMovie.toSearchResponse(): MovieSearchResponse {
- return newMovieSearchResponse(
- name = this.title ?: this.original_title,
- url = getUrl(id, false),
- type = TvType.Movie,
- fix = false
- ) {
- this.id = this@toSearchResponse.id
- this.posterUrl = getImageUrl(poster_path)
- this.score = Score.from10(vote_average)
- this.year = release_date?.let {
+ return MovieSearchResponse(
+ this.title ?: this.original_title,
+ getUrl(id, false),
+ apiName,
+ TvType.TvSeries,
+ getImageUrl(this.poster_path),
+ this.release_date?.let {
Calendar.getInstance().apply {
time = it
}.get(Calendar.YEAR)
- }
- }
+ },
+ this.id,
+ )
}
private fun List?.toActors(): List>? {
@@ -133,39 +96,39 @@ open class TmdbProvider : MainAPI() {
}
private suspend fun TvShow.toLoadResponse(): TvSeriesLoadResponse {
- val tvSeasonsService = tmdb.tvSeasonsService()
- val episodes = mutableListOf()
-
- val validSeasons = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 } ?: emptyList()
- for (season in validSeasons) {
- val seasonNumber = season.season_number ?: continue
-
- val response: Response = tmdb.tvSeasonsService()
- .season(this.id, seasonNumber, "external_ids,images,episodes")
- .awaitResponse()
-
- val fullSeason = response.body() ?: continue
-
- fullSeason.episodes?.forEach { episode ->
- episodes += newEpisode(
- TmdbLink(
- episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
- this.id,
- episode.episode_number,
+ val episodes = this.seasons?.filter { !disableSeasonZero || (it.season_number ?: 0) != 0 }
+ ?.mapNotNull { season ->
+ season.episodes?.map { episode ->
+ Episode(
+ TmdbLink(
+ episode.external_ids?.imdb_id ?: this.external_ids?.imdb_id,
+ this.id,
+ episode.episode_number,
+ episode.season_number,
+ this.name ?: this.original_name,
+ ).toJson(),
+ episode.name,
episode.season_number,
- this.name ?: this.original_name
- ).toJson()
- ) {
- this.name = episode.name
- this.season = episode.season_number
- this.episode = episode.episode_number
- this.score = Score.from10(episode.vote_average)
- this.description = episode.overview
- this.date = episode.air_date?.time
- this.posterUrl = getImageUrl(episode.still_path)
+ episode.episode_number,
+ getImageUrl(episode.still_path),
+ episode.rating,
+ episode.overview,
+ episode.air_date?.time,
+ )
+ } ?: (1..(season.episode_count ?: 1)).map { episodeNum ->
+ Episode(
+ episode = episodeNum,
+ data = TmdbLink(
+ this.external_ids?.imdb_id,
+ this.id,
+ episodeNum,
+ season.season_number,
+ this.name ?: this.original_name,
+ ).toJson(),
+ season = season.season_number
+ )
}
- }
- }
+ }?.flatten() ?: listOf()
return newTvSeriesLoadResponse(
this.name ?: this.original_name,
@@ -181,13 +144,16 @@ open class TmdbProvider : MainAPI() {
}
plot = overview
addImdbId(external_ids?.imdb_id)
+
tags = genres?.mapNotNull { it.name }
duration = episode_run_time?.average()?.toInt()
- score = Score.from10(vote_average)
+ rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
+
recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
addActors(credits?.cast?.toList().toActors())
+
contentRating = fetchContentRating(id, "US")
}
}
@@ -225,7 +191,7 @@ open class TmdbProvider : MainAPI() {
addImdbId(external_ids?.imdb_id)
tags = genres?.mapNotNull { it.name }
duration = runtime
- score = Score.from10(vote_average)
+ rating = this@toLoadResponse.rating
addTrailer(videos.toTrailers())
recommendations = (this@toLoadResponse.recommendations
@@ -236,15 +202,15 @@ open class TmdbProvider : MainAPI() {
}
}
- override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
+ override suspend fun getMainPage(page: Int, request : MainPageRequest): HomePageResponse {
// SAME AS DISCOVER IT SEEMS
-// val popularSeries = tmdb.tvService().popular(page, "en-US").execute().body()?.results?.map {
+// val popularSeries = tmdb.tvService().popular(1, "en-US").execute().body()?.results?.map {
// it.toSearchResponse()
// } ?: listOf()
//
// val popularMovies =
-// tmdb.moviesService().popular(page, "en-US", "840").execute().body()?.results?.map {
+// tmdb.moviesService().popular(1, "en-US", "840").execute().body()?.results?.map {
// it.toSearchResponse()
// } ?: listOf()
@@ -252,31 +218,31 @@ open class TmdbProvider : MainAPI() {
var discoverSeries: List = listOf()
var topMovies: List = listOf()
var topSeries: List = listOf()
- runAllAsync(
+ argamap(
{
- discoverMovies = tmdb.discoverMovie().page(page).build().awaitResponse().body()?.results?.map {
+ discoverMovies = tmdb.discoverMovie().build().awaitResponse().body()?.results?.map {
it.toSearchResponse()
} ?: listOf()
}, {
- discoverSeries = tmdb.discoverTv().page(page).build().awaitResponse().body()?.results?.map {
+ discoverSeries = tmdb.discoverTv().build().awaitResponse().body()?.results?.map {
it.toSearchResponse()
} ?: listOf()
}, {
// https://en.wikipedia.org/wiki/ISO_3166-1
topMovies =
- tmdb.moviesService().topRated(page, "en-US", "US").awaitResponse()
+ tmdb.moviesService().topRated(1, "en-US", "US").awaitResponse()
.body()?.results?.map {
it.toSearchResponse()
} ?: listOf()
}, {
topSeries =
- tmdb.tvService().topRated(page, "en-US").awaitResponse().body()?.results?.map {
+ tmdb.tvService().topRated(1, "en-US").awaitResponse().body()?.results?.map {
it.toSearchResponse()
} ?: listOf()
}
)
- return newHomePageResponse(
+ return HomePageResponse(
listOf(
// HomePageList("Popular Series", popularSeries),
// HomePageList("Popular Movies", popularMovies),
@@ -396,27 +362,29 @@ open class TmdbProvider : MainAPI() {
} else {
loadFromTmdb(id)?.let { return it }
if (isTvSeries) {
- tmdb.tvService().externalIds(id).awaitResponse().body()?.imdb_id?.let {
+ tmdb.tvService().externalIds(id, "en-US").awaitResponse().body()?.imdb_id?.let {
val fromImdb = loadFromImdb(it)
val result = if (fromImdb == null) {
val details = tmdb.tvService().tv(id, "en-US").awaitResponse().body()
loadFromImdb(it, details?.seasons ?: listOf())
?: loadFromTmdb(id, details?.seasons ?: listOf())
- } else fromImdb
+ } else {
+ fromImdb
+ }
result
}
} else {
- tmdb.moviesService().externalIds(id).awaitResponse()
+ tmdb.moviesService().externalIds(id, "en-US").awaitResponse()
.body()?.imdb_id?.let { loadFromImdb(it) }
}
}
}
- override suspend fun search(query: String, page: Int): SearchResponseList? {
- return tmdb.searchService().multi(query, page, "en-US", "US", includeAdult).awaitResponse()
+ override suspend fun search(query: String): List? {
+ return tmdb.searchService().multi(query, 1, "en-Us", "US", includeAdult).awaitResponse()
.body()?.results?.mapNotNull {
it.movie?.toSearchResponse() ?: it.tvShow?.toSearchResponse()
- }?.toNewSearchResponseList()
+ }
}
-}
\ No newline at end of file
+}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
similarity index 78%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
rename to app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
index 59dcd2711..addee9a02 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
@@ -1,8 +1,9 @@
package com.lagradost.cloudstream3.metaproviders
+import android.net.Uri
import com.fasterxml.jackson.annotation.JsonAlias
import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.api.BuildConfig
+import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.Actor
import com.lagradost.cloudstream3.ActorData
@@ -16,24 +17,24 @@ import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.NextAiring
import com.lagradost.cloudstream3.ProviderType
-import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.SearchResponse
-import com.lagradost.cloudstream3.SearchResponseList
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.addDate
import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.isUpcoming
+import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.mainPageOf
-import com.lagradost.cloudstream3.newEpisode
+import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.newHomePageResponse
import com.lagradost.cloudstream3.newMovieLoadResponse
import com.lagradost.cloudstream3.newMovieSearchResponse
-import com.lagradost.cloudstream3.newSearchResponseList
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import java.text.SimpleDateFormat
+import java.util.Locale
+import kotlin.math.roundToInt
open class TraktProvider : MainAPI() {
override var name = "Trakt"
@@ -45,9 +46,9 @@ open class TraktProvider : MainAPI() {
TvType.Anime,
)
- private val traktApiUrl = "https://api.trakt.tv"
-
- val traktClientId: String = BuildConfig.TRAKT_CLIENT_ID
+ private val traktClientId =
+ base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
+ private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
override val mainPage = mainPageOf(
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
@@ -57,7 +58,8 @@ open class TraktProvider : MainAPI() {
)
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
- val apiResponse = getApi("${request.data}?extended=full,images&page=$page")
+
+ val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
val results = parseJson>(apiResponse).map { element ->
element.toSearchResponse()
@@ -70,76 +72,76 @@ open class TraktProvider : MainAPI() {
val media = this.media ?: this
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
val poster = media.images?.poster?.firstOrNull()
- return if (mediaType == TvType.Movie) {
- newMovieSearchResponse(
- name = media.title ?: "",
+
+ if (mediaType == TvType.Movie) {
+ return newMovieSearchResponse(
+ name = media.title!!,
url = Data(
type = mediaType,
mediaDetails = media,
).toJson(),
type = TvType.Movie,
) {
- score = Score.from10(media.rating)
posterUrl = fixPath(poster)
}
} else {
- newTvSeriesSearchResponse(
- name = media.title ?: "",
+ return newTvSeriesSearchResponse(
+ name = media.title!!,
url = Data(
type = mediaType,
mediaDetails = media,
).toJson(),
type = TvType.TvSeries,
) {
- score = Score.from10(media.rating)
this.posterUrl = fixPath(poster)
}
}
}
- override suspend fun search(query: String, page: Int): SearchResponseList? {
+ override suspend fun search(query: String): List? {
val apiResponse =
- getApi("$traktApiUrl/search/movie,show?extended=full,images&limit=20&page=$page&query=$query")
+ getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
- return newSearchResponseList(parseJson>(apiResponse).map { element ->
+ val results = parseJson>(apiResponse).map { element ->
element.toSearchResponse()
- })
+ }
+
+ return results
}
override suspend fun load(url: String): LoadResponse {
+
val data = parseJson(url)
val mediaDetails = data.mediaDetails
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
- val posterUrl = fixPath(mediaDetails?.images?.poster?.firstOrNull())
- val backDropUrl = fixPath(mediaDetails?.images?.fanart?.firstOrNull())
- val logoUrl = fixPath(mediaDetails?.images?.logo?.firstOrNull())
+ val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
+ val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
val resActor =
- getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=full,images")
+ getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
val actors = parseJson(resActor).cast?.map {
ActorData(
Actor(
name = it.person?.name!!,
- image = fixPath(it.person.images?.headshot?.firstOrNull())
+ image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
),
roleString = it.character
)
}
val resRelated =
- getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=full,images&limit=20")
+ getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() }
val isCartoon =
mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
val isAnime =
- isCartoon && (mediaDetails.language == "zh" || mediaDetails.language == "ja")
+ isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
val isBollywood = mediaDetails?.country == "in"
- val uniqueUrl = data.mediaDetails?.ids?.trakt?.toJson() ?: data.toJson()
if (data.type == TvType.Movie) {
@@ -169,21 +171,19 @@ open class TraktProvider : MainAPI() {
dataUrl = linkData.toJson(),
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
) {
- this.uniqueUrl = uniqueUrl
this.name = mediaDetails.title
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
- this.posterUrl = posterUrl
+ this.posterUrl = getOriginalWidthImageUrl(posterUrl)
this.year = mediaDetails.year
this.plot = mediaDetails.overview
- this.score = Score.from10(mediaDetails.rating)
+ this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
this.tags = mediaDetails.genres
this.duration = mediaDetails.runtime
this.recommendations = relatedMedia
this.actors = actors
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
- this.backgroundPosterUrl = backDropUrl
- this.logoUrl = logoUrl
+ this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
@@ -192,7 +192,7 @@ open class TraktProvider : MainAPI() {
} else {
val resSeasons =
- getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=full,images,episodes")
+ getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
val episodes = mutableListOf()
val seasons = parseJson>(resSeasons)
var nextAir: NextAiring? = null
@@ -228,16 +228,16 @@ open class TraktProvider : MainAPI() {
).toJson()
episodes.add(
- newEpisode(linkData.toJson()) {
- this.name = episode.title
- this.season = episode.season
- this.episode = episode.number
- this.description = episode.overview
- this.runTime = episode.runtime
- this.posterUrl = fixPath( episode.images?.screenshot?.firstOrNull())
- //this.rating = episode.rating?.times(10)?.roundToInt()
- this.score = Score.from10(episode.rating)
-
+ Episode(
+ data = linkData.toJson(),
+ name = episode.title,
+ season = episode.season,
+ episode = episode.number,
+ posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
+ rating = episode.rating?.times(10)?.roundToInt(),
+ description = episode.overview,
+ runTime = episode.runtime
+ ).apply {
this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) {
nextAir = NextAiring(
@@ -257,15 +257,14 @@ open class TraktProvider : MainAPI() {
type = if (isAnime) TvType.Anime else TvType.TvSeries,
episodes = episodes
) {
- this.uniqueUrl = uniqueUrl
this.name = mediaDetails.title
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
this.episodes = episodes
- this.posterUrl = posterUrl
+ this.posterUrl = getOriginalWidthImageUrl(posterUrl)
this.year = mediaDetails.year
this.plot = mediaDetails.overview
this.showStatus = getStatus(mediaDetails.status)
- this.score = Score.from10(mediaDetails.rating)
+ this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
this.tags = mediaDetails.genres
this.duration = mediaDetails.runtime
this.recommendations = relatedMedia
@@ -273,8 +272,7 @@ open class TraktProvider : MainAPI() {
this.comingSoon = isUpcoming(mediaDetails.released)
//posterHeaders
this.nextAiring = nextAir
- this.backgroundPosterUrl = backDropUrl
- this.logoUrl = logoUrl
+ this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
this.contentRating = mediaDetails.certification
addTrailer(mediaDetails.trailer)
addImdbId(mediaDetails.ids?.imdb)
@@ -291,7 +289,18 @@ open class TraktProvider : MainAPI() {
"trakt-api-version" to "2",
"trakt-api-key" to traktClientId,
)
- ).text
+ ).toString()
+ }
+
+ private fun isUpcoming(dateString: String?): Boolean {
+ return try {
+ val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
+ val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
+ unixTimeMS < dateTime
+ } catch (t: Throwable) {
+ logError(t)
+ false
+ }
}
private fun getStatus(t: String?): ShowStatus {
@@ -307,6 +316,19 @@ open class TraktProvider : MainAPI() {
return "https://$url"
}
+ private fun getWidthImageUrl(path: String?, width: String): String? {
+ if (path == null) return null
+ if (!path.contains("image.tmdb.org")) return fixPath(path)
+ val fileName = Uri.parse(path).lastPathSegment ?: return null
+ return "https://image.tmdb.org/t/p/${width}/${fileName}"
+ }
+
+ private fun getOriginalWidthImageUrl(path: String?): String? {
+ if (path == null) return null
+ if (!path.contains("image.tmdb.org")) return fixPath(path)
+ return getWidthImageUrl(path, "original")
+ }
+
data class Data(
val type: TvType? = null,
val mediaDetails: MediaDetails? = null,
@@ -357,10 +379,10 @@ open class TraktProvider : MainAPI() {
)
data class Images(
- @JsonProperty("poster") val poster: List? = null,
@JsonProperty("fanart") val fanart: List? = null,
+ @JsonProperty("poster") val poster: List? = null,
@JsonProperty("logo") val logo: List? = null,
- @JsonProperty("clearart") val clearArt: List? = null,
+ @JsonProperty("clearart") val clearart: List? = null,
@JsonProperty("banner") val banner: List? = null,
@JsonProperty("thumb") val thumb: List? = null,
@JsonProperty("screenshot") val screenshot: List? = null,
@@ -420,30 +442,30 @@ open class TraktProvider : MainAPI() {
)
data class LinkData(
- @JsonProperty("id") val id: Int? = null,
- @JsonProperty("trakt_id") val traktId: Int? = null,
- @JsonProperty("trakt_slug") val traktSlug: String? = null,
- @JsonProperty("tmdb_id") val tmdbId: Int? = null,
- @JsonProperty("imdb_id") val imdbId: String? = null,
- @JsonProperty("tvdb_id") val tvdbId: Int? = null,
- @JsonProperty("tvrage_id") val tvrageId: String? = null,
- @JsonProperty("type") val type: String? = null,
- @JsonProperty("season") val season: Int? = null,
- @JsonProperty("episode") val episode: Int? = null,
- @JsonProperty("ani_id") val aniId: String? = null,
- @JsonProperty("anime_id") val animeId: String? = null,
- @JsonProperty("title") val title: String? = null,
- @JsonProperty("year") val year: Int? = null,
- @JsonProperty("org_title") val orgTitle: String? = null,
- @JsonProperty("is_anime") val isAnime: Boolean = false,
- @JsonProperty("aired_year") val airedYear: Int? = null,
- @JsonProperty("last_season") val lastSeason: Int? = null,
- @JsonProperty("eps_title") val epsTitle: String? = null,
- @JsonProperty("jp_title") val jpTitle: String? = null,
- @JsonProperty("date") val date: String? = null,
- @JsonProperty("aired_date") val airedDate: String? = null,
- @JsonProperty("is_asian") val isAsian: Boolean = false,
- @JsonProperty("is_bollywood") val isBollywood: Boolean = false,
- @JsonProperty("is_cartoon") val isCartoon: Boolean = false,
+ val id: Int? = null,
+ val traktId: Int? = null,
+ val traktSlug: String? = null,
+ val tmdbId: Int? = null,
+ val imdbId: String? = null,
+ val tvdbId: Int? = null,
+ val tvrageId: String? = null,
+ val type: String? = null,
+ val season: Int? = null,
+ val episode: Int? = null,
+ val aniId: String? = null,
+ val animeId: String? = null,
+ val title: String? = null,
+ val year: Int? = null,
+ val orgTitle: String? = null,
+ val isAnime: Boolean = false,
+ val airedYear: Int? = null,
+ val lastSeason: Int? = null,
+ val epsTitle: String? = null,
+ val jpTitle: String? = null,
+ val date: String? = null,
+ val airedDate: String? = null,
+ val isAsian: Boolean = false,
+ val isBollywood: Boolean = false,
+ val isCartoon: Boolean = false,
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
index 482ec05fc..3df5197cd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
@@ -1,68 +1,16 @@
package com.lagradost.cloudstream3.mvvm
-import android.view.View
-import androidx.activity.ComponentActivity
-import androidx.core.view.doOnAttach
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
-import androidx.lifecycle.findViewTreeLifecycleOwner
-import androidx.viewbinding.ViewBinding
-import com.lagradost.cloudstream3.ui.BaseFragment
/** NOTE: Only one observer at a time per value */
-fun ComponentActivity.observe(liveData: LiveData, action: (T) -> Unit) {
- observeNullable(liveData) { t -> t?.run(action) }
-}
-
-/** NOTE: Only one observer at a time per value */
-fun ComponentActivity.observeNullable(liveData: LiveData, action: (T?) -> Unit) {
+fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) {
liveData.removeObservers(this)
- liveData.observe(this, action)
+ liveData.observe(this) { it?.let { t -> action(t) } }
}
/** NOTE: Only one observer at a time per value */
-fun BaseFragment.observe(liveData: LiveData, action: (T) -> Unit) {
- observeNullable(liveData) { t -> t?.run(action) }
+fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
+ liveData.removeObservers(this)
+ liveData.observe(this) { action(it) }
}
-
-/**
- * Attaches an observable to the root binding, instead of the fragment. This is more efficient as
- * it will not call observe if the view is in the background.
- *
- * NOTE: Only one observer at a time per value
- * */
-fun BaseFragment.observeNullable(
- liveData: LiveData, action: (T?) -> Unit
-) {
- val root = this.binding?.root
- if (root == null) {
- liveData.removeObservers(this)
- liveData.observe(this, action)
- } else {
- root.doOnAttach { view ->
- // On attach should make findViewTreeLifecycleOwner non-null, but use "this" just in case
- val owner: LifecycleOwner = view.findViewTreeLifecycleOwner() ?: this@observeNullable
- liveData.removeObservers(owner)
- liveData.observe(owner, action)
- }
- }
-}
-
-/** NOTE: Only one observer at a time per value */
-fun View.observe(liveData: LiveData, action: (T) -> Unit) {
- observeNullable(liveData) { t -> t?.run(action) }
-}
-
-/** NOTE: Only one observer at a time per value */
-fun View.observeNullable(liveData: LiveData, action: (T?) -> Unit) {
- doOnAttach { view ->
- // On attach should make findViewTreeLifecycleOwner non-null
- val owner: LifecycleOwner? = view.findViewTreeLifecycleOwner()
- if(owner == null) {
- debugException { "Expected non-null findViewTreeLifecycleOwner" }
- return@doOnAttach
- }
- liveData.removeObservers(owner)
- liveData.observe(owner, action)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
index 9efa88a37..85a9db5db 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
@@ -5,7 +5,7 @@ import android.webkit.CookieManager
import androidx.annotation.AnyThread
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugWarning
-import com.lagradost.cloudstream3.mvvm.safe
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests.Companion.await
import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking
@@ -32,7 +32,7 @@ class CloudflareKiller : Interceptor {
init {
// Needs to clear cookies between sessions to generate new cookies.
- safe {
+ normalSafeApiCall {
// This can throw an exception on unsupported devices :(
CookieManager.getInstance().removeAllCookies(null)
}
@@ -77,7 +77,7 @@ class CloudflareKiller : Interceptor {
}
private fun getWebViewCookie(url: String): String? {
- return safe {
+ return normalSafeApiCall {
CookieManager.getInstance()?.getCookie(url)
}
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt
index 4127799e8..55e092513 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/DohProviders.kt
@@ -84,24 +84,4 @@ fun OkHttpClient.Builder.addQuad9Dns() = (
"9.9.9.9",
"149.112.112.112",
)
- ))
-
-fun OkHttpClient.Builder.addDnsSbDns() = (
- addGenericDns(
- "https://doh.dns.sb/dns-query",
- //https://dns.sb/guide/
- listOf(
- "185.222.222.222",
- "45.11.45.11",
- )
- ))
-
-fun OkHttpClient.Builder.addCanadianShieldDns() = (
- addGenericDns(
- "https://private.canadianshield.cira.ca/dns-query",
- //https://www.cira.ca/en/canadian-shield/configure/summary-cira-canadian-shield-dns-resolver-addresses/
- listOf(
- "149.112.121.10",
- "149.112.122.10",
- )
- ))
+ ))
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt
index 6234297d0..a1d84f6cd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/RequestsHelper.kt
@@ -2,10 +2,9 @@ package com.lagradost.cloudstream3.network
import android.content.Context
import androidx.preference.PreferenceManager
-import com.lagradost.cloudstream3.Prerelease
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
-import com.lagradost.cloudstream3.mvvm.safe
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache
@@ -16,38 +15,14 @@ import org.conscrypt.Conscrypt
import java.io.File
import java.security.Security
-// Backwards compatible constructor, mark as deprecated later
-fun Requests.initClient(context: Context) {
- this.baseClient = buildDefaultClient(context)
-}
-
-/** Only use ignoreSSL if you know what you are doing*/
-@Prerelease
-fun Requests.initClient(context: Context, ignoreSSL: Boolean = false) {
- this.baseClient = buildDefaultClient(context, ignoreSSL)
-}
-
-
-// Backwards compatible constructor, mark as deprecated later
-fun buildDefaultClient(context: Context): OkHttpClient {
- return buildDefaultClient(context, false)
-}
-
-/** Only use ignoreSSL if you know what you are doing*/
-@Prerelease
-fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClient {
- safe { Security.insertProviderAt(Conscrypt.newProvider(), 1) }
-
+fun Requests.initClient(context: Context): OkHttpClient {
+ normalSafeApiCall { Security.insertProviderAt(Conscrypt.newProvider(), 1) }
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
- val baseClient = OkHttpClient.Builder()
+ baseClient = OkHttpClient.Builder()
.followRedirects(true)
.followSslRedirects(true)
- .apply {
- if (ignoreSSL) {
- ignoreAllSSLErrors()
- }
- }
+ .ignoreAllSSLErrors()
.cache(
// Note that you need to add a ResponseInterceptor to make this 100% active.
// The server response dictates if and when stuff should be cached.
@@ -63,8 +38,6 @@ fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClie
4 -> addAdGuardDns()
5 -> addDNSWatchDns()
6 -> addQuad9Dns()
- 7 -> addDnsSbDns()
- 8 -> addCanadianShieldDns()
}
}
// Needs to be build as otherwise the other builders will change this object
@@ -72,6 +45,11 @@ fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClie
return baseClient
}
+//val Request.cookies: Map
+// get() {
+// return this.headers.getCookies("Cookie")
+// }
+
private val DEFAULT_HEADERS = mapOf("user-agent" to USER_AGENT)
/**
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
rename to app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
index e1496db06..e35ae24b9 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
@@ -2,20 +2,56 @@ package com.lagradost.cloudstream3.plugins
import android.content.Context
import android.content.res.Resources
+import kotlin.Throws
+import com.lagradost.cloudstream3.MainAPI
+import com.lagradost.cloudstream3.APIHolder
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.extractorApis
import android.util.Log
+import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
-import kotlin.Throws
-abstract class Plugin : BasePlugin() {
+const val PLUGIN_TAG = "PluginInstance"
+
+abstract class Plugin {
/**
* Called when your Plugin is loaded
* @param context Context
*/
@Throws(Throwable::class)
open fun load(context: Context) {
- // If not overridden by an extension then try the cross-platform load()
- load()
+ }
+
+ /**
+ * Called when your Plugin is being unloaded
+ */
+ @Throws(Throwable::class)
+ open fun beforeUnload() {
+ }
+
+ /**
+ * Used to register providers instances of MainAPI
+ * @param element MainAPI provider you want to register
+ */
+ fun registerMainAPI(element: MainAPI) {
+ Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
+ element.sourcePlugin = this.filename
+ // Race condition causing which would case duplicates if not for distinctBy
+ synchronized(APIHolder.allProviders) {
+ APIHolder.allProviders.add(element)
+ }
+ APIHolder.addPluginMapping(element)
+ }
+
+ /**
+ * Used to register extractor instances of ExtractorApi
+ * @param element ExtractorApi provider you want to register
+ */
+ fun registerExtractorAPI(element: ExtractorApi) {
+ Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
+ element.sourcePlugin = this.filename
+ extractorApis.add(element)
}
/**
@@ -25,16 +61,35 @@ abstract class Plugin : BasePlugin() {
fun registerVideoClickAction(element: VideoClickAction) {
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
element.sourcePlugin = this.filename
- VideoClickActionHolder.allVideoClickActions.add(element)
+ synchronized(VideoClickActionHolder.allVideoClickActions) {
+ VideoClickActionHolder.allVideoClickActions.add(element)
+ }
+ }
+
+ class Manifest {
+ @JsonProperty("name")
+ var name: String? = null
+ @JsonProperty("pluginClassName")
+ var pluginClassName: String? = null
+ @JsonProperty("version")
+ var version: Int? = null
+ @JsonProperty("requiresResources")
+ var requiresResources: Boolean = false
}
/**
* This will contain your resources if you specified requiresResources in gradle
*/
var resources: Resources? = null
+ /** Full file path to the plugin. */
+ @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename"))
+ var __filename: String?
+ get() = filename
+ set(value) {filename = value}
+ var filename: String? = null
/**
* This will add a button in the settings allowing you to add custom settings
*/
var openSettings: ((context: Context) -> Unit)? = null
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
index debd3f0eb..8535592d4 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
@@ -1,10 +1,7 @@
package com.lagradost.cloudstream3.plugins
import android.Manifest
-import android.app.Activity
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
+import android.app.*
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.AssetManager
@@ -13,56 +10,45 @@ import android.os.Build
import android.os.Environment
import android.util.Log
import android.widget.Toast
-import androidx.annotation.WorkerThread
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.APIHolder
+import com.google.gson.Gson
+import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
-import com.lagradost.cloudstream3.AllLanguagesName
-import com.lagradost.cloudstream3.AutoDownloadMode
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
-import com.lagradost.cloudstream3.InternalAPI
-import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
-import com.lagradost.cloudstream3.MainActivity.Companion.lastError
-import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
-import com.lagradost.cloudstream3.PROVIDER_STATUS_OK
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
-import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.safe
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
-import com.lagradost.cloudstream3.plugins.RepositoryManager.sha256
+import com.lagradost.cloudstream3.ui.result.UiText
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings
-import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import com.lagradost.cloudstream3.utils.UiText
-import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.sanitizeFilename
+import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.utils.extractorApis
-import com.lagradost.cloudstream3.utils.txt
import dalvik.system.PathClassLoader
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.io.InputStreamReader
+import java.util.*
// Different keys for local and not since local can be removed at any time without app knowing, hence the local are getting rebuilt on every app start
const val PLUGINS_KEY = "PLUGINS_KEY"
@@ -80,7 +66,6 @@ data class PluginData(
@JsonProperty("filePath") val filePath: String,
@JsonProperty("version") val version: Int,
) {
- @WorkerThread
fun toSitePlugin(): SitePlugin {
return SitePlugin(
this.filePath,
@@ -95,9 +80,7 @@ data class PluginData(
null,
null,
null,
- File(this.filePath).length(),
- // No file hash for local plugins. Local plugins have no use for the hash, and it's expensive to compute.
- null
+ File(this.filePath).length()
)
}
}
@@ -151,7 +134,7 @@ object PluginManager {
!it.filePath.contains(repositoryPath)
}
val file = File(repositoryPath)
- safe {
+ normalSafeApiCall {
if (file.exists()) file.deleteRecursively()
}
setKey(PLUGINS_KEY, plugins)
@@ -188,21 +171,22 @@ object PluginManager {
var currentlyLoading: String? = null
// Maps filepath to plugin
- val plugins: MutableMap =
- LinkedHashMap()
+ val plugins: MutableMap =
+ LinkedHashMap()
// Maps urls to plugin
- val urlPlugins: MutableMap =
- LinkedHashMap()
+ val urlPlugins: MutableMap =
+ LinkedHashMap()
- private val classLoaders: MutableMap =
- HashMap()
+ private val classLoaders: MutableMap =
+ HashMap()
var loadedLocalPlugins = false
private set
var loadedOnlinePlugins = false
private set
+ private val gson = Gson()
private suspend fun maybeLoadPlugin(context: Context, file: File) {
val name = file.name
@@ -261,24 +245,16 @@ object PluginManager {
* 2. If disabled do nothing
* 3. If outdated download and load the plugin
* 4. Else load the plugin normally
- *
- * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
- * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
- */
- @Suppress("FunctionName")
- @InternalAPI
- @Throws
- suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_updateAllOnlinePluginsAndLoadThem(activity: Activity) {
- assertNonRecursiveCallstack()
-
+ **/
+ fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
// Load all plugins as fast as possible!
- ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(activity)
+ loadAllOnlinePlugins(activity)
afterPluginsLoadedEvent.invoke(false)
val urls = (getKey>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
- val onlinePlugins = urls.toList().amap {
+ val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url }
@@ -299,7 +275,7 @@ object PluginManager {
val updatedPlugins = mutableListOf()
- outdatedPlugins.amap { pluginData ->
+ outdatedPlugins.apmap { pluginData ->
if (pluginData.isDisabled) {
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
unloadPlugin(pluginData.savedData.filePath)
@@ -307,7 +283,6 @@ object PluginManager {
downloadPlugin(
activity,
pluginData.onlineData.second.url,
- pluginData.onlineData.second.fileHash,
pluginData.savedData.internalName,
File(pluginData.savedData.filePath),
true
@@ -339,23 +314,12 @@ object PluginManager {
* 1. Gets all online data from online plugins repo
* 2. Fetch all not downloaded plugins
* 3. Download them and reload plugins
- *
- * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
- * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
- */
- @Suppress("FunctionName")
- @InternalAPI
- @Throws
- suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_downloadNotExistingPluginsAndLoad(
- activity: Activity,
- mode: AutoDownloadMode
- ) {
- assertNonRecursiveCallstack()
-
+ **/
+ fun downloadNotExistingPluginsAndLoad(activity: Activity, mode: AutoDownloadMode) {
val newDownloadPlugins = mutableListOf()
val urls = (getKey>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
- val onlinePlugins = urls.toList().amap {
+ val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url }
@@ -415,11 +379,10 @@ object PluginManager {
}
//Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
- notDownloadedPlugins.amap { pluginData ->
+ notDownloadedPlugins.apmap { pluginData ->
downloadPlugin(
activity,
pluginData.onlineData.second.url,
- pluginData.onlineData.second.fileHash,
pluginData.savedData.internalName,
pluginData.onlineData.first,
!pluginData.isDisabled
@@ -441,27 +404,12 @@ object PluginManager {
Log.i(TAG, "Plugin download done!")
}
- @Throws
- private fun assertNonRecursiveCallstack() {
- if (Thread.currentThread().stackTrace.any { it.methodName == "loadPlugin" }) {
- throw Error("You tried to call a function that will recursively call loadPlugin, this will cause crashes or memory leaks. Do not do this, there is better ways to implement the feature than reloading plugins. Are you sure you read the compile error or docs?")
- }
- }
-
/**
* Use updateAllOnlinePluginsAndLoadThem
- *
- * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
- * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
- */
- @Suppress("FunctionName")
- @InternalAPI
- @Throws
- suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context: Context) {
- assertNonRecursiveCallstack()
-
+ * */
+ fun loadAllOnlinePlugins(context: Context) {
// Load all plugins as fast as possible!
- (getPluginsOnline()).toList().amap { pluginData ->
+ (getPluginsOnline()).toList().apmap { pluginData ->
loadPlugin(
context,
File(pluginData.filePath),
@@ -472,37 +420,21 @@ object PluginManager {
/**
* Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb
- *
- * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
- * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
- */
- @Suppress("FunctionName")
- @InternalAPI
- @Throws
- suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_hotReloadAllLocalPlugins(activity: FragmentActivity?) {
- assertNonRecursiveCallstack()
-
+ **/
+ fun hotReloadAllLocalPlugins(activity: FragmentActivity?) {
Log.d(TAG, "Reloading all local plugins!")
if (activity == null) return
getPluginsLocal().forEach {
unloadPlugin(it.filePath)
}
- ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(activity, true)
+ loadAllLocalPlugins(activity, true)
}
/**
* @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins
* and reload all pages even if they are previously valid
- *
- * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
- * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
- */
- @Suppress("FunctionName")
- @InternalAPI
- @Throws
- suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(context: Context, forceReload: Boolean) {
- assertNonRecursiveCallstack()
-
+ **/
+ fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
val dir = File(LOCAL_PLUGINS_PATH)
if (!dir.exists()) {
@@ -516,64 +448,24 @@ object PluginManager {
val sortedPlugins = dir.listFiles()
// Always sort plugins alphabetically for reproducible results
- Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: ${sortedPlugins?.size}")
+ Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins")
- // Use app-specific external files directory and copy the file there.
- // We have to do this because on Android 14+, it otherwise gives SecurityException
- // due to dex files and setReadOnly seems to have no effect unless it it here.
- val pluginDirectory = File(context.getExternalFilesDir(null), "plugins")
- if (!pluginDirectory.exists()) {
- pluginDirectory.mkdirs() // Ensure the plugins directory exists
- }
-
- // Make sure all local plugins are fully refreshed.
- removeKey(PLUGINS_KEY_LOCAL)
-
- sortedPlugins?.sortedBy { it.name }?.amap { file ->
- try {
- val destinationFile = File(pluginDirectory, file.name)
-
- // Only copy the file if the destination file doesn't exist or if it
- // has been modified (check file length and modification time).
- if (!destinationFile.exists() ||
- destinationFile.length() != file.length() ||
- destinationFile.lastModified() != file.lastModified()
- ) {
-
- // Copy the file to the app-specific plugin directory
- file.copyTo(destinationFile, overwrite = true)
-
- // After copying, set the destination file's modification time
- // to match the source file. We do this for performance so that we
- // can check the modification time and not make redundant writes.
- destinationFile.setLastModified(file.lastModified())
- }
-
- // Load the plugin after it has been copied
- maybeLoadPlugin(context, destinationFile)
- } catch (t: Throwable) {
- Log.e(TAG, "Failed to copy the file")
- logError(t)
- }
+ sortedPlugins?.sortedBy { it.name }?.apmap { file ->
+ maybeLoadPlugin(context, file)
}
loadedLocalPlugins = true
afterPluginsLoadedEvent.invoke(forceReload)
}
- /** @return true if safe mode is enabled in any possible way. */
- fun isSafeMode(): Boolean {
- return checkSafeModeFile() || lastError != null
- }
-
/**
* This can be used to override any extension loading to fix crashes!
* @return true if safe mode file is present
**/
fun checkSafeModeFile(): Boolean {
- return safe {
+ return normalSafeApiCall {
val folder = File(CLOUD_STREAM_FOLDER)
- if (!folder.exists()) return@safe false
+ if (!folder.exists()) return@normalSafeApiCall false
val files = folder.listFiles { _, name ->
name.equals("safe", ignoreCase = true)
}
@@ -591,26 +483,26 @@ object PluginManager {
Log.i(TAG, "Loading plugin: $data")
return try {
- // In case of Android 14+ then
+ // in case of android 14 then
try {
- // Set the file as read-only and log if it fails
- if (!file.setReadOnly()) {
- Log.e(TAG, "Failed to set read-only on plugin file: ${file.name}")
- }
+ File(filePath).setReadOnly()
} catch (t: Throwable) {
- Log.e(TAG, "Failed to set dex as read-only")
+ Log.e(TAG, "Failed to set dex as readonly")
logError(t)
}
val loader = PathClassLoader(filePath, context.classLoader)
- var manifest: BasePlugin.Manifest
+ var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream ->
if (stream == null) {
Log.e(TAG, "Failed to load plugin $fileName: No manifest found")
return false
}
InputStreamReader(stream).use { reader ->
- manifest = parseJson(reader.readText())
+ manifest = gson.fromJson(
+ reader,
+ Plugin.Manifest::class.java
+ )
}
}
@@ -623,9 +515,9 @@ object PluginManager {
@Suppress("UNCHECKED_CAST")
val pluginClass: Class<*> =
- loader.loadClass(manifest.pluginClassName) as Class
- val pluginInstance: BasePlugin =
- pluginClass.getDeclaredConstructor().newInstance() as BasePlugin
+ loader.loadClass(manifest.pluginClassName) as Class
+ val pluginInstance: Plugin =
+ pluginClass.getDeclaredConstructor().newInstance() as Plugin
// Sets with the proper version
setPluginData(data.copy(version = version))
@@ -645,33 +537,23 @@ object PluginManager {
addAssetPath.invoke(assets, file.absolutePath)
@Suppress("DEPRECATION")
- (pluginInstance as? Plugin)?.resources = Resources(
+ pluginInstance.resources = Resources(
assets,
context.resources.displayMetrics,
context.resources.configuration
)
}
- synchronized(plugins) {
- plugins[filePath] = pluginInstance
- }
- synchronized(classLoaders) {
- classLoaders[loader] = pluginInstance
- }
- synchronized(urlPlugins) {
- urlPlugins[data.url ?: filePath] = pluginInstance
- }
- if (pluginInstance is Plugin) {
- pluginInstance.load(context)
- } else {
- pluginInstance.load()
- }
+ plugins[filePath] = pluginInstance
+ classLoaders[loader] = pluginInstance
+ urlPlugins[data.url ?: filePath] = pluginInstance
+ pluginInstance.load(context)
Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
currentlyLoading = null
true
} catch (e: Throwable) {
Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}")
showToast(
- // context.getActivity(), // we are not always on the main thread
+ context.getActivity(),
context.getString(R.string.plugin_load_fail).format(fileName),
Toast.LENGTH_LONG
)
@@ -695,33 +577,25 @@ object PluginManager {
}
// remove all registered apis
- APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
- removePluginMapping(it)
+ synchronized(APIHolder.apis) {
+ APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
+ removePluginMapping(it)
+ }
+ }
+ synchronized(APIHolder.allProviders) {
+ APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
}
- APIHolder.allProviders.withLock {
- APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
+ extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
+
+ synchronized(VideoClickActionHolder.allVideoClickActions) {
+ VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
}
- extractorApis.withLock {
- extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
- }
+ classLoaders.values.removeIf { v -> v == plugin }
- VideoClickActionHolder.allVideoClickActions.withLock {
- VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename }
- }
-
- synchronized(classLoaders) {
- classLoaders.values.removeIf { v -> v == plugin }
- }
-
- synchronized(plugins) {
- plugins.remove(absolutePath)
- }
-
- synchronized(urlPlugins) {
- urlPlugins.values.removeIf { v -> v == plugin }
- }
+ plugins.remove(absolutePath)
+ urlPlugins.values.removeIf { v -> v == plugin }
}
/**
@@ -751,27 +625,25 @@ object PluginManager {
suspend fun downloadPlugin(
activity: Activity,
pluginUrl: String,
- pluginHash: String?,
internalName: String,
repositoryUrl: String,
loadPlugin: Boolean
): Boolean {
val file = getPluginPath(activity, internalName, repositoryUrl)
- return downloadPlugin(activity, pluginUrl, pluginHash, internalName, file, loadPlugin)
+ return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin)
}
suspend fun downloadPlugin(
activity: Activity,
pluginUrl: String,
- pluginHash: String?,
internalName: String,
file: File,
- loadPlugin: Boolean,
+ loadPlugin: Boolean
): Boolean {
try {
Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}")
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
- val newFile = downloadPluginToFile(activity, pluginUrl, file, pluginHash) ?: return false
+ val newFile = downloadPluginToFile(pluginUrl, file) ?: return false
val data = PluginData(
internalName,
@@ -814,84 +686,6 @@ object PluginManager {
}
}
- /**
- * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
- * If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
- */
- @Suppress("FunctionName")
- @InternalAPI
- @Throws
- suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_manuallyReloadAndUpdatePlugins(activity: Activity) {
- assertNonRecursiveCallstack()
-
- showToast(activity.getString(R.string.starting_plugin_update_manually), Toast.LENGTH_LONG)
-
- ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(activity)
- afterPluginsLoadedEvent.invoke(false)
-
- val urls = (getKey>(REPOSITORIES_KEY)
- ?: emptyArray()) + PREBUILT_REPOSITORIES
- val onlinePlugins = urls.toList().amap {
- getRepoPlugins(it.url)?.toList() ?: emptyList()
- }.flatten().distinctBy { it.second.url }
-
- val allPlugins = getPluginsOnline().flatMap { savedData ->
- onlinePlugins
- .filter { it.second.internalName == savedData.internalName }
- .mapNotNull { onlineData ->
- OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) }
- }
- }.distinctBy { it.onlineData.second.url }
-
- val updatedPlugins = mutableListOf()
-
- allPlugins.amap { pluginData ->
- if (pluginData.isDisabled) {
- Log.e(
- "PluginManager",
- "Unloading disabled plugin: ${pluginData.onlineData.second.name}"
- )
- unloadPlugin(pluginData.savedData.filePath)
- } else {
- val existingFile = File(pluginData.savedData.filePath)
- if (existingFile.exists()) existingFile.delete()
-
- if (downloadPlugin(
- activity,
- pluginData.onlineData.second.url,
- pluginData.onlineData.second.fileHash,
- pluginData.savedData.internalName,
- existingFile,
- true
- )
- ) {
- updatedPlugins.add(pluginData.onlineData.second.name)
- }
- }
- }.also {
- main {
- val message = if (updatedPlugins.isNotEmpty()) {
- activity.getString(R.string.plugins_updated_manually, updatedPlugins.size)
- } else {
- activity.getString(R.string.no_plugins_updated_manually)
- }
- showToast(message, Toast.LENGTH_LONG)
-
- val notificationText = UiText.StringResource(
- R.string.plugins_updated_manually,
- listOf(updatedPlugins.size)
- )
- createNotification(activity, notificationText, updatedPlugins)
-
- }
- }
-
- loadedOnlinePlugins = true
- afterPluginsLoadedEvent.invoke(false)
-
- Log.i("PluginManager", "Plugin update done!")
- }
-
private fun Context.createNotificationChannel() {
hasCreatedNotChanel = true
// Create the NotificationChannel, but only on API 26+ because
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
index 07d6aaa37..c6ec9df7f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
@@ -1,17 +1,16 @@
package com.lagradost.cloudstream3.plugins
import android.content.Context
-import androidx.annotation.WorkerThread
import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.context
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.safe
-import com.lagradost.cloudstream3.mvvm.safeAsync
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
+import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
@@ -19,19 +18,16 @@ import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import java.io.BufferedInputStream
import java.io.File
-import java.nio.file.AtomicMoveNotSupportedException
-import java.nio.file.Files
-import java.nio.file.StandardCopyOption
-import java.security.MessageDigest
-import java.util.concurrent.atomic.AtomicInteger
+import java.io.InputStream
+import java.io.OutputStream
/**
* Comes with the app, always available in the app, non removable.
* */
data class Repository(
- @JsonProperty("iconUrl") val iconUrl: String?,
@JsonProperty("name") val name: String,
@JsonProperty("description") val description: String?,
@JsonProperty("manifestVersion") val manifestVersion: Int,
@@ -65,12 +61,10 @@ data class SitePlugin(
@JsonProperty("repositoryUrl") val repositoryUrl: String?,
// These types are yet to be mapped and used, ignore for now
@JsonProperty("tvTypes") val tvTypes: List?,
- // Most often a language tag like "en" or "zh-TW"
@JsonProperty("language") val language: String?,
@JsonProperty("iconUrl") val iconUrl: String?,
// Automatically generated by the gradle plugin
@JsonProperty("fileSize") val fileSize: Long?,
- @JsonProperty("fileHash") val fileHash: String?,
)
@@ -79,26 +73,7 @@ object RepositoryManager {
val PREBUILT_REPOSITORIES: Array by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
}
- private val GH_REGEX =
- Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
-
-
- /** Returns a SHA-256 string of the file content.
- * Example: "sha256-b70462c264cb7f90fc2860a8e58d7544ce747ff347d1d11fa093623901853573" **/
- @WorkerThread
- fun sha256(file: File): String {
- val digest = MessageDigest.getInstance("SHA-256")
-
- file.inputStream().use { fis ->
- val buffer = ByteArray(8192)
- var read = fis.read(buffer)
- while (read != -1) {
- digest.update(buffer, 0, read)
- read = fis.read(buffer)
- }
- }
- return "sha256-" + digest.digest().joinToString("") { "%02x".format(it) }
- }
+ private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String {
@@ -119,12 +94,12 @@ object RepositoryManager {
else fixedUrl
}
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
- safeAsync {
+ suspendSafeApiCall {
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
it2.headers["Location"]?.let { url ->
- if (url.startsWith("https://cutt.ly/404")) return@safeAsync null
- if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null
- return@safeAsync url
+ if (url.startsWith("https://cutt.ly/404")) return@suspendSafeApiCall null
+ if (url.removeSuffix("/") == "https://cutt.ly") return@suspendSafeApiCall null
+ return@suspendSafeApiCall url
}
}
}
@@ -132,7 +107,7 @@ object RepositoryManager {
}
suspend fun parseRepository(url: String): Repository? {
- return safeAsync {
+ return suspendSafeApiCall {
// Take manifestVersion and such into account later
app.get(convertRawGitUrl(url)).parsedSafe()
}
@@ -163,52 +138,21 @@ object RepositoryManager {
}.flatten()
}
-
suspend fun downloadPluginToFile(
- context: Context,
pluginUrl: String,
- file: File,
- expectedFileHash: String?
+ file: File
): File? {
- return safeAsync {
- val parentDir = file.parentFile ?: return@safeAsync null
- parentDir.mkdirs()
+ return suspendSafeApiCall {
+ file.mkdirs()
- // Prevent corrupting the plugin file if the operation fails
- val tempFile = File.createTempFile(file.name, ".tmp", context.cacheDir)
+ // Overwrite if exists
+ if (file.exists()) {
+ file.delete()
+ }
+ file.createNewFile()
val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body
-
- body.byteStream().use { body ->
- tempFile.outputStream().use { fileSteam ->
- body.copyTo(fileSteam)
- }
- }
-
- if (expectedFileHash != null) {
- val downloadHash = sha256(tempFile)
- if (expectedFileHash != downloadHash) {
- tempFile.delete()
- throw IllegalStateException("Extension hash mismatch when validating '${file.name}'! Expected: '$expectedFileHash', got: '$downloadHash'.")
- }
- }
-
- // We prefer the operation to be atomic
- try {
- Files.move(
- tempFile.toPath(),
- file.toPath(),
- StandardCopyOption.REPLACE_EXISTING,
- StandardCopyOption.ATOMIC_MOVE
- )
- } catch (_: AtomicMoveNotSupportedException) {
- Files.move(
- tempFile.toPath(),
- file.toPath(),
- StandardCopyOption.REPLACE_EXISTING
- )
- }
-
+ write(body.byteStream(), file.outputStream())
file
}
}
@@ -247,7 +191,7 @@ object RepositoryManager {
// Unload all plugins, not using deletePlugin since we
// delete all data and files in deleteRepositoryData
- safe {
+ normalSafeApiCall {
file.listFiles { plugin: File ->
unloadPlugin(plugin.absolutePath)
false
@@ -256,4 +200,13 @@ object RepositoryManager {
PluginManager.deleteRepositoryData(file.absolutePath)
}
+
+ private fun write(stream: InputStream, output: OutputStream) {
+ val input = BufferedInputStream(stream)
+ val dataBuffer = ByteArray(512)
+ var readBytes: Int
+ while (input.read(dataBuffer).also { readBytes = it } != -1) {
+ output.write(dataBuffer, 0, readBytes)
+ }
+ }
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
index 85a806f0b..d1b702f4c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
@@ -2,9 +2,9 @@ package com.lagradost.cloudstream3.plugins
import android.util.Log
import android.widget.Toast
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.context
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import java.security.MessageDigest
import com.lagradost.cloudstream3.app
@@ -12,76 +12,87 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
-object VotingApi {
-
+object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi"
- private const val API_DOMAIN = "https://api.countify.xyz"
- private fun transformUrl(url: String): String =
+ private const val API_DOMAIN = "https://counterapi.com/api"
+
+ private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest
.getInstance("SHA-256")
.digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) }
- suspend fun SitePlugin.getVotes(): Int = getVotes(url)
- fun SitePlugin.hasVoted(): Boolean = hasVoted(url)
- suspend fun SitePlugin.vote(): Int = vote(url)
- fun SitePlugin.canVote(): Boolean = canVote(this.url)
+ suspend fun SitePlugin.getVotes(): Int {
+ return getVotes(url)
+ }
+ fun SitePlugin.hasVoted(): Boolean {
+ return hasVoted(url)
+ }
+
+ suspend fun SitePlugin.vote(): Int {
+ return vote(url)
+ }
+
+ fun SitePlugin.canVote(): Boolean {
+ return canVote(this.url)
+ }
+
+ // Plugin url to Int
private val votesCache = mutableMapOf()
+ private fun getRepository(pluginUrl: String) = pluginUrl
+ .split("/")
+ .drop(2)
+ .take(3)
+ .joinToString("-")
+
private suspend fun readVote(pluginUrl: String): Int {
- val id = transformUrl(pluginUrl)
- val url = "$API_DOMAIN/get-total/$id"
- Log.d(LOGKEY, "Requesting GET: $url")
- return app.get(url).parsedSafe()?.count ?: 0
+ val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
+ Log.d(LOGKEY, "Requesting: $url")
+ return app.get(url).parsedSafe()?.value ?: 0
}
private suspend fun writeVote(pluginUrl: String): Boolean {
- val id = transformUrl(pluginUrl)
- val url = "$API_DOMAIN/increment/$id"
- Log.d(LOGKEY, "Requesting POST: $url")
- return app.post(url, emptyMap())
- .parsedSafe()?.count != null
+ val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
+ Log.d(LOGKEY, "Requesting: $url")
+ return app.get(url).parsedSafe()?.value != null
}
suspend fun getVotes(pluginUrl: String): Int =
- votesCache[pluginUrl] ?: readVote(pluginUrl).also {
- votesCache[pluginUrl] = it
- }
+ votesCache[pluginUrl] ?: readVote(pluginUrl).also {
+ votesCache[pluginUrl] = it
+ }
fun hasVoted(pluginUrl: String) =
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
- fun canVote(pluginUrl: String): Boolean =
- PluginManager.urlPlugins.contains(pluginUrl)
+ fun canVote(pluginUrl: String): Boolean {
+ return PluginManager.urlPlugins.contains(pluginUrl)
+ }
private val voteLock = Mutex()
-
suspend fun vote(pluginUrl: String): Int {
+ // Prevent multiple requests at the same time.
voteLock.withLock {
if (!canVote(pluginUrl)) {
main {
- Toast.makeText(
- context,
- R.string.extension_install_first,
- Toast.LENGTH_SHORT
- ).show()
+ Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT)
+ .show()
}
return getVotes(pluginUrl)
}
if (hasVoted(pluginUrl)) {
main {
- Toast.makeText(
- context,
- R.string.already_voted,
- Toast.LENGTH_SHORT
- ).show()
+ Toast.makeText(context, R.string.already_voted, Toast.LENGTH_SHORT)
+ .show()
}
return getVotes(pluginUrl)
}
+
if (writeVote(pluginUrl)) {
setKey("cs3-votes/${transformUrl(pluginUrl)}", true)
votesCache[pluginUrl] = votesCache[pluginUrl]?.plus(1) ?: 1
@@ -91,8 +102,7 @@ object VotingApi {
}
}
- private data class CountifyResult(
- val id: String? = null,
- val count: Int? = null
+ private data class Result(
+ val value: Int?
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt
index f130831c6..4ef841f58 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt
@@ -1,8 +1,6 @@
package com.lagradost.cloudstream3.services
import android.content.Context
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
-import android.os.Build.VERSION.SDK_INT
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
@@ -84,11 +82,12 @@ class BackupWorkManager(val context: Context, workerParams: WorkerParameters) :
BACKUP_CHANNEL_DESCRIPTION
)
- val foregroundInfo = if (SDK_INT >= 29)
+ setForeground(
ForegroundInfo(
- BACKUP_NOTIFICATION_ID, backupNotificationBuilder.build(), FOREGROUND_SERVICE_TYPE_DATA_SYNC
- ) else ForegroundInfo(BACKUP_NOTIFICATION_ID, backupNotificationBuilder.build())
- setForeground(foregroundInfo)
+ BACKUP_NOTIFICATION_ID,
+ backupNotificationBuilder.build()
+ )
+ )
BackupUtils.backup(context)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt
deleted file mode 100644
index e07747a86..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt
+++ /dev/null
@@ -1,279 +0,0 @@
-package com.lagradost.cloudstream3.services
-
-import android.Manifest
-import android.app.Service
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
-import android.os.Build.VERSION.SDK_INT
-import android.os.IBinder
-import android.util.Log
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
-import androidx.core.app.PendingIntentCompat
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
-import com.lagradost.cloudstream3.MainActivity
-import com.lagradost.cloudstream3.MainActivity.Companion.lastError
-import com.lagradost.cloudstream3.MainActivity.Companion.setLastError
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.mvvm.debugAssert
-import com.lagradost.cloudstream3.mvvm.debugWarning
-import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.safe
-import com.lagradost.cloudstream3.plugins.PluginManager
-import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
-import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
-import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
-import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
-import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESUME_IN_QUEUE
-import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESUME_PACKAGES
-import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.downloadEvent
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.takeWhile
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.flow.updateAndGet
-import kotlinx.coroutines.withTimeoutOrNull
-import kotlin.system.measureTimeMillis
-import kotlin.time.Duration.Companion.milliseconds
-import kotlin.time.Duration.Companion.seconds
-
-class DownloadQueueService : Service() {
- companion object {
- const val TAG = "DownloadQueueService"
- const val DOWNLOAD_QUEUE_CHANNEL_ID = "cloudstream3.download.queue"
- const val DOWNLOAD_QUEUE_CHANNEL_NAME = "Download queue service"
- const val DOWNLOAD_QUEUE_CHANNEL_DESCRIPTION = "App download queue notification."
- const val DOWNLOAD_QUEUE_NOTIFICATION_ID = 917194232 // Random unique
- @Volatile
- var isRunning = false
-
- fun getIntent(
- context: Context,
- ): Intent {
- return Intent(context, DownloadQueueService::class.java)
- }
-
- private val _downloadInstances: MutableStateFlow> =
- MutableStateFlow(emptyList())
-
- /** Flow of all active downloads, not queued. May temporarily contain completed or failed EpisodeDownloadInstances.
- * Completed or failed instances are automatically removed by the download queue service.
- *
- */
- val downloadInstances: StateFlow> =
- _downloadInstances
-
- private val totalDownloadFlow =
- downloadInstances.combine(DownloadQueueManager.queue) { instances, queue ->
- instances to queue
- }
- .combine(VideoDownloadManager.currentDownloads) { (instances, queue), currentDownloads ->
- Triple(instances, queue, currentDownloads)
- }
- }
-
-
- private val baseNotification by lazy {
- val intent = Intent(this, MainActivity::class.java)
- val pendingIntent =
- PendingIntentCompat.getActivity(this, 0, intent, 0, false)
-
- val activeDownloads = resources.getQuantityString(R.plurals.downloads_active, 0).format(0)
- val activeQueue = resources.getQuantityString(R.plurals.downloads_queued, 0).format(0)
-
- NotificationCompat.Builder(this, DOWNLOAD_QUEUE_CHANNEL_ID)
- .setOngoing(true) // Make it persistent
- .setAutoCancel(false)
- .setColorized(false)
- .setOnlyAlertOnce(true)
- .setSilent(true)
- .setShowWhen(false)
- // If low priority then the notification might not show :(
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setColor(this.colorFromAttribute(R.attr.colorPrimary))
- .setContentText(activeDownloads)
- .setSubText(activeQueue)
- .setContentIntent(pendingIntent)
- .setSmallIcon(R.drawable.download_icon_load)
- }
-
-
- private fun updateNotification(context: Context, downloads: Int, queued: Int) {
- if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
- != PackageManager.PERMISSION_GRANTED
- ) return
-
- val activeDownloads =
- resources.getQuantityString(R.plurals.downloads_active, downloads).format(downloads)
- val activeQueue =
- resources.getQuantityString(R.plurals.downloads_queued, queued).format(queued)
-
- val newNotification = baseNotification
- .setContentText(activeDownloads)
- .setSubText(activeQueue)
- .build()
-
- safe {
- NotificationManagerCompat.from(context)
- .notify(DOWNLOAD_QUEUE_NOTIFICATION_ID, newNotification)
- }
- }
-
- // We always need to listen to events, even before the download is launched.
- // Stopping link loading is an event which can trigger before downloading.
- val downloadEventListener = { event: Pair ->
- when (event.second) {
- VideoDownloadManager.DownloadActionType.Stop -> {
- removeKey(KEY_RESUME_PACKAGES, event.first.toString())
- removeKey(KEY_RESUME_IN_QUEUE, event.first.toString())
- DownloadQueueManager.cancelDownload(event.first)
- }
-
- else -> {}
- }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
- override fun onCreate() {
- isRunning = true
- val context: Context = this // To make code more readable
-
- Log.d(TAG, "Download queue service started.")
- this.createNotificationChannel(
- DOWNLOAD_QUEUE_CHANNEL_ID,
- DOWNLOAD_QUEUE_CHANNEL_NAME,
- DOWNLOAD_QUEUE_CHANNEL_DESCRIPTION
- )
- if (SDK_INT >= 29) {
- startForeground(
- DOWNLOAD_QUEUE_NOTIFICATION_ID,
- baseNotification.build(),
- FOREGROUND_SERVICE_TYPE_DATA_SYNC
- )
- } else {
- startForeground(DOWNLOAD_QUEUE_NOTIFICATION_ID, baseNotification.build())
- }
-
- downloadEvent += downloadEventListener
-
- val queueJob = ioSafe {
- // Ensure this is up to date to prevent race conditions with MainActivity launches
- setLastError(context)
- // Early return, to prevent waiting for plugins in safe mode
- if (lastError != null) return@ioSafe
-
- // Try to ensure all plugins are loaded before starting the downloader.
- // To prevent infinite stalls we use a timeout of 15 seconds, it is judged as long enough
- val timeout = 15.seconds
- val timeTaken = withTimeoutOrNull(timeout) {
- measureTimeMillis {
- while (!(PluginManager.loadedOnlinePlugins && PluginManager.loadedLocalPlugins)) {
- delay(100.milliseconds)
- }
- }
- }
-
- debugWarning({ timeTaken == null || timeTaken > 3_000 }, {
- "Abnormally long downloader startup time of: ${timeTaken ?: timeout.inWholeMilliseconds}ms"
- })
- debugAssert({ timeTaken == null }, { "Downloader startup should not time out" })
-
- totalDownloadFlow
- .debounce { (instances, queue) ->
- // Filter away incorrect transient queue states.
- // For example when we pop the queue and add a download instance there exists a transient state where
- // there is no queue and no download instances (leading to an early exit)
- if (instances.isEmpty() && queue.isEmpty()) {
- 500.milliseconds
- } else {
- 0.milliseconds
- }
- }
- .takeWhile { (instances, queue) ->
- // Stop if destroyed
- isRunning
- // Run as long as there is a queue to process
- && (instances.isNotEmpty() || queue.isNotEmpty())
- // Run as long as there are no app crashes
- && lastError == null
- }
- .collect { (_, queue, currentDownloads) ->
- // Remove completed or failed
- val newInstances = _downloadInstances.updateAndGet { currentInstances ->
- currentInstances.filterNot { it.isCompleted || it.isFailed || it.isCancelled }
- }
-
- val maxDownloads = VideoDownloadManager.maxConcurrentDownloads(context)
- val currentInstanceCount = newInstances.size
-
- val newDownloads = minOf(
- // Cannot exceed the max downloads
- maxOf(0, maxDownloads - currentInstanceCount),
- // Cannot start more downloads than the queue size
- queue.size
- )
-
- // Cant start multiple downloads at once. If this is rerun it may start too many downloads.
- if (newDownloads > 0) {
- _downloadInstances.update { instances ->
- val downloadInstance = DownloadQueueManager.popQueue(context)
- if (downloadInstance != null) {
- downloadInstance.startDownload()
- instances + downloadInstance
- } else {
- instances
- }
- }
- }
-
- // The downloads actually displayed to the user with a notification
- val currentVisualDownloads =
- currentDownloads.size + newInstances.count {
- currentDownloads.contains(it.downloadQueueWrapper.id)
- .not()
- }
- // Just the queue
- val currentVisualQueue = queue.size
-
- updateNotification(context, currentVisualDownloads, currentVisualQueue)
- }
- }
-
- // Stop self regardless of job outcome
- queueJob.invokeOnCompletion { throwable ->
- if (throwable != null) {
- logError(throwable)
- }
- safe {
- stopSelf()
- }
- }
- }
-
- override fun onDestroy() {
- Log.d(TAG, "Download queue service stopped.")
- downloadEvent -= downloadEventListener
- isRunning = false
- super.onDestroy()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- return START_STICKY // We want the service restarted if its killed
- }
-
- override fun onBind(intent: Intent?): IBinder? = null
-
- override fun onTimeout(reason: Int) {
- stopSelf()
- Log.e(TAG, "Service stopped due to timeout: $reason")
- }
-
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
index 7134650ed..00c74dfff 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
@@ -1,12 +1,12 @@
package com.lagradost.cloudstream3.services
+import android.annotation.SuppressLint
import android.app.NotificationManager
+import android.app.PendingIntent
import android.content.Context
import android.content.Intent
-import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
-import android.os.Build.VERSION.SDK_INT
+import android.os.Build
import androidx.core.app.NotificationCompat
-import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import androidx.work.*
import com.lagradost.cloudstream3.*
@@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager
-import com.lagradost.cloudstream3.utils.txt
+import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
@@ -22,7 +22,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.getImageBitmapFromUrl
+import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.TimeUnit
@@ -75,7 +75,7 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(context.getString(R.string.subscription_in_progress_notification))
- .setSmallIcon(com.google.android.gms.cast.framework.R.drawable.quantum_ic_refresh_white_24)
+ .setSmallIcon(R.drawable.quantum_ic_refresh_white_24)
.setProgress(0, 0, true)
private val updateNotificationBuilder =
@@ -98,6 +98,7 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
)
}
+ @SuppressLint("UnspecifiedImmutableFlag")
override suspend fun doWork(): Result {
try {
// println("Update subscriptions!")
@@ -107,13 +108,12 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
SUBSCRIPTION_CHANNEL_DESCRIPTION
)
- val foregroundInfo = if (SDK_INT >= 29)
+ setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
- progressNotificationBuilder.build(),
- FOREGROUND_SERVICE_TYPE_DATA_SYNC
- ) else ForegroundInfo(SUBSCRIPTION_NOTIFICATION_ID, progressNotificationBuilder.build(),)
- setForeground(foregroundInfo)
+ progressNotificationBuilder.build()
+ )
+ )
val subscriptions = getAllSubscriptions()
@@ -128,18 +128,18 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
updateProgress(max, progress, true)
// We need all plugins loaded.
- PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context)
- PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(context, false)
+ PluginManager.loadAllOnlinePlugins(context)
+ PluginManager.loadAllLocalPlugins(context, false)
- subscriptions.amap { savedData ->
+ subscriptions.apmap { savedData ->
try {
- val id = savedData.id ?: return@amap null
- val api = getApiFromNameNull(savedData.apiName) ?: return@amap null
+ val id = savedData.id ?: return@apmap null
+ val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
// Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse
- } ?: return@amap null
+ } ?: return@apmap null
val dubPreference =
getDub(id) ?: if (
@@ -183,10 +183,19 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }.putExtra(MainActivity.API_NAME_EXTRA_KEY, api.name)
+ }
val pendingIntent =
- PendingIntentCompat.getActivity(context, 0, intent, 0, false)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ } else {
+ PendingIntent.getActivity(context, 0, intent, 0)
+ }
val poster = ioWork {
savedData.posterUrl?.let { url ->
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
index d63b18cdc..6151a0edd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
@@ -2,13 +2,12 @@ package com.lagradost.cloudstream3.services
import android.app.Service
import android.content.Intent
import android.os.IBinder
-import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
+import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
-/** Handle notification actions such as pause/resume downloads */
class VideoDownloadService : Service() {
private val downloadScope = CoroutineScope(Dispatchers.Default)
@@ -43,3 +42,19 @@ class VideoDownloadService : Service() {
super.onDestroy()
}
}
+// override fun onHandleIntent(intent: Intent?) {
+// if (intent != null) {
+// val id = intent.getIntExtra("id", -1)
+// val type = intent.getStringExtra("type")
+// if (id != -1 && type != null) {
+// val state = when (type) {
+// "resume" -> VideoDownloadManager.DownloadActionType.Resume
+// "pause" -> VideoDownloadManager.DownloadActionType.Pause
+// "stop" -> VideoDownloadManager.DownloadActionType.Stop
+// else -> return
+// }
+// VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
+// }
+// }
+// }
+//}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
index 9e6f241fb..df64caabc 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
@@ -1,8 +1,12 @@
package com.lagradost.cloudstream3.subtitles
+import androidx.annotation.WorkerThread
import androidx.core.net.toUri
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
+import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
+import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
import okio.BufferedSource
import okio.buffer
@@ -11,6 +15,32 @@ import okio.source
import java.io.File
import java.util.zip.ZipInputStream
+interface AbstractSubProvider {
+ val idPrefix: String
+
+ @WorkerThread
+ suspend fun search(query: SubtitleSearch): List? {
+ throw NotImplementedError()
+ }
+
+ @WorkerThread
+ suspend fun load(data: SubtitleEntity): String? {
+ throw NotImplementedError()
+ }
+
+ @WorkerThread
+ suspend fun SubtitleResource.getResources(data: SubtitleEntity) {
+ this.addUrl(load(data))
+ }
+
+ @WorkerThread
+ suspend fun getResource(data: SubtitleEntity): SubtitleResource {
+ return SubtitleResource().apply {
+ this.getResources(data)
+ }
+ }
+}
+
/**
* A builder for subtitle files.
* @see addUrl
@@ -91,3 +121,4 @@ class SubtitleResource {
}
}
+interface AbstractSubApi : AbstractSubProvider, AuthAPI
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
index 3bc5f2733..2e14c3c46 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
@@ -1,165 +1,149 @@
-package com.lagradost.cloudstream3.syncproviders
-
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
-import com.lagradost.cloudstream3.LoadResponse
-import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed
-import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
-import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi
-import com.lagradost.cloudstream3.syncproviders.providers.LocalList
-import com.lagradost.cloudstream3.syncproviders.providers.MALApi
-import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
-import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
-import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi
-import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi
-import com.lagradost.cloudstream3.utils.DataStoreHelper
-import com.lagradost.cloudstream3.utils.videoskip.AnimeSkipAuth
-import java.util.concurrent.TimeUnit
-
-abstract class AccountManager {
- companion object {
- const val NONE_ID: Int = -1
- val malApi = MALApi()
- val kitsuApi = KitsuApi()
- val aniListApi = AniListApi()
- val simklApi = SimklApi()
- val localListApi = LocalList()
-
- val openSubtitlesApi = OpenSubtitlesApi()
- val addic7ed = Addic7ed()
- val subDlApi = SubDlApi()
- val subSourceApi = SubSourceApi()
- val animeSkipApi = AnimeSkipAuth()
-
- var cachedAccounts: MutableMap>
- var cachedAccountIds: MutableMap
-
- const val ACCOUNT_TOKEN = "auth_tokens"
- const val ACCOUNT_IDS = "auth_ids"
-
- fun accounts(prefix: String): Array {
- require(prefix != "NONE")
- return getKey>(
- ACCOUNT_TOKEN,
- "${prefix}/${DataStoreHelper.currentAccount}"
- ) ?: arrayOf()
- }
-
- fun updateAccounts(prefix: String, array: Array) {
- require(prefix != "NONE")
- setKey(ACCOUNT_TOKEN, "${prefix}/${DataStoreHelper.currentAccount}", array)
- synchronized(cachedAccounts) {
- cachedAccounts[prefix] = array
- }
- }
-
- fun updateAccountsId(prefix: String, id: Int) {
- require(prefix != "NONE")
- setKey(ACCOUNT_IDS, "${prefix}/${DataStoreHelper.currentAccount}", id)
- synchronized(cachedAccountIds) {
- cachedAccountIds[prefix] = id
- }
- }
-
- val allApis = arrayOf(
- SyncRepo(malApi),
- SyncRepo(kitsuApi),
- SyncRepo(aniListApi),
- SyncRepo(simklApi),
- SyncRepo(localListApi),
- SubtitleRepo(openSubtitlesApi),
- SubtitleRepo(addic7ed),
- SubtitleRepo(subDlApi),
- PlainAuthRepo(animeSkipApi)
- )
-
- fun updateAccountIds() {
- val ids = mutableMapOf()
- for (api in allApis) {
- ids.put(
- api.idPrefix,
- getKey(
- ACCOUNT_IDS,
- "${api.idPrefix}/${DataStoreHelper.currentAccount}",
- NONE_ID
- ) ?: NONE_ID
- )
- }
- synchronized(cachedAccountIds) {
- cachedAccountIds = ids
- }
- }
-
- init {
- val data = mutableMapOf>()
- val ids = mutableMapOf()
- for (api in allApis) {
- data.put(api.idPrefix, accounts(api.idPrefix))
- ids.put(
- api.idPrefix,
- getKey(
- ACCOUNT_IDS,
- "${api.idPrefix}/${DataStoreHelper.currentAccount}",
- NONE_ID
- ) ?: NONE_ID
- )
- }
- cachedAccounts = data
- cachedAccountIds = ids
- }
-
- // I do not want to place this in the init block as JVM initialization order is weird, and it may cause exceptions
- // accessing other classes
- fun initMainAPI() {
- LoadResponse.malIdPrefix = malApi.idPrefix
- LoadResponse.kitsuIdPrefix = kitsuApi.idPrefix
- LoadResponse.aniListIdPrefix = aniListApi.idPrefix
- LoadResponse.simklIdPrefix = simklApi.idPrefix
- }
-
- val subtitleProviders = arrayOf(
- SubtitleRepo(openSubtitlesApi),
- SubtitleRepo(addic7ed),
- SubtitleRepo(subDlApi)
- )
- val syncApis = arrayOf(
- SyncRepo(malApi),
- SyncRepo(kitsuApi),
- SyncRepo(aniListApi),
- SyncRepo(simklApi),
- SyncRepo(localListApi)
- )
-
- const val APP_STRING = "cloudstreamapp"
- const val APP_STRING_REPO = "cloudstreamrepo"
- const val APP_STRING_PLAYER = "cloudstreamplayer"
-
- // Instantly start the search given a query
- const val APP_STRING_SEARCH = "cloudstreamsearch"
-
- // Instantly resume watching a show
- const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
-
- const val APP_STRING_SHARE = "csshare"
-
- fun secondsToReadable(seconds: Int, completedValue: String): String {
- var secondsLong = seconds.toLong()
- val days = TimeUnit.SECONDS
- .toDays(secondsLong)
- secondsLong -= TimeUnit.DAYS.toSeconds(days)
-
- val hours = TimeUnit.SECONDS
- .toHours(secondsLong)
- secondsLong -= TimeUnit.HOURS.toSeconds(hours)
-
- val minutes = TimeUnit.SECONDS
- .toMinutes(secondsLong)
- secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
- if (minutes < 0) {
- return completedValue
- }
- //println("$days $hours $minutes")
- return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
- }
- }
-}
\ No newline at end of file
+package com.lagradost.cloudstream3.syncproviders
+
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
+import com.lagradost.cloudstream3.LoadResponse
+import com.lagradost.cloudstream3.syncproviders.providers.*
+import java.util.concurrent.TimeUnit
+
+abstract class AccountManager(private val defIndex: Int) : AuthAPI {
+ companion object {
+ val malApi = MALApi(0).also { api ->
+ LoadResponse.Companion.malIdPrefix = api.idPrefix
+ }
+ val aniListApi = AniListApi(0).also { api ->
+ LoadResponse.Companion.aniListIdPrefix = api.idPrefix
+ }
+ val simklApi = SimklApi(0).also { api ->
+ LoadResponse.Companion.simklIdPrefix = api.idPrefix
+ }
+ val openSubtitlesApi = OpenSubtitlesApi(0)
+ val addic7ed = Addic7ed()
+ val subDlApi = SubDlApi(0)
+ val localListApi = LocalList()
+ val subSourceApi = SubSourceApi()
+
+ // used to login via app intent
+ val OAuth2Apis
+ get() = listOf(
+ malApi, aniListApi, simklApi
+ )
+
+ // this needs init with context and can be accessed in settings
+ val accountManagers
+ get() = listOf(
+ malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi
+ )
+
+ // used for active syncing
+ val SyncApis
+ get() = listOf(
+ SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi)
+ )
+
+ val inAppAuths
+ get() = listOf(
+ openSubtitlesApi,
+ subDlApi
+ )//, nginxApi)
+
+ val subtitleProviders
+ get() = listOf(
+ openSubtitlesApi,
+ addic7ed,
+ subDlApi,
+ subSourceApi
+ )
+
+ const val APP_STRING = "cloudstreamapp"
+ const val APP_STRING_REPO = "cloudstreamrepo"
+ const val APP_STRING_PLAYER = "cloudstreamplayer"
+
+ // Instantly start the search given a query
+ const val APP_STRING_SEARCH = "cloudstreamsearch"
+
+ // Instantly resume watching a show
+ const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
+
+ val unixTime: Long
+ get() = System.currentTimeMillis() / 1000L
+ val unixTimeMs: Long
+ get() = System.currentTimeMillis()
+
+ const val MAX_STALE = 60 * 10
+
+ fun secondsToReadable(seconds: Int, completedValue: String): String {
+ var secondsLong = seconds.toLong()
+ val days = TimeUnit.SECONDS
+ .toDays(secondsLong)
+ secondsLong -= TimeUnit.DAYS.toSeconds(days)
+
+ val hours = TimeUnit.SECONDS
+ .toHours(secondsLong)
+ secondsLong -= TimeUnit.HOURS.toSeconds(hours)
+
+ val minutes = TimeUnit.SECONDS
+ .toMinutes(secondsLong)
+ secondsLong -= TimeUnit.MINUTES.toSeconds(minutes)
+ if (minutes < 0) {
+ return completedValue
+ }
+ //println("$days $hours $minutes")
+ return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
+ }
+ }
+
+ var accountIndex = defIndex
+ private var lastAccountIndex = defIndex
+ protected val accountId get() = "${idPrefix}_account_$accountIndex"
+ private val accountActiveKey get() = "${idPrefix}_active"
+
+ // int array of all accounts indexes
+ private val accountsKey get() = "${idPrefix}_accounts"
+
+ protected fun removeAccountKeys() {
+ removeKeys(accountId)
+ val accounts = getAccounts()?.toMutableList() ?: mutableListOf()
+ accounts.remove(accountIndex)
+ setKey(accountsKey, accounts.toIntArray())
+
+ init()
+ }
+
+ fun getAccounts(): IntArray? {
+ return getKey(accountsKey, intArrayOf())
+ }
+
+ fun init() {
+ accountIndex = getKey(accountActiveKey, defIndex)!!
+ val accounts = getAccounts()
+ if (accounts?.isNotEmpty() == true && this.loginInfo() == null) {
+ accountIndex = accounts.first()
+ }
+ }
+
+ protected fun switchToNewAccount() {
+ val accounts = getAccounts()
+ lastAccountIndex = accountIndex
+ accountIndex = (accounts?.maxOrNull() ?: 0) + 1
+ }
+ protected fun switchToOldAccount() {
+ accountIndex = lastAccountIndex
+ }
+
+ protected fun registerAccount() {
+ setKey(accountActiveKey, accountIndex)
+ val accounts = getAccounts()?.toMutableList() ?: mutableListOf()
+ if (!accounts.contains(accountIndex)) {
+ accounts.add(accountIndex)
+ }
+
+ setKey(accountsKey, accounts.toIntArray())
+ }
+
+ fun changeAccount(index: Int) {
+ accountIndex = index
+ setKey(accountActiveKey, index)
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt
index 184a9fbcc..8b085bc0b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthAPI.kt
@@ -1,280 +1,23 @@
package com.lagradost.cloudstream3.syncproviders
-import android.util.Base64
-import androidx.annotation.WorkerThread
-import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.APIHolder.unixTime
-import com.lagradost.cloudstream3.ActorData
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
-import com.lagradost.cloudstream3.CommonActivity.showToast
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.LoadResponse
-import com.lagradost.cloudstream3.NextAiring
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.Score
-import com.lagradost.cloudstream3.SearchQuality
-import com.lagradost.cloudstream3.SearchResponse
-import com.lagradost.cloudstream3.ShowStatus
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.mvvm.Resource
-import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.safe
-import com.lagradost.cloudstream3.mvvm.safeApiCall
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID
-import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed
-import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
-import com.lagradost.cloudstream3.syncproviders.providers.LocalList
-import com.lagradost.cloudstream3.syncproviders.providers.MALApi
-import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi
-import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
-import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
-import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi
-import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi
-import com.lagradost.cloudstream3.ui.SyncWatchType
-import com.lagradost.cloudstream3.ui.library.ListSorting
-import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
-import com.lagradost.cloudstream3.utils.DataStoreHelper
-import com.lagradost.cloudstream3.utils.UiText
-import com.lagradost.cloudstream3.utils.txt
-import java.net.URL
-import java.security.SecureRandom
-import java.util.Date
-import java.util.concurrent.TimeUnit
+interface AuthAPI {
+ val name: String
+ val icon: Int?
-data class AuthLoginPage(
- /** The website to open to authenticate */
- val url: String,
- /**
- * State/control code to verify against the redirectUrl to make sure the request is valid.
- * This parameter will be saved, and then used in AuthAPI::login.
- * */
- val payload: String? = null,
-)
+ val requiresLogin: Boolean
-data class AuthToken(
- /**
- * This is the general access tokens/api token representing a logged in user.
- *
- * `Access tokens are the thing that applications use to make API requests on behalf of a user.`
- * */
- @JsonProperty("accessToken")
- val accessToken: String? = null,
- /**
- * For OAuth a special refresh token is issues to refresh the access token.
- * */
- @JsonProperty("refreshToken")
- val refreshToken: String? = null,
- /** In UnixTime (sec) when it expires */
- @JsonProperty("accessTokenLifetime")
- val accessTokenLifetime: Long? = null,
- /** In UnixTime (sec) when it expires */
- @JsonProperty("refreshTokenLifetime")
- val refreshTokenLifetime: Long? = null,
- /** Sometimes AuthToken needs to be customized to store e.g. username/password,
- * this acts as a catch all to store text or JSON data. */
- @JsonProperty("payload")
- val payload: String? = null,
-) {
- fun isAccessTokenExpired(marginSec: Long = 10L) =
- accessTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= accessTokenLifetime
+ val createAccountUrl : String?
- fun isRefreshTokenExpired(marginSec: Long = 10L) =
- refreshTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= refreshTokenLifetime
-}
+ // don't change this as all keys depend on it
+ val idPrefix: String
-data class AuthUser(
- /** Account display-name, can also be email if name does not exist */
- @JsonProperty("name")
- val name: String?,
- /** Unique account identifier,
- * if a subsequent login is done then it will be refused if another account with the same id exists*/
- @JsonProperty("id")
- val id: Int,
- /** Profile picture URL */
- @JsonProperty("profilePicture")
- val profilePicture: String? = null,
- /** Profile picture Headers of the URL */
- @JsonProperty("profilePictureHeader")
- val profilePictureHeaders: Map? = null
-)
+ // if this returns null then you are not logged in
+ fun loginInfo(): LoginInfo?
+ fun logOut()
-/**
- * Stores all information that should be used to authorize access.
- * Be aware that token and user may change independently when a refresh is needed,
- * and as such there should be no strong pairing between the two.
- *
- * Any local set/get key should use user.id.toString(),
- * as token.accessToken (even hashed) is unsecure, and will rotate.
- * */
-data class AuthData(
- @JsonProperty("user")
- val user: AuthUser,
- @JsonProperty("token")
- val token: AuthToken,
-)
-
-data class AuthPinData(
- val deviceCode: String,
- val userCode: String,
- /** QR Code url */
- val verificationUrl: String,
- /** In seconds */
- val expiresIn: Int,
- /** Check if the code has been verified interval */
- val interval: Int,
-)
-
-/** The login field requirements to display to the user */
-data class AuthLoginRequirement(
- val password: Boolean = false,
- val username: Boolean = false,
- val email: Boolean = false,
- val server: Boolean = false,
-)
-
-/** What the user responds to the AuthLoginRequirement */
-data class AuthLoginResponse(
- @JsonProperty("password")
- val password: String?,
- @JsonProperty("username")
- val username: String?,
- @JsonProperty("email")
- val email: String?,
- @JsonProperty("server")
- val server: String?,
-)
-
-/** Stateless Authentication class used for all personalized content */
-abstract class AuthAPI {
- open val name: String = "NONE"
- open val idPrefix: String = "NONE"
-
- /** Drawable icon of the service */
- open val icon: Int? = null
-
- /** If this service requires an account to use */
- open val requiresLogin: Boolean = true
-
- /** Link to a website for creating a new account */
- open val createAccountUrl: String? = null
-
- /** The sensitive redirect URL from OAuth should contain "/redirectUrlIdentifier" to trigger the login */
- open val redirectUrlIdentifier: String? = null
-
- /** Has OAuth2 login support, including login, loginRequest and refreshToken */
- open val hasOAuth2: Boolean = false
-
- /** Has on device pin support, aka login with a QR code */
- open val hasPin: Boolean = false
-
- /** Has in app login support, aka login with a dialog */
- open val hasInApp: Boolean = false
-
- /** The requirements to login in app */
- open val inAppLoginRequirement: AuthLoginRequirement? = null
-
- companion object {
- val unixTime: Long
- get() = System.currentTimeMillis() / 1000L
- val unixTimeMs: Long
- get() = System.currentTimeMillis()
-
- fun splitRedirectUrl(redirectUrl: String): Map {
- return splitQuery(
- URL(
- redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
- )
- )
- }
-
- fun generateCodeVerifier(): String {
- // It is recommended to use a URL-safe string as code_verifier.
- // See section 4 of RFC 7636 for more details.
- val secureRandom = SecureRandom()
- val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128
- secureRandom.nextBytes(codeVerifierBytes)
- return Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=')
- .replace("+", "-")
- .replace("/", "_").replace("\n", "")
- }
- }
-
- /** Is this url a valid redirect url for this service? */
- @Throws
- open fun isValidRedirectUrl(url: String): Boolean =
- redirectUrlIdentifier != null && url.contains("/$redirectUrlIdentifier")
-
- /** OAuth2 login from a valid redirectUrl, and payload given in loginRequest */
- @Throws
- open suspend fun login(redirectUrl: String, payload: String?): AuthToken? =
- throw NotImplementedError()
-
- /** OAuth2 login request, asking the service to provide a url to open in the browser */
- @Throws
- open fun loginRequest(): AuthLoginPage? = throw NotImplementedError()
-
- /** Pin login request, asking the service to provide an verificationUrl to display with a QR code */
- @Throws
- open suspend fun pinRequest(): AuthPinData? = throw NotImplementedError()
-
- /** OAuth2 token refresh, this ensures that all token passed to other functions will be valid */
- @Throws
- open suspend fun refreshToken(token: AuthToken): AuthToken? = throw NotImplementedError()
-
- /** Pin login, this will be called periodically while logging in to check if the pin has been verified by the user */
- @Throws
- open suspend fun login(payload: AuthPinData): AuthToken? = throw NotImplementedError()
-
- /** In app login */
- @Throws
- open suspend fun login(form: AuthLoginResponse): AuthToken? = throw NotImplementedError()
-
- /** Get the visible user account */
- @Throws
- open suspend fun user(token: AuthToken?): AuthUser? = throw NotImplementedError()
-
- /**
- * An optional security measure to make sure that even if an attacker gets ahold of the token, it will be invalid.
- *
- * Note that this will currently only be called *once* on logout,
- * and as such any network issues it will fail silently, and the token will not be revoked.
- **/
- @Throws
- open suspend fun invalidateToken(token: AuthToken): Nothing = throw NotImplementedError()
-
- @Throws
- @Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
- fun toRepo(): AuthRepo = when (this) {
- is SubtitleAPI -> SubtitleRepo(this)
- is SyncAPI -> SyncRepo(this)
- else -> throw NotImplementedError("Unknown inheritance from AuthAPI")
- }
-
- @Suppress("DEPRECATION_ERROR")
- @Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
- fun loginInfo(): LoginInfo? {
- return this.toRepo().authUser()?.let { user ->
- LoginInfo(
- profilePicture = user.profilePicture,
- name = user.name,
- accountIndex = -1,
- )
- }
- }
-
- @Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
- suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
- @Suppress("DEPRECATION_ERROR")
- return (this.toRepo() as? SyncRepo)?.library()?.getOrThrow()
- }
-
- @Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
class LoginInfo(
val profilePicture: String? = null,
val name: String?,
val accountIndex: Int,
)
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt
deleted file mode 100644
index 645a19e3a..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AuthRepo.kt
+++ /dev/null
@@ -1,168 +0,0 @@
-package com.lagradost.cloudstream3.syncproviders
-
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser
-import com.lagradost.cloudstream3.CommonActivity.showToast
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.safe
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID
-import com.lagradost.cloudstream3.utils.txt
-
-/** General-purpose repo */
-class PlainAuthRepo(api: AuthAPI) : AuthRepo(api)
-
-/** Safe abstraction for AuthAPI that provides both a catching interface, and automatic token management. */
-abstract class AuthRepo(open val api: AuthAPI) {
- fun isValidRedirectUrl(url: String) = safe { api.isValidRedirectUrl(url) } ?: false
- val idPrefix get() = api.idPrefix
- val name get() = api.name
- val icon get() = api.icon
- val requiresLogin get() = api.requiresLogin
- val createAccountUrl get() = api.createAccountUrl
- val hasOAuth2 get() = api.hasOAuth2
- val hasPin get() = api.hasPin
- val hasInApp get() = api.hasInApp
- val inAppLoginRequirement get() = api.inAppLoginRequirement
- val isAvailable get() = !api.requiresLogin || authUser() != null
-
- companion object {
- private val oauthPayload: MutableMap = mutableMapOf()
- }
-
- @Throws
- protected suspend fun freshAuth(): AuthData? {
- val data = authData() ?: return null
- if (data.token.isAccessTokenExpired()) {
- val newToken = api.refreshToken(data.token) ?: return null
- val newAuth = AuthData(user = data.user, token = newToken)
- refreshUser(newAuth)
- return newAuth
- }
- return data
- }
-
- @Throws
- fun openOAuth2Page(): Boolean {
- val page = api.loginRequest() ?: return false
- synchronized(oauthPayload) {
- oauthPayload.put(idPrefix, page.payload)
- }
- openBrowser(page.url)
- return true
- }
-
- fun openOAuth2PageWithToast() {
- try {
- if (!openOAuth2Page()) {
- showToast(txt(R.string.authenticated_user_fail, api.name))
- }
- } catch (t: Throwable) {
- logError(t)
- if (t is ErrorLoadingException && t.message != null) {
- showToast(t.message)
- return
- }
- showToast(txt(R.string.authenticated_user_fail, api.name))
- }
- }
-
- suspend fun logout(from: AuthUser) {
- val currentAccounts = AccountManager.accounts(idPrefix)
- val (newAccounts, oldAccounts) = currentAccounts.partition { it.user.id != from.id }
- if (newAccounts.size < currentAccounts.size) {
- AccountManager.updateAccounts(idPrefix, newAccounts.toTypedArray())
- AccountManager.updateAccountsId(idPrefix, 0)
- }
-
- for (oldAccount in oldAccounts) {
- try {
- api.invalidateToken(oldAccount.token)
- } catch (_: NotImplementedError) {
- // no-op
- } catch (t: Throwable) {
- logError(t)
- }
- }
- }
-
- fun refreshUser(newAuth: AuthData) {
- val currentAccounts = AccountManager.accounts(idPrefix)
- val newAccounts = currentAccounts.map {
- if (it.user.id == newAuth.user.id) {
- newAuth
- } else {
- it
- }
- }.toTypedArray()
- AccountManager.updateAccounts(idPrefix, newAccounts)
- }
-
- fun authData(): AuthData? = synchronized(AccountManager.cachedAccountIds) {
- AccountManager.cachedAccountIds[idPrefix]?.let { id ->
- AccountManager.cachedAccounts[idPrefix]?.firstOrNull { data -> data.user.id == id }
- }
- }
-
- fun authToken(): AuthToken? = authData()?.token
-
- fun authUser(): AuthUser? = authData()?.user
-
- val accounts
- get() = synchronized(AccountManager.cachedAccounts) {
- AccountManager.cachedAccounts[idPrefix] ?: emptyArray()
- }
- var accountId
- get() = synchronized(AccountManager.cachedAccountIds) {
- AccountManager.cachedAccountIds[idPrefix] ?: NONE_ID
- }
- set(value) {
- AccountManager.updateAccountsId(idPrefix, value)
- }
-
- @Throws
- suspend fun pinRequest() =
- api.pinRequest()
-
- @Throws
- private suspend fun setupLogin(token: AuthToken): Boolean {
- val user = api.user(token) ?: return false
-
- val newAccount = AuthData(
- token = token,
- user = user,
- )
-
- val currentAccounts = AccountManager.accounts(idPrefix)
- if (currentAccounts.any { it.user.id == newAccount.user.id }) {
- throw ErrorLoadingException("Already logged into this account")
- }
-
- val newAccounts = currentAccounts + newAccount
- AccountManager.updateAccounts(idPrefix, newAccounts)
- AccountManager.updateAccountsId(idPrefix, user.id)
- if (this is SyncRepo) {
- requireLibraryRefresh = true
- }
- return true
- }
-
- @Throws
- suspend fun login(form: AuthLoginResponse): Boolean {
- return setupLogin(api.login(form) ?: return false)
- }
-
- @Throws
- suspend fun login(payload: AuthPinData): Boolean {
- return setupLogin(api.login(payload) ?: return false)
- }
-
- @Throws
- suspend fun login(redirectUrl: String): Boolean {
- return setupLogin(
- api.login(
- redirectUrl,
- synchronized(oauthPayload) { oauthPayload[api.idPrefix] }) ?: return false
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt
deleted file mode 100644
index 5efb88e5b..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/BackupAPI.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.lagradost.cloudstream3.syncproviders
-
-/** Work in progress */
-abstract class BackupAPI : AuthAPI() {
- open val filename : String = "cloudstream-backup.json"
-
- /** Get the backup file as a JSON string from the remote storage. Return null if not found/empty */
- @Throws
- open suspend fun downloadFile(auth: AuthData?) : String? = throw NotImplementedError()
-
- /** Get the backup file as a JSON string from the remote storage. */
- @Throws
- open suspend fun uploadFile(auth: AuthData?, data : String) : String? = throw NotImplementedError()
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/InAppAuthAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/InAppAuthAPI.kt
new file mode 100644
index 000000000..8b6fdf463
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/InAppAuthAPI.kt
@@ -0,0 +1,66 @@
+package com.lagradost.cloudstream3.syncproviders
+
+import androidx.annotation.WorkerThread
+
+interface InAppAuthAPI : AuthAPI {
+ data class LoginData(
+ val username: String? = null,
+ val password: String? = null,
+ val server: String? = null,
+ val email: String? = null,
+ )
+
+ // this is for displaying the UI
+ val requiresPassword: Boolean
+ val requiresUsername: Boolean
+ val requiresServer: Boolean
+ val requiresEmail: Boolean
+
+ // if this is false we can assume that getLatestLoginData returns null and wont be called
+ // this is used in case for some reason it is not preferred to store any login data besides the "token" or encrypted data
+ val storesPasswordInPlainText: Boolean
+
+ // return true if logged in successfully
+ suspend fun login(data: LoginData): Boolean
+
+ // used to fill the UI if you want to edit any data about your login info
+ fun getLatestLoginData(): LoginData?
+}
+
+abstract class InAppAuthAPIManager(defIndex: Int) : AccountManager(defIndex), InAppAuthAPI {
+ override val requiresPassword = false
+ override val requiresUsername = false
+ override val requiresEmail = false
+ override val requiresServer = false
+ override val storesPasswordInPlainText = true
+ override val requiresLogin = true
+
+ // runs on startup
+ @WorkerThread
+ open suspend fun initialize() {
+ }
+
+ override fun logOut() {
+ throw NotImplementedError()
+ }
+
+ override val idPrefix: String
+ get() = throw NotImplementedError()
+
+ override val name: String
+ get() = throw NotImplementedError()
+
+ override val icon: Int? = null
+
+ override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
+ throw NotImplementedError()
+ }
+
+ override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
+ throw NotImplementedError()
+ }
+
+ override fun loginInfo(): AuthAPI.LoginInfo? {
+ throw NotImplementedError()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt
new file mode 100644
index 000000000..3d0bb9402
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt
@@ -0,0 +1,27 @@
+package com.lagradost.cloudstream3.syncproviders
+
+import androidx.fragment.app.FragmentActivity
+
+interface OAuth2API : AuthAPI {
+ val key: String
+ val redirectUrl: String
+ val supportDeviceAuth: Boolean
+
+ suspend fun handleRedirect(url: String) : Boolean
+ fun authenticate(activity: FragmentActivity?)
+ suspend fun getDevicePin() : PinAuthData? {
+ return null
+ }
+
+ suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean {
+ return false
+ }
+
+ data class PinAuthData(
+ val deviceCode: String,
+ val userCode: String,
+ val verificationUrl: String,
+ val expiresIn: Int,
+ val interval: Int,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleAPI.kt
deleted file mode 100644
index a1149b5f8..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleAPI.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.lagradost.cloudstream3.syncproviders
-
-import androidx.annotation.WorkerThread
-import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
-import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
-import com.lagradost.cloudstream3.subtitles.SubtitleResource
-
-/**
- * Stateless subtitle class for external subtitles.
- *
- * All non-null `AuthToken` will be non-expired when each function is called.
- */
-abstract class SubtitleAPI : AuthAPI() {
- @WorkerThread
- @Throws
- open suspend fun search(auth: AuthData?, query: SubtitleSearch): List? =
- throw NotImplementedError()
-
- @WorkerThread
- @Throws
- open suspend fun load(auth: AuthData?, subtitle: SubtitleEntity): String? =
- throw NotImplementedError()
-
- @WorkerThread
- @Throws
- open suspend fun SubtitleResource.getResources(auth: AuthData?, subtitle: SubtitleEntity) {
- this.addUrl(load(auth, subtitle))
- }
-
- @WorkerThread
- @Throws
- suspend fun resource(auth: AuthData?, subtitle: SubtitleEntity): SubtitleResource {
- return SubtitleResource().apply {
- this.getResources(auth, subtitle)
- }
- }
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt
deleted file mode 100644
index 0b8c3e5ae..000000000
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SubtitleRepo.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.lagradost.cloudstream3.syncproviders
-
-import androidx.annotation.WorkerThread
-import com.lagradost.cloudstream3.APIHolder.unixTime
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
-import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
-import com.lagradost.cloudstream3.subtitles.SubtitleResource
-import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
-
-/** Stateless safe abstraction of SubtitleAPI */
-class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
- companion object {
- data class SavedSearchResponse(
- val unixTime: Long,
- val response: List,
- val query: SubtitleSearch
- )
-
- data class SavedResourceResponse(
- val unixTime: Long,
- val response: SubtitleResource,
- val query: SubtitleEntity
- )
-
- // maybe make this a generic struct? right now there is a lot of boilerplate
- private val searchCache = atomicListOf()
- private var searchCacheIndex: Int = 0
- private val resourceCache = atomicListOf()
- private var resourceCacheIndex: Int = 0
- const val CACHE_SIZE = 20
- }
-
- @WorkerThread
- suspend fun resource(data: SubtitleEntity): Result = runCatching {
- val cached = resourceCache.withLock {
- var found: SubtitleResource? = null
- for (item in resourceCache) {
- // 20 min save
- if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
- found = item.response
- break
- }
- }
- found
- }
- if (cached != null) return@runCatching cached
-
- val returnValue = api.resource(freshAuth(), data)
- resourceCache.withLock {
- val add = SavedResourceResponse(unixTime, returnValue, data)
- if (resourceCache.size > CACHE_SIZE) {
- resourceCache[resourceCacheIndex] = add // rolling cache
- resourceCacheIndex = (resourceCacheIndex + 1) % CACHE_SIZE
- } else {
- resourceCache.add(add)
- }
- }
- returnValue
- }
-
- @WorkerThread
- suspend fun search(query: SubtitleSearch): Result> {
- return runCatching {
- val cached = searchCache.withLock {
- var found: List? = null
- for (item in searchCache) {
- // 120 min save
- if (item.query == query && (unixTime - item.unixTime) < 60 * 120) {
- found = item.response
- break
- }
- }
- found
- }
-
- if (cached != null) return@runCatching cached
- val returnValue = api.search(freshAuth(), query) ?: emptyList()
-
- // only cache valid return values
- if (returnValue.isNotEmpty()) {
- val add = SavedSearchResponse(unixTime, returnValue, query)
- searchCache.withLock {
- if (searchCache.size > CACHE_SIZE) {
- searchCache[searchCacheIndex] = add // rolling cache
- searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE
- } else {
- searchCache.add(add)
- }
- }
- }
- returnValue
- }
- }
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
similarity index 61%
rename from app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt
rename to app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
index f30a64748..dcb8bbead 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
@@ -1,194 +1,170 @@
-package com.lagradost.cloudstream3.syncproviders
-
-import androidx.annotation.WorkerThread
-import com.lagradost.cloudstream3.ActorData
-import com.lagradost.cloudstream3.NextAiring
-import com.lagradost.cloudstream3.Score
-import com.lagradost.cloudstream3.SearchQuality
-import com.lagradost.cloudstream3.SearchResponse
-import com.lagradost.cloudstream3.ShowStatus
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.ui.SyncWatchType
-import com.lagradost.cloudstream3.ui.library.ListSorting
-import com.lagradost.cloudstream3.utils.Levenshtein
-import com.lagradost.cloudstream3.utils.UiText
-import java.util.Date
-
-/**
- * Stateless synchronization class, used for syncing status about a specific movie/show.
- *
- * All non-null `AuthToken` will be non-expired when each function is called.
- */
-abstract class SyncAPI : AuthAPI() {
- /**
- * Set this to true if the user updates something on the list like watch status or score
- **/
- open var requireLibraryRefresh: Boolean = true
- open val mainUrl: String = "NONE"
-
- /** Currently unused, but will be used to correctly render the UI.
- * This should specify what sync watch types can be used with this service. */
- open val supportedWatchTypes: Set = SyncWatchType.entries.toSet()
- /**
- * Allows certain providers to open pages from
- * library links.
- **/
- open val syncIdName: SyncIdName? = null
-
- /** Modify the current status of an item */
- @Throws
- @WorkerThread
- open suspend fun updateStatus(
- auth: AuthData?,
- id: String,
- newStatus: AbstractSyncStatus
- ): Boolean = throw NotImplementedError()
-
- /** Get the current status of an item */
- @Throws
- @WorkerThread
- open suspend fun status(auth: AuthData?, id: String): AbstractSyncStatus? =
- throw NotImplementedError()
-
- /** Get metadata about an item */
- @Throws
- @WorkerThread
- open suspend fun load(auth: AuthData?, id: String): SyncResult? = throw NotImplementedError()
-
- /** Search this service for any results for a given query */
- @Throws
- @WorkerThread
- open suspend fun search(auth: AuthData?, query: String): List? =
- throw NotImplementedError()
-
- /** Get the current library/bookmarks of this service */
- @Throws
- @WorkerThread
- open suspend fun library(auth: AuthData?): LibraryMetadata? = throw NotImplementedError()
-
- /** Helper function, may be used in the future */
- @Throws
- open fun urlToId(url: String): String? = null
-
- data class SyncSearchResult(
- override val name: String,
- override val apiName: String,
- var syncId: String,
- override val url: String,
- override var posterUrl: String?,
- override var type: TvType? = null,
- override var quality: SearchQuality? = null,
- override var posterHeaders: Map? = null,
- override var id: Int? = null,
- override var score: Score? = null,
- ) : SearchResponse
-
- abstract class AbstractSyncStatus {
- abstract var status: SyncWatchType
- abstract var score: Score?
- abstract var watchedEpisodes: Int?
- abstract var isFavorite: Boolean?
- abstract var maxEpisodes: Int?
- }
-
- data class SyncStatus(
- override var status: SyncWatchType,
- override var score: Score?,
- override var watchedEpisodes: Int?,
- override var isFavorite: Boolean? = null,
- override var maxEpisodes: Int? = null,
- ) : AbstractSyncStatus()
-
- data class SyncResult(
- /**Used to verify*/
- var id: String,
-
- var totalEpisodes: Int? = null,
-
- var title: String? = null,
- var publicScore: Score? = null,
- /**In minutes*/
- var duration: Int? = null,
- var synopsis: String? = null,
- var airStatus: ShowStatus? = null,
- var nextAiring: NextAiring? = null,
- var studio: List? = null,
- var genres: List? = null,
- var synonyms: List? = null,
- var trailers: List? = null,
- var isAdult: Boolean? = null,
- var posterUrl: String? = null,
- var backgroundPosterUrl: String? = null,
-
- /** In unixtime */
- var startDate: Long? = null,
- /** In unixtime */
- var endDate: Long? = null,
- var recommendations: List? = null,
- var nextSeason: SyncSearchResult? = null,
- var prevSeason: SyncSearchResult? = null,
- var actors: List? = null,
- )
-
- data class Page(
- val title: UiText, var items: List
- ) {
- fun sort(method: ListSorting?, query: String? = null) {
- items = when (method) {
- ListSorting.Query ->
- if (query != null) {
- items.sortedBy {
- -Levenshtein.partialRatio(
- query.lowercase(), it.name.lowercase()
- )
- }
- } else items
-
- ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating?.toInt(100) ?: 0) }
- ListSorting.RatingLow -> items.sortedBy { (it.personalRating?.toInt(100) ?: 0) }
- ListSorting.AlphabeticalA -> items.sortedBy { it.name }
- ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
- ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
- ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
- ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate }
- ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate }
- else -> items
- }
- }
- }
-
- data class LibraryMetadata(
- val allLibraryLists: List,
- val supportedListSorting: Set
- )
-
- data class LibraryList(
- val name: UiText,
- val items: List
- )
-
- data class LibraryItem(
- override val name: String,
- override val url: String,
- /**
- * Unique unchanging string used for data storage.
- * This should be the actual id when you change scores and status
- * since score changes from library might get added in the future.
- **/
- val syncId: String,
- val episodesCompleted: Int?,
- val episodesTotal: Int?,
- val personalRating: Score?,
- val lastUpdatedUnixTime: Long?,
- override val apiName: String,
- override var type: TvType?,
- override var posterUrl: String?,
- override var posterHeaders: Map?,
- override var quality: SearchQuality?,
- val releaseDate: Date?,
- override var id: Int? = null,
- val plot: String? = null,
- override var score: Score? = null,
- val tags: List? = null
- ) : SearchResponse
-}
+package com.lagradost.cloudstream3.syncproviders
+
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.ui.SyncWatchType
+import com.lagradost.cloudstream3.ui.library.ListSorting
+import com.lagradost.cloudstream3.ui.result.UiText
+import me.xdrop.fuzzywuzzy.FuzzySearch
+import java.util.Date
+
+interface SyncAPI : OAuth2API {
+ /**
+ * Set this to true if the user updates something on the list like watch status or score
+ **/
+ var requireLibraryRefresh: Boolean
+ val mainUrl: String
+
+ /**
+ * Allows certain providers to open pages from
+ * library links.
+ **/
+ val syncIdName: SyncIdName
+
+ /**
+ -1 -> None
+ 0 -> Watching
+ 1 -> Completed
+ 2 -> OnHold
+ 3 -> Dropped
+ 4 -> PlanToWatch
+ 5 -> ReWatching
+ */
+ suspend fun score(id: String, status: AbstractSyncStatus): Boolean
+
+ suspend fun getStatus(id: String): AbstractSyncStatus?
+
+ suspend fun getResult(id: String): SyncResult?
+
+ suspend fun search(name: String): List?
+
+ suspend fun getPersonalLibrary(): LibraryMetadata?
+
+ fun getIdFromUrl(url: String): String
+
+ data class SyncSearchResult(
+ override val name: String,
+ override val apiName: String,
+ var syncId: String,
+ override val url: String,
+ override var posterUrl: String?,
+ override var type: TvType? = null,
+ override var quality: SearchQuality? = null,
+ override var posterHeaders: Map? = null,
+ override var id: Int? = null,
+ ) : SearchResponse
+
+ abstract class AbstractSyncStatus {
+ abstract var status: SyncWatchType
+
+ /** 1-10 */
+ abstract var score: Int?
+ abstract var watchedEpisodes: Int?
+ abstract var isFavorite: Boolean?
+ abstract var maxEpisodes: Int?
+ }
+
+
+ data class SyncStatus(
+ override var status: SyncWatchType,
+ /** 1-10 */
+ override var score: Int?,
+ override var watchedEpisodes: Int?,
+ override var isFavorite: Boolean? = null,
+ override var maxEpisodes: Int? = null,
+ ) : AbstractSyncStatus()
+
+ data class SyncResult(
+ /**Used to verify*/
+ var id: String,
+
+ var totalEpisodes: Int? = null,
+
+ var title: String? = null,
+ /**1-1000*/
+ var publicScore: Int? = null,
+ /**In minutes*/
+ var duration: Int? = null,
+ var synopsis: String? = null,
+ var airStatus: ShowStatus? = null,
+ var nextAiring: NextAiring? = null,
+ var studio: List? = null,
+ var genres: List? = null,
+ var synonyms: List? = null,
+ var trailers: List? = null,
+ var isAdult: Boolean? = null,
+ var posterUrl: String? = null,
+ var backgroundPosterUrl: String? = null,
+
+ /** In unixtime */
+ var startDate: Long? = null,
+ /** In unixtime */
+ var endDate: Long? = null,
+ var recommendations: List? = null,
+ var nextSeason: SyncSearchResult? = null,
+ var prevSeason: SyncSearchResult? = null,
+ var actors: List? = null,
+ )
+
+
+ data class Page(
+ val title: UiText, var items: List
+ ) {
+ fun sort(method: ListSorting?, query: String? = null) {
+ items = when (method) {
+ ListSorting.Query ->
+ if (query != null) {
+ items.sortedBy {
+ -FuzzySearch.partialRatio(
+ query.lowercase(), it.name.lowercase()
+ )
+ }
+ } else items
+ ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
+ ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
+ ListSorting.AlphabeticalA -> items.sortedBy { it.name }
+ ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
+ ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
+ ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
+ ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate }
+ ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate }
+ else -> items
+ }
+ }
+ }
+
+ data class LibraryMetadata(
+ val allLibraryLists: List,
+ val supportedListSorting: Set
+ )
+
+ data class LibraryList(
+ val name: UiText,
+ val items: List
+ )
+
+ data class LibraryItem(
+ override val name: String,
+ override val url: String,
+ /**
+ * Unique unchanging string used for data storage.
+ * This should be the actual id when you change scores and status
+ * since score changes from library might get added in the future.
+ **/
+ val syncId: String,
+ val episodesCompleted: Int?,
+ val episodesTotal: Int?,
+ /** Out of 100 */
+ val personalRating: Int?,
+ val lastUpdatedUnixTime: Long?,
+ override val apiName: String,
+ override var type: TvType?,
+ override var posterUrl: String?,
+ override var posterHeaders: Map?,
+ override var quality: SearchQuality?,
+ val releaseDate: Date?,
+ override var id: Int? = null,
+ val plot : String? = null,
+ val rating: Int? = null,
+ val tags: List? = null
+ ) : SearchResponse
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt
index de82624fc..9363cb6fb 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncRepo.kt
@@ -1,30 +1,48 @@
-package com.lagradost.cloudstream3.syncproviders
-
-/** Stateless safe abstraction of SyncAPI */
-class SyncRepo(override val api: SyncAPI) : AuthRepo(api) {
- val syncIdName = api.syncIdName
- var requireLibraryRefresh: Boolean
- get() = api.requireLibraryRefresh
- set(value) {
- api.requireLibraryRefresh = value
- }
-
- suspend fun updateStatus(id: String, newStatus: SyncAPI.AbstractSyncStatus): Result =
- runCatching {
- val status = api.updateStatus(freshAuth() ?: return@runCatching false, id, newStatus)
- requireLibraryRefresh = true
- status
- }
-
- suspend fun status(id: String): Result = runCatching {
- api.status(freshAuth(), id)
- }
-
- suspend fun load(id: String): Result = runCatching {
- api.load(freshAuth(), id)
- }
-
- suspend fun library(): Result = runCatching {
- api.library(freshAuth())
- }
-}
+package com.lagradost.cloudstream3.syncproviders
+
+import com.lagradost.cloudstream3.ErrorLoadingException
+import com.lagradost.cloudstream3.mvvm.Resource
+import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
+import com.lagradost.cloudstream3.mvvm.safeApiCall
+
+class SyncRepo(private val repo: SyncAPI) {
+ val idPrefix = repo.idPrefix
+ val name = repo.name
+ val icon = repo.icon
+ val mainUrl = repo.mainUrl
+ val requiresLogin = repo.requiresLogin
+ val syncIdName = repo.syncIdName
+ var requireLibraryRefresh: Boolean
+ get() = repo.requireLibraryRefresh
+ set(value) {
+ repo.requireLibraryRefresh = value
+ }
+
+ suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Resource {
+ return safeApiCall { repo.score(id, status) }
+ }
+
+ suspend fun getStatus(id: String): Resource {
+ return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
+ }
+
+ suspend fun getResult(id: String): Resource {
+ return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
+ }
+
+ suspend fun search(query: String): Resource> {
+ return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
+ }
+
+ suspend fun getPersonalLibrary(): Resource {
+ return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
+ }
+
+ fun hasAccount(): Boolean {
+ return normalSafeApiCall { repo.loginInfo() != null } ?: false
+ }
+
+ fun getIdFromUrl(url: String): String? = normalSafeApiCall {
+ repo.getIdFromUrl(url)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
index 144efff99..db4676393 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
@@ -1,205 +1,108 @@
package com.lagradost.cloudstream3.syncproviders.providers
-import com.lagradost.cloudstream3.AllLanguagesName
-import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
-import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
-import com.lagradost.cloudstream3.syncproviders.AuthData
-import com.lagradost.cloudstream3.syncproviders.SubtitleAPI
import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToEnglishLanguageName
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.subtitles.AbstractSubApi
+import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
+import com.lagradost.cloudstream3.utils.SubtitleHelper
-class Addic7ed : SubtitleAPI() {
+class Addic7ed : AbstractSubApi {
override val name = "Addic7ed"
override val idPrefix = "addic7ed"
override val requiresLogin = false
+ override val icon: Nothing? = null
+ override val createAccountUrl: Nothing? = null
+
+ override fun loginInfo(): Nothing? = null
+
+ override fun logOut() {}
companion object {
const val HOST = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED"
}
- private fun String.fixUrl(): String {
- val url = this
+ private fun fixUrl(url: String): String {
return if (url.startsWith("/")) HOST + url
else if (!url.startsWith("http")) "$HOST/$url"
else url
+
}
- override suspend fun search(
- auth: AuthData?,
- query: SubtitleSearch
- ): List? {
- val langTagIETF = query.lang ?: AllLanguagesName
- val langNumAddic7ed =
- langTagIETF2Addic7ed[langTagIETF]?.first ?: 0 // all languages = 0
- val langName =
- langTagIETF2Addic7ed[langTagIETF]?.second ?:
- fromTagToEnglishLanguageName(langTagIETF) ?:
- "Completed" // this bypasses language filtering
- val title = query.query.trim()
+ override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List {
+ val lang = query.lang
+ val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
+ val queryText = query.query.trim()
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
- val searchQuery = if (seasonNum > 0) "$title $seasonNum $epNum" else title
- var downloadPage = ""
- fun newSubtitleEntity (
- displayName: String?,
- link: String?,
+ fun cleanResources(
+ results: MutableList,
+ name: String,
+ link: String,
+ headers: Map,
isHearingImpaired: Boolean
- ): SubtitleEntity? {
- if (displayName.isNullOrBlank() || link.isNullOrBlank()) return null
- return SubtitleEntity(
- idPrefix = this.idPrefix,
- name = displayName,
- lang = langTagIETF,
- data = link,
- source = this.name,
- type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
- epNumber = epNum,
- seasonNumber = seasonNum,
- year = yearNum,
- headers = mapOf("referer" to "$HOST/"),
- isHearingImpaired = isHearingImpaired
+ ) {
+ results.add(
+ AbstractSubtitleEntities.SubtitleEntity(
+ idPrefix = idPrefix,
+ name = name,
+ lang = queryLang.toString(),
+ data = link,
+ source = this.name,
+ type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
+ epNumber = epNum,
+ seasonNumber = seasonNum,
+ year = yearNum,
+ headers = headers,
+ isHearingImpaired = isHearingImpaired
+ )
)
}
- val response = app.get(url = "$HOST/search.php?search=$searchQuery&Submit=Search")
- val hostDocument = response.document
-
- // 1st case: found one movie or episode. Redirected to $HOST/movie/1234 or $HOST/serie/show-name/$seasonNum/$epNum/ep-name
- if (response.url.contains("/movie/") || response.url.contains("/serie/"))
- downloadPage = response.url
-
- // 2nd case: found tv series ep list. Redirected to $HOST/show/1234
- else if (response.url.contains("/show/")) {
- val showId = response.url.substringAfterLast("/")
+ val title = queryText.substringBefore("(").trim()
+ val url = "$HOST/search.php?search=${title}&Submit=Search"
+ val hostDocument = app.get(url).document
+ var searchResult = ""
+ if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
+ else if (!hostDocument.select("table.tabel")
+ .isNullOrEmpty()
+ ) searchResult = hostDocument.select("a:contains($title)").attr("href").toString()
+ else {
+ val show =
+ hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
+ ?.substringBefore(",")
val doc = app.get(
- "$HOST/ajax_loadShow.php?show=$showId&season=$seasonNum&langs=|$langNumAddic7ed|&hd=0&hi=0",
+ "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
referer = "$HOST/"
).document
-
- // get direct subtitles links from list
- return doc.select("#season tbody tr").mapNotNull { node ->
- if (node.select("td:eq(1)").text().toIntOrNull() == epNum)
- newSubtitleEntity(
- displayName = node.select("td:eq(2)").text() + "\n" + node.select("td:eq(4)").text(),
- link = node.selectFirst("a[href~=updated\\/|original\\/]")?.attr("href")?.fixUrl(),
- isHearingImpaired = node.select("td:eq(6)").text().isNotEmpty()
- )
- else null
+ doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
+ if (node.selectFirst("td")?.text()
+ ?.toIntOrNull() == seasonNum && node.select("td:eq(1)")
+ .text()
+ .toIntOrNull() == epNum
+ ) searchResult = fixUrl(node.select("a").attr("href"))
}
- // 3rd case: found several or no results. Still in $HOST/search.php?search=title
- } else {// (response.url.contains("/search.php"))
- downloadPage = hostDocument.select("table.tabel a").selectFirst({
- // tv series
- if (seasonNum > 0) "a[href~=serie\\/.+\\/$seasonNum\\/$epNum\\/\\w]"
- // movie + year
- else if( yearNum > 0) "a[href~=movie\\/]:contains($yearNum)"
- // movie
- else "a[href~=movie\\/]"
- }())?.attr("href")?.fixUrl() ?: return null
}
+ val results = mutableListOf()
+ val document = app.get(
+ url = fixUrl(searchResult),
+ ).document
- // filter download page by language. Do not work for movies :/
- if (downloadPage.contains("/serie/"))
- downloadPage = downloadPage.substringBeforeLast("/") + "/$langNumAddic7ed"
- val doc = app.get(url = downloadPage).document
-
- // get subtitles links from download page
- return doc.select(".tabel95 .tabel95 tr:has(.language):contains($langName)").mapNotNull { node ->
- val displayName =
- doc.selectFirst("span.titulo")?.text()?.substringBefore(" Subtitle") + "\n" +
- node.parent()!!.select(".NewsTitle").text().substringAfter("Version ").substringBefore(", Duration")
- val link =
- node.selectFirst("a[href~=updated\\/|original\\/]")?.attr("href")?.fixUrl()
+ document.select(".tabel95 .tabel95 tr:contains($queryLang)").mapNotNull { node ->
+ val name = if (seasonNum > 0) "${document.select(".titulo").text().replace("Subtitle","").trim()}${
+ node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")
+ }" else "${document.select(".titulo").text().replace("Subtitle","").trim()}${node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")}"
+ val link = fixUrl(node.select("a.buttonDownload").attr("href"))
val isHearingImpaired =
- node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNotEmpty()
-
- newSubtitleEntity(displayName, link, isHearingImpaired)
+ !node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
+ cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired)
}
+ return results
}
- override suspend fun load(
- auth: AuthData?,
- subtitle: SubtitleEntity
- ): String? {
- return subtitle.data
+ override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String {
+ return data.data
}
-
- // Missing (?_?)
- // Pair("2", ""),
- // Pair("3", ""),
- // Pair("33", ""),
- // Pair("34", ""),
- // Do not modify unless Addic7ed changes them!
- // as they are the exact values from their website
- private val langTagIETF2Addic7ed = mapOf(
- "ar" to Pair("38", "Arabic"),
- "az" to Pair("48", "Azerbaijani"),
- "bg" to Pair("35", "Bulgarian"),
- "bn" to Pair("47", "Bengali"),
- "bs" to Pair("44", "Bosnian"),
- "ca" to Pair("12", "Català"),
- "cs" to Pair("14", "Czech"),
- "cy" to Pair("65", "Welsh"),
- "da" to Pair("30", "Danish"),
- "de" to Pair("11", "German"),
- "el" to Pair("27", "Greek"),
- "en" to Pair("1", "English"),
- "es-419" to Pair("6", "Spanish (Latin America)"),
- "es-ar" to Pair("69", "Spanish (Argentina)"),
- "es-es" to Pair("5", "Spanish (Spain)"),
- "es" to Pair("4", "Spanish"),
- "et" to Pair("54", "Estonian"),
- "eu" to Pair("13", "Euskera"),
- "fa" to Pair("43", "Persian"),
- "fi" to Pair("28", "Finnish"),
- "fr-ca" to Pair("53", "French (Canadian)"),
- "fr" to Pair("8", "French"),
- "gl" to Pair("15", "Galego"),
- "he" to Pair("23", "Hebrew"),
- "hi" to Pair("55", "Hindi"),
- "hr" to Pair("31", "Croatian"),
- "hu" to Pair("20", "Hungarian"),
- "hy" to Pair("50", "Armenian"),
- "id" to Pair("37", "Indonesian"),
- "is" to Pair("56", "Icelandic"),
- "it" to Pair("7", "Italian"),
- "ja" to Pair("32", "Japanese"),
- "kn" to Pair("66", "Kannada"),
- "ko" to Pair("42", "Korean"),
- "lt" to Pair("58", "Lithuanian"),
- "lv" to Pair("57", "Latvian"),
- "mk" to Pair("49", "Macedonian"),
- "ml" to Pair("67", "Malayalam"),
- "mr" to Pair("62", "Marathi"),
- "ms" to Pair("40", "Malay"),
- "nl" to Pair("17", "Dutch"),
- "no" to Pair("29", "Norwegian"),
- "pl" to Pair("21", "Polish"),
- "pt-br" to Pair("10", "Portuguese (Brazilian)"),
- "pt" to Pair("9", "Portuguese"),
- "ro" to Pair("26", "Romanian"),
- "ru" to Pair("19", "Russian"),
- "si" to Pair("60", "Sinhala"),
- "sk" to Pair("25", "Slovak"),
- "sl" to Pair("22", "Slovenian"),
- "sq" to Pair("52", "Albanian"),
- "sr-latn" to Pair("36", "Serbian (Latin)"),
- "sr" to Pair("39", "Serbian (Cyrillic)"),
- "sv" to Pair("18", "Swedish"),
- "ta" to Pair("59", "Tamil"),
- "te" to Pair("63", "Telugu"),
- "th" to Pair("46", "Thai"),
- "tl" to Pair("68", "Tagalog"),
- "tlh" to Pair("61", "Klingon"),
- "tr" to Pair("16", "Turkish"),
- "uk" to Pair("51", "Ukrainian"),
- "vi" to Pair("45", "Vietnamese"),
- "yue" to Pair("64", "Cantonese"),
- "zh-hans" to Pair("41", "Chinese (Simplified)"),
- "zh-hant" to Pair("24", "Chinese (Traditional)"),
- )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
index 177018e19..6112c7dbe 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
@@ -1,91 +1,93 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
+import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.Actor
-import com.lagradost.cloudstream3.ActorData
-import com.lagradost.cloudstream3.ActorRole
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
-import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.NextAiring
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.Score
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
+import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.syncproviders.AuthData
-import com.lagradost.cloudstream3.syncproviders.AuthLoginPage
-import com.lagradost.cloudstream3.syncproviders.AuthToken
-import com.lagradost.cloudstream3.syncproviders.AuthUser
+import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
+import com.lagradost.cloudstream3.syncproviders.AccountManager
+import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
+import com.lagradost.cloudstream3.ui.result.txt
+import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
-import com.lagradost.cloudstream3.utils.txt
+import java.net.URL
import java.net.URLEncoder
import java.util.Locale
-class AniListApi : SyncAPI() {
+class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override var name = "AniList"
+ override val key = "6871"
+ override val redirectUrl = "anilistlogin"
override val idPrefix = "anilist"
-
- val key = "6871"
- override val redirectUrlIdentifier = "anilistlogin"
override var requireLibraryRefresh = true
- override val hasOAuth2 = true
+ override val supportDeviceAuth = false
override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon
+ override val requiresLogin = false
override val createAccountUrl = "$mainUrl/signup"
override val syncIdName = SyncIdName.Anilist
- override fun loginRequest(): AuthLoginPage? =
- AuthLoginPage("https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token")
-
- override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
- val sanitizer = splitRedirectUrl(redirectUrl)
- val token = AuthToken(
- accessToken = sanitizer["access_token"]
- ?: throw ErrorLoadingException("No access token"),
- //refreshToken = sanitizer["refresh_token"],
- accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(),
- )
- return token
+ override fun loginInfo(): AuthAPI.LoginInfo? {
+ // context.getUser(true)?.
+ getKey(accountId, ANILIST_USER_KEY)?.let { user ->
+ return AuthAPI.LoginInfo(
+ profilePicture = user.picture,
+ name = user.name,
+ accountIndex = accountIndex
+ )
+ }
+ return null
}
- // https://docs.anilist.co/guide/auth/
- override suspend fun refreshToken(token: AuthToken): AuthToken? {
- // AniList access tokens are long-lived. They will remain valid for 1 year from the time they are issued.
- // Refresh tokens are not currently supported. Once a token expires, you will need to re-authenticate your users.
- return super.refreshToken(token)
+ override fun logOut() {
+ requireLibraryRefresh = true
+ removeAccountKeys()
}
- override suspend fun user(token: AuthToken?): AuthUser? {
- val user = getUser(token ?: return null)
- ?: throw ErrorLoadingException("Unable to fetch user data")
-
- return AuthUser(
- id = user.id,
- name = user.name,
- profilePicture = user.picture,
- )
+ override fun authenticate(activity: FragmentActivity?) {
+ val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
+ openBrowser(request, activity)
}
- override fun urlToId(url: String): String? =
- url.removePrefix("$mainUrl/anime/").removeSuffix("/")
+ override suspend fun handleRedirect(url: String): Boolean {
+ val sanitizer =
+ splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
+ val token = sanitizer["access_token"]!!
+ val expiresIn = sanitizer["expires_in"]!!
+ val endTime = unixTime + expiresIn.toLong()
+
+ switchToNewAccount()
+ setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
+ setKey(accountId, ANILIST_TOKEN_KEY, token)
+ val user = getUser()
+ requireLibraryRefresh = true
+ return user != null
+ }
+
+ override fun getIdFromUrl(url: String): String {
+ return url.removePrefix("$mainUrl/anime/").removeSuffix("/")
+ }
private fun getUrlFromId(id: Int): String {
return "$mainUrl/anime/$id"
}
- override suspend fun search(auth: AuthData?, query: String): List? {
- val data = searchShows(query) ?: return null
+ override suspend fun search(name: String): List? {
+ val data = searchShows(name) ?: return null
return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult(
it.title.romaji ?: return null,
@@ -97,7 +99,7 @@ class AniListApi : SyncAPI() {
}
}
- override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
+ override suspend fun getResult(id: String): SyncAPI.SyncResult {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
val season = getSeason(internalId).data.media
@@ -139,11 +141,11 @@ class AniListApi : SyncAPI() {
}
)
},
- publicScore = Score.from100(season.averageScore),
+ publicScore = season.averageScore?.times(100),
recommendations = season.recommendations?.edges?.mapNotNull { rec ->
val recMedia = rec.node.mediaRecommendation
SyncAPI.SyncSearchResult(
- name = recMedia?.title?.userPreferred ?: return@mapNotNull null,
+ name = recMedia.title?.userPreferred ?: return@mapNotNull null,
this.name,
recMedia.id?.toString() ?: return@mapNotNull null,
getUrlFromId(recMedia.id),
@@ -159,12 +161,12 @@ class AniListApi : SyncAPI() {
)
}
- override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
+ override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? {
val internalId = id.toIntOrNull() ?: return null
- val data = getDataAboutId(auth ?: return null, internalId) ?: return null
+ val data = getDataAboutId(internalId) ?: return null
return SyncAPI.SyncStatus(
- score = Score.from100(data.score),
+ score = data.score,
watchedEpisodes = data.progress,
status = SyncWatchType.fromInternalId(data.type?.value ?: return null),
isFavorite = data.isFavourite,
@@ -172,25 +174,24 @@ class AniListApi : SyncAPI() {
)
}
- override suspend fun updateStatus(
- auth: AuthData?,
- id: String,
- newStatus: AbstractSyncStatus
- ): Boolean {
+ override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
return postDataAboutId(
- auth ?: return false,
id.toIntOrNull() ?: return false,
- fromIntToAnimeStatus(newStatus.status.internalId),
- newStatus.score,
- newStatus.watchedEpisodes
- )
+ fromIntToAnimeStatus(status.status.internalId),
+ status.score,
+ status.watchedEpisodes
+ ).also {
+ requireLibraryRefresh = requireLibraryRefresh || it
+ }
}
companion object {
- const val MAX_STALE = 60 * 10
private val aniListStatusString =
arrayOf("CURRENT", "COMPLETED", "PAUSED", "DROPPED", "PLANNING", "REPEATING")
+ const val ANILIST_UNIXTIME_KEY: String = "anilist_unixtime" // When token expires
+ const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api
+ const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
private fun fixName(name: String): String {
@@ -460,7 +461,21 @@ class AniListApi : SyncAPI() {
}
}
- private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? {
+ fun initGetUser() {
+ if (getAuth() == null) return
+ ioSafe {
+ getUser()
+ }
+ }
+
+ private fun checkToken(): Boolean {
+ return unixTime > getKey(
+ accountId,
+ ANILIST_UNIXTIME_KEY, 0L
+ )!!
+ }
+
+ private suspend fun getDataAboutId(id: Int): AniListTitleHolder? {
val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
@@ -470,7 +485,7 @@ class AniListApi : SyncAPI() {
mediaListEntry {
progress
status
- score (format: POINT_100)
+ score (format: POINT_10)
}
title {
english
@@ -479,7 +494,7 @@ class AniListApi : SyncAPI() {
}
}"""
- val data = postApi(auth.token, q, true)
+ val data = postApi(q, true)
val d = parseJson(data ?: return null)
val main = d.data?.media
@@ -507,24 +522,37 @@ class AniListApi : SyncAPI() {
}
- private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? {
- return app.post(
- "https://graphql.anilist.co/",
- headers = mapOf(
- "Authorization" to "Bearer ${token.accessToken ?: return null}",
- if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache"
- ),
- cacheTime = 0,
- data = mapOf(
- "query" to URLEncoder.encode(
- q,
- "UTF-8"
- )
- ), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
- timeout = 5 // REASONABLE TIMEOUT
- ).text.replace("\\/", "/")
+ private fun getAuth(): String? {
+ return getKey(
+ accountId,
+ ANILIST_TOKEN_KEY
+ )
}
+ private suspend fun postApi(q: String, cache: Boolean = false): String? {
+ return suspendSafeApiCall {
+ if (!checkToken()) {
+ app.post(
+ "https://graphql.anilist.co/",
+ headers = mapOf(
+ "Authorization" to "Bearer " + (getAuth()
+ ?: return@suspendSafeApiCall null),
+ if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache"
+ ),
+ cacheTime = 0,
+ data = mapOf(
+ "query" to URLEncoder.encode(
+ q,
+ "UTF-8"
+ )
+ ), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
+ timeout = 5 // REASONABLE TIMEOUT
+ ).text.replace("\\/", "/")
+ } else {
+ null
+ }
+ }
+ }
data class MediaRecommendation(
@JsonProperty("id") val id: Int,
@@ -596,7 +624,7 @@ class AniListApi : SyncAPI() {
this.media.id.toString(),
this.progress,
this.media.episodes,
- Score.from100(this.score),
+ this.score,
this.updatedAt.toLong(),
"AniList",
TvType.Anime,
@@ -624,23 +652,27 @@ class AniListApi : SyncAPI() {
@JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection
)
- private suspend fun getAniListAnimeListSmart(auth: AuthData): Array? {
+ private fun getAniListListCached(): Array? {
+ return getKey(ANILIST_CACHED_LIST) as? Array
+ }
+
+ private suspend fun getAniListAnimeListSmart(): Array? {
+ if (getAuth() == null) return null
+
+ if (checkToken()) return null
return if (requireLibraryRefresh) {
- val list = getFullAniListList(auth)?.data?.mediaListCollection?.lists?.toTypedArray()
+ val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray()
if (list != null) {
- setKey(ANILIST_CACHED_LIST, auth.user.id.toString(), list)
+ setKey(ANILIST_CACHED_LIST, list)
}
list
} else {
- getKey>(
- ANILIST_CACHED_LIST,
- auth.user.id.toString()
- ) as? Array
+ getAniListListCached()
}
}
- override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? {
- val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
+ override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
+ val list = getAniListAnimeListSmart()?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten()
@@ -667,8 +699,10 @@ class AniListApi : SyncAPI() {
)
}
- private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? {
- val userID = auth.user.id
+ private suspend fun getFullAniListList(): FullAnilistList? {
+ /** WARNING ASSUMES ONE USER! **/
+
+ val userID = getKey(accountId, ANILIST_USER_KEY)?.id ?: return null
val mediaType = "ANIME"
val query = """
@@ -711,11 +745,11 @@ class AniListApi : SyncAPI() {
}
}
"""
- val text = postApi(auth.token, query)
+ val text = postApi(query)
return text?.toKotlinObject()
}
- suspend fun toggleLike(auth: AuthData, id: Int): Boolean {
+ suspend fun toggleLike(id: Int): Boolean {
val q = """mutation (${'$'}animeId: Int = $id) {
ToggleFavourite (animeId: ${'$'}animeId) {
anime {
@@ -728,7 +762,7 @@ class AniListApi : SyncAPI() {
}
}
}"""
- val data = postApi(auth.token, q)
+ val data = postApi(q)
return data != ""
}
@@ -738,17 +772,15 @@ class AniListApi : SyncAPI() {
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId(
- auth: AuthData,
id: Int,
type: AniListStatusType,
- score: Score?,
+ score: Int?,
progress: Int?
): Boolean {
- val userID = auth.user.id
-
val q =
// Delete item if status type is None
if (type == AniListStatusType.None) {
+ val userID = getKey(accountId, ANILIST_USER_KEY)?.id ?: return false
// Get list ID for deletion
val idQuery = """
query MediaList(${'$'}userId: Int = $userID, ${'$'}mediaId: Int = $id) {
@@ -757,7 +789,7 @@ class AniListApi : SyncAPI() {
}
}
"""
- val response = postApi(auth.token, idQuery)
+ val response = postApi(idQuery)
val listId =
tryParseJson(response)?.data?.mediaList?.id ?: return false
"""
@@ -773,7 +805,7 @@ class AniListApi : SyncAPI() {
0,
type.value
)]
- }, ${if (score != null) "${'$'}scoreRaw: Int = ${score.toInt(100)}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
+ }, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
id
status
@@ -783,11 +815,11 @@ class AniListApi : SyncAPI() {
}"""
}
- val data = postApi(auth.token, q)
+ val data = postApi(q)
return data != ""
}
- private suspend fun getUser(token: AuthToken): AniListUser? {
+ private suspend fun getUser(setSettings: Boolean = true): AniListUser? {
val q = """
{
Viewer {
@@ -805,15 +837,23 @@ class AniListApi : SyncAPI() {
}
}
}"""
- val data = postApi(token, q)
+ val data = postApi(q)
if (data.isNullOrBlank()) return null
val userData = parseJson