diff --git a/.github/ISSUE_TEMPLATE/application-bug.yml b/.github/ISSUE_TEMPLATE/application-bug.yml
index f3590067..931db3bd 100644
--- a/.github/ISSUE_TEMPLATE/application-bug.yml
+++ b/.github/ISSUE_TEMPLATE/application-bug.yml
@@ -80,13 +80,13 @@ body:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- - label: I am sure my issue is related to the app and **NOT some extension**.
- required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
required: true
+ - label: If related to a provider, I have checked the site and it works, but not the app.
+ required: true
- label: I will fill out all of the requested information in this form.
required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index b56cdf8e..cd3c2574 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- - name: Request a new provider or report bug with an existing provider
+ - name: Report provider bug
url: https://github.com/recloudstream
- about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
+ about: 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.
- name: Discord
url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
index e18daebb..9c35ba56 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -27,7 +27,9 @@ body:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- - label: My suggestion is **NOT** about adding a new provider
- required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
- required: true
\ No newline at end of file
+ required: true
+ - label: I have written a short but informative title.
+ required: true
+ - label: I will fill out all of the requested information in this form.
+ required: true
diff --git a/.github/downloads.jpg b/.github/downloads.jpg
new file mode 100644
index 00000000..0b671edc
Binary files /dev/null and b/.github/downloads.jpg differ
diff --git a/.github/home.jpg b/.github/home.jpg
new file mode 100644
index 00000000..2ccfaff4
Binary files /dev/null and b/.github/home.jpg differ
diff --git a/.github/locales.py b/.github/locales.py
deleted file mode 100644
index a74d7258..00000000
--- a/.github/locales.py
+++ /dev/null
@@ -1,69 +0,0 @@
-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-"
-ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
-INDENT = " "*4
-
-iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
-
-# Load settings file
-src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
-before_src, rest = src.split(START_MARKER)
-rest, after_src = rest.split(END_MARKER)
-
-# Load already added langs
-languages = {}
-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):]
- if iso not in languages.keys():
- entry = iso_map.get(iso.lower(),{'nativeName':iso})
- languages[iso] = ("", entry['nativeName'].split(',')[0])
-
-# 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(triples) +
- "\n" +
- END_MARKER +
- after_src
-)
-
-# Go through each values.xml file and fix escaped \@string
-for file in glob.glob(f"{XML_NAME}*/strings.xml"):
- try:
- tree = ET.parse(file)
- for child in tree.getroot():
- if not child.text:
- continue
- if child.text.startswith("\\@string/"):
- print(f"[{file}] fixing {child.attrib['name']}")
- child.text = child.text.replace("\\@string/", "@string/")
- 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/player.jpg b/.github/player.jpg
new file mode 100644
index 00000000..0580fb03
Binary files /dev/null and b/.github/player.jpg differ
diff --git a/.github/results.jpg b/.github/results.jpg
new file mode 100644
index 00000000..5e63169f
Binary files /dev/null and b/.github/results.jpg differ
diff --git a/.github/search.jpg b/.github/search.jpg
new file mode 100644
index 00000000..998b7753
Binary files /dev/null and b/.github/search.jpg differ
diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml
deleted file mode 100644
index e84bb08b..00000000
--- a/.github/workflows/build_to_archive.yml
+++ /dev/null
@@ -1,78 +0,0 @@
-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 96e61644..032ea8d0 100644
--- a/.github/workflows/generate_dokka.yml
+++ b/.github/workflows/generate_dokka.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
@@ -39,17 +39,17 @@ jobs:
- name: Clean old builds
run: |
+ shopt -s extglob
cd $GITHUB_WORKSPACE/dokka/
- rm -rf "./-cloudstream"
+ rm -rf !(.git)
- - name: Setup JDK 17
- uses: actions/setup-java@v4
+ - name: Setup JDK 11
+ uses: actions/setup-java@v1
with:
- java-version: 17
- distribution: 'adopt'
+ java-version: 11
- name: Setup Android SDK
- uses: android-actions/setup-android@v3
+ uses: android-actions/setup-android@v2
- name: Generate Dokka
run: |
diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue-action.yml
similarity index 67%
rename from .github/workflows/issue_action.yml
rename to .github/workflows/issue-action.yml
index 88ab3656..bfcb10d0 100644
--- a/.github/workflows/issue_action.yml
+++ b/.github/workflows/issue-action.yml
@@ -1,88 +1,63 @@
-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'
-
-
+name: Issue automatic actions
+
+on:
+ issues:
+ types: [opened, edited]
+
+jobs:
+ issue-moderator:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate access token
+ id: generate_token
+ uses: tibdex/github-app-token@v1
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ - name: Similarity analysis
+ uses: actions-cool/issues-similarity-analysis@v1
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ filter-threshold: 0.5
+ title-excludes: ''
+ comment-title: |
+ ### Your issue looks similar to these issues:
+ Please close if duplicate.
+ comment-body: '${index}. ${similarity} #${number}'
+ - uses: actions/checkout@v2
+ - 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 "::set-output name=name::${RES}"
+ - 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: 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 f35cd58c..71301e25 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -18,16 +18,16 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- - uses: actions/checkout@v4
- - name: Set up JDK 17
- uses: actions/setup-java@v4
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
with:
- java-version: '17'
+ java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
@@ -40,25 +40,24 @@ jobs:
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
+ echo "::set-output name=key_pwd::$KEY_PWD"
- name: Run Gradle
run: |
- ./gradlew assemblePrerelease build androidSourcesJar
- ./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
+ ./gradlew assemblePrerelease
+ ./gradlew androidSourcesJar
+ ./gradlew makeJar
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 }}
- name: Create pre-release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "pre-release"
- prerelease: true
+ prerelease: false
title: "Pre-release Build"
files: |
- app/build/outputs/apk/prerelease/release/*.apk
+ app/build/outputs/apk/prerelease/*.apk
app/build/libs/app-sources.jar
app/build/classes.jar
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 7f6dd412..1a4db134 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -6,18 +6,18 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - name: Set up JDK 17
- uses: actions/setup-java@v4
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
with:
- java-version: '17'
+ java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run Gradle
- run: ./gradlew assemblePrereleaseDebug
+ run: ./gradlew assembleDebug
- name: Upload Artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v2
with:
name: pull-request-build
- path: "app/build/outputs/apk/prerelease/debug/*.apk"
+ path: "app/build/outputs/apk/debug/*.apk"
diff --git a/.github/workflows/update_locales.yml b/.github/workflows/update_locales.yml
deleted file mode 100644
index ce140e55..00000000
--- a/.github/workflows/update_locales.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-name: Fix locale issues
-
-on:
- workflow_dispatch:
- push:
- paths:
- - '**.xml'
- branches:
- - master
-
-concurrency:
- group: "locale"
- cancel-in-progress: true
-
-jobs:
- create:
- 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/cloudstream"
- - uses: actions/checkout@v4
- with:
- token: ${{ steps.generate_token.outputs.token }}
- - name: Install dependencies
- run: |
- pip3 install lxml
- - name: Edit files
- run: |
- python3 .github/locales.py
- - name: Commit to the repo
- run: |
- git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
- git config --local user.name "recloudstream[bot]"
- git add .
- # "echo" returns true so the build succeeds, even if no changed files
- git commit -m 'chore(locales): fix locale issues' || echo
- git push
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56e..5421743a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d7c08c9c..10c26704 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,16 +4,17 @@
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 333d4937..652d9f3f 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -31,10 +31,5 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/README.md b/README.md
index 8949304e..0035daf7 100644
--- a/README.md
+++ b/README.md
@@ -1,19 +1,44 @@
# CloudStream
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
+You can find the list of community-maintained extension repositories [here
+](https://recloudstream.github.io/repos/)
-[](https://discord.gg/5Hus6fM)
+[](https://discord.gg/5Hus6fM)
-### Features:
+***Features:***
+ **AdFree**, No ads whatsoever
+ No tracking/analytics
+ Bookmarks
-+ Phone and TV support
++ Download and stream movies, tv-shows and anime
+ Chromecast
-+ Extension system for personal customization
-### Supported languages:
-
-
-
+***Screenshots:***
+
+


+
+
+***The list of supported languages:***
+* 🇱🇧 Arabic
+* 🇨🇿 Czech
+* 🇳🇱 Dutch
+* 🇬🇧 English
+* 🇫🇷 French
+* 🇩🇪 German
+* 🇬🇷 Greek
+* 🇮🇳 Hindi
+* 🇮🇩 Indonesian
+* 🇮🇹 Italian
+* 🇲🇰 Macedonian
+* 🇮🇳 Malayalam
+* 🇳🇴 Norsk
+* 🇵🇱 Polish
+* 🇧🇷 Portuguese (Brazil)
+* 🇷🇴 Romanian
+* 🇪🇸 Spanish
+* 🇸🇪 Swedish
+* 🇵🇭 Tagalog
+* 🇹🇷 Turkish
+* 🇻🇳 Vietnamese
+
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
deleted file mode 100644
index 7f7fd14c..00000000
--- a/app/CMakeLists.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-# 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 b/app/build.gradle
new file mode 100644
index 00000000..1aa62378
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,215 @@
+plugins {
+ id 'com.android.application'
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+ id 'kotlin-android-extensions'
+ id 'org.jetbrains.dokka'
+}
+
+def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
+def allFilesFromDir = new File(tmpFilePath).listFiles()
+def prereleaseStoreFile = null
+if (allFilesFromDir != null) {
+ prereleaseStoreFile = allFilesFromDir.first()
+}
+
+android {
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+ signingConfigs {
+ prerelease {
+ if (prereleaseStoreFile != null) {
+ storeFile = file(prereleaseStoreFile)
+ storePassword System.getenv("SIGNING_STORE_PASSWORD")
+ keyAlias System.getenv("SIGNING_KEY_ALIAS")
+ keyPassword System.getenv("SIGNING_KEY_PASSWORD")
+ }
+ }
+ }
+ compileSdkVersion 31
+ buildToolsVersion "30.0.3"
+
+ defaultConfig {
+ applicationId "com.lagradost.cloudstream3"
+ minSdkVersion 21
+ targetSdkVersion 30
+
+ versionCode 50
+ versionName "3.1.3"
+
+ resValue "string", "app_version",
+ "${defaultConfig.versionName}${versionNameSuffix ?: ""}"
+
+ resValue "string", "commit_hash",
+ ("git rev-parse --short HEAD".execute().text.trim() ?: "")
+
+ resValue "bool", "is_prerelease", "false"
+
+ buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ kapt {
+ includeCompileClasspath = true
+ }
+ }
+
+ buildTypes {
+ // release {
+ // debuggable false
+ // minifyEnabled false
+ // shrinkResources false
+ // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ // resValue "bool", "is_prerelease", "false"
+ // }
+ prerelease {
+ applicationIdSuffix ".prerelease"
+ buildConfigField("boolean", "BETA", "true")
+ signingConfig signingConfigs.prerelease
+ versionNameSuffix '-PRE'
+ debuggable false
+ minifyEnabled false
+ shrinkResources false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ resValue "bool", "is_prerelease", "true"
+ }
+ debug {
+ debuggable true
+ applicationIdSuffix ".debug"
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ resValue "bool", "is_prerelease", "true"
+ }
+ }
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ freeCompilerArgs = ['-Xjvm-default=compatibility']
+ }
+ lintOptions {
+ checkReleaseBuilds false
+ abortOnError false
+ }
+}
+
+repositories {
+ maven { url 'https://jitpack.io' }
+}
+
+dependencies {
+ implementation 'com.google.android.mediahome:video:1.0.0'
+ implementation 'androidx.test.ext:junit-ktx:1.1.3'
+ testImplementation 'org.json:json:20180813'
+
+ implementation 'androidx.core:core-ktx:1.8.0'
+ implementation 'androidx.appcompat:appcompat:1.4.2'
+
+ // dont change this to 1.6.0 it looks ugly af
+ implementation 'com.google.android.material:material:1.5.0'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
+ implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+
+ //implementation "io.karn:khttp-android:0.1.2" //okhttp instead
+// implementation 'org.jsoup:jsoup:1.13.1'
+ implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
+
+ implementation "androidx.preference:preference-ktx:1.2.0"
+
+ implementation 'com.github.bumptech.glide:glide:4.13.1'
+ kapt 'com.github.bumptech.glide:compiler:4.13.1'
+ implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
+
+ implementation 'jp.wasabeef:glide-transformations:4.3.0'
+
+ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+
+ // implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
+
+ // Exoplayer
+ implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
+ implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
+ implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
+ implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
+
+ //implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
+
+ // Bug reports
+ implementation "ch.acra:acra-core:5.8.4"
+ implementation "ch.acra:acra-toast:5.8.4"
+
+ compileOnly "com.google.auto.service:auto-service-annotations:1.0"
+ //either for java sources:
+ annotationProcessor "com.google.auto.service:auto-service:1.0"
+ //or for kotlin sources (requires kapt gradle plugin):
+ kapt "com.google.auto.service:auto-service:1.0"
+
+ // subtitle color picker
+ implementation 'com.jaredrummler:colorpicker:1.1.0'
+
+ //run JS
+ implementation 'org.mozilla:rhino:1.7.14'
+
+ // TorrentStream
+ //implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
+
+ // Downloading
+ implementation "androidx.work:work-runtime:2.7.1"
+ implementation "androidx.work:work-runtime-ktx:2.7.1"
+
+ // Networking
+// implementation "com.squareup.okhttp3:okhttp:4.9.2"
+// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
+ implementation 'com.github.Blatzar:NiceHttp:0.3.2'
+
+ // Util to skip the URI file fuckery 🙏
+ implementation "com.github.tachiyomiorg:unifile:17bec43"
+
+ // API because cba maintaining it myself
+ implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
+
+ implementation 'com.github.discord:OverlappingPanels:0.1.3'
+ // debugImplementation because LeakCanary should only run in debug builds.
+ // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
+
+ // for shimmer when loading
+ implementation 'com.facebook.shimmer:shimmer:0.5.0'
+
+ implementation "androidx.tvprovider:tvprovider:1.0.0"
+
+ // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
+ implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
+
+ // slow af yt
+ //implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
+
+ // newpipe yt
+ implementation 'com.github.recloudstream:NewPipeExtractor:master-SNAPSHOT'
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
+
+ // Library/extensions searching with Levenshtein distance
+ implementation 'me.xdrop:fuzzywuzzy:1.4.0'
+}
+
+task androidSourcesJar(type: Jar) {
+ getArchiveClassifier().set('sources')
+ from android.sourceSets.main.java.srcDirs//full sources
+}
+
+task makeJar(type: Copy) {
+ // after modifying here, you can export. Jar
+ from('build/intermediates/compile_app_classes_jar/debug')
+ into('build') // output location
+ include('classes.jar') // the classes file of the imported rack package
+ dependsOn build
+}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
deleted file mode 100644
index d0c86bab..00000000
--- a/app/build.gradle.kts
+++ /dev/null
@@ -1,304 +0,0 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
-import org.jetbrains.dokka.gradle.DokkaTask
-import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-import java.io.ByteArrayOutputStream
-import java.net.URL
-
-plugins {
- id("com.android.application")
- id("com.google.devtools.ksp")
- id("kotlin-android")
- id("org.jetbrains.dokka")
-}
-
-val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
-val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
-
-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 {
- testOptions {
- unitTests.isReturnDefaultValues = true
- }
-
- viewBinding {
- enable = true
- }
-
- /* disable this for now
- externalNativeBuild {
- cmake {
- path("CMakeLists.txt")
- }
- }*/
-
- signingConfigs {
- if (prereleaseStoreFile != null) {
- create("prerelease") {
- storeFile = file(prereleaseStoreFile)
- storePassword = System.getenv("SIGNING_STORE_PASSWORD")
- keyAlias = System.getenv("SIGNING_KEY_ALIAS")
- keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
- }
- }
- }
-
- compileSdk = 34
- buildToolsVersion = "34.0.0"
-
- defaultConfig {
- applicationId = "com.lagradost.cloudstream3"
- 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"
-
- 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)
-
- buildConfigField(
- "long",
- "BUILD_DATE",
- "${System.currentTimeMillis()}"
- )
- buildConfigField(
- "String",
- "SIMKL_CLIENT_ID",
- "\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\""
- )
- buildConfigField(
- "String",
- "SIMKL_CLIENT_SECRET",
- "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
- )
- testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
-
- ksp {
- arg("room.schemaLocation", "$projectDir/schemas")
- arg("exportSchema", "true")
- }
- }
-
- buildTypes {
- release {
- isDebuggable = false
- isMinifyEnabled = false
- isShrinkResources = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- debug {
- isDebuggable = true
- applicationIdSuffix = ".debug"
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
- }
- }
-
- flavorDimensions.add("state")
- 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")
- } else {
- logger.warn("No prerelease signing config!")
- }
- versionNameSuffix = "-PRE"
- versionCode = (System.currentTimeMillis() / 60000).toInt()
- }
- }
-
- compileOptions {
- isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
- }
-
- lint {
- abortOnError = false
- checkReleaseBuilds = false
- }
-
- buildFeatures {
- buildConfig = true
- }
-
- namespace = "com.lagradost.cloudstream3"
-}
-
-repositories {
- maven("https://jitpack.io")
-}
-
-dependencies {
- // Testing
- 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("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("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")
-
- // 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("androidx.media3:media3-ui:1.1.1")
- implementation("androidx.media3:media3-cast:1.1.1")
- implementation("androidx.media3:media3-common:1.1.1")
- implementation("androidx.media3:media3-session:1.1.1")
- implementation("androidx.media3:media3-exoplayer:1.1.1")
- implementation("com.google.android.mediahome:video:1.0.0")
- implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
- implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
- implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
-
- // PlayBack
- 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:176da72") /* 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("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("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("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") {
- // 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.srcDirs) // Full Sources
-}
-
-tasks.register("copyJar") {
- from(
- "build/intermediates/compile_app_classes_jar/prereleaseDebug",
- "../library/build/libs"
- )
- into("build/app-classes")
- include("classes.jar", "library-jvm*.jar")
- // Remove the version
- rename("library-jvm.*.jar", "library-jvm.jar")
-}
-
-// Merge the app classes and the library classes into classes.jar
-tasks.register("makeJar") {
- // Duplicates cause hard to catch errors, better to fail at compile time.
- duplicatesStrategy = DuplicatesStrategy.FAIL
- dependsOn(tasks.getByName("copyJar"))
- from(
- zipTree("build/app-classes/classes.jar"),
- zipTree("build/app-classes/library-jvm.jar")
- )
- destinationDirectory.set(layout.buildDirectory)
- archivesName = "classes"
-}
-
-tasks.withType {
- kotlinOptions {
- jvmTarget = "1.8"
- freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
- }
-}
-
-tasks.withType().configureEach {
- moduleName.set("Cloudstream")
- dokkaSourceSets {
- named("main") {
- sourceLink {
- // Unix based directory relative path to the root of the project (where you execute gradle respectively).
- localDirectory.set(file("src/main/java"))
-
- // URL showing where the source code can be accessed through the web browser
- remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
-
- // Suffix which is used to append the line number to the URL. Use #L for GitHub
- remoteLineSuffix.set("#L")
- }
- }
- }
-}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index ff59496d..481bb434 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.kts.
+# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
index c7f02baf..201ddea3 100644
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
@@ -1,57 +1,155 @@
package com.lagradost.cloudstream3
-import android.app.Activity
-import android.os.Bundle
-import android.os.PersistableBundle
-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.FragmentHomeBinding
-import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
-import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
-import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
-import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
-import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
-import com.lagradost.cloudstream3.databinding.FragmentResultBinding
-import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
-import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
-import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
-import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
-import com.lagradost.cloudstream3.databinding.HomepageParentBinding
-import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
-import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
-import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
-import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
-import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
-import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
-import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
-import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
-import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
+import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper
-import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
-
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
-class TestApplication : Activity() {
- override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
- super.onCreate(savedInstanceState, persistentState)
- }
-}
-
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
- private fun getAllProviders(): Array {
- println("Providers: ${APIHolder.allProviders.size}")
- return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
+ //@Test
+ //fun useAppContext() {
+ // // Context of the app under test.
+ // val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ // assertEquals("com.lagradost.cloudstream3", appContext.packageName)
+ //}
+
+ private fun getAllProviders(): List {
+ return APIHolder.allProviders //.filter { !it.usesWebView }
+ }
+
+ private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
+ Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
+ if (url == null) return true
+ var linksLoaded = 0
+ try {
+ val success = api.loadLinks(url, false, {}) { link ->
+ Assert.assertTrue(
+ "Api ${api.name} returns link with invalid Quality",
+ Qualities.values().map { it.value }.contains(link.quality)
+ )
+ Assert.assertTrue(
+ "Api ${api.name} returns link with invalid url ${link.url}",
+ link.url.length > 4
+ )
+ linksLoaded++
+ }
+ if (success) {
+ return linksLoaded > 0
+ }
+ Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider has not implemented .loadLinks")
+ }
+ logError(e)
+ }
+ return true
+ }
+
+ private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
+ val searchQueries = listOf("over", "iron", "guy")
+ var correctResponses = 0
+ var searchResult: List? = null
+ for (query in searchQueries) {
+ val response = try {
+ api.search(query)
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider has not implemented .search")
+ }
+ logError(e)
+ null
+ }
+ if (!response.isNullOrEmpty()) {
+ correctResponses++
+ if (searchResult == null) {
+ searchResult = response
+ }
+ }
+ }
+
+ if (correctResponses == 0 || searchResult == null) {
+ System.err.println("Api ${api.name} did not return any valid search responses")
+ return false
+ }
+
+ try {
+ var validResults = false
+ for (result in searchResult) {
+ Assert.assertEquals(
+ "Invalid apiName on response on ${api.name}",
+ result.apiName,
+ api.name
+ )
+ val load = api.load(result.url) ?: continue
+ Assert.assertEquals(
+ "Invalid apiName on load on ${api.name}",
+ load.apiName,
+ result.apiName
+ )
+ Assert.assertTrue(
+ "Api ${api.name} on load does not contain any of the supportedTypes",
+ api.supportedTypes.contains(load.type)
+ )
+ when (load) {
+ is AnimeLoadResponse -> {
+ val gotNoEpisodes =
+ load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
+
+ if (gotNoEpisodes) {
+ println("Api ${api.name} got no episodes on ${load.url}")
+ continue
+ }
+
+ val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
+ validResults = loadLinks(api, url)
+ if (!validResults) continue
+ }
+ is MovieLoadResponse -> {
+ val gotNoEpisodes = load.dataUrl.isBlank()
+ if (gotNoEpisodes) {
+ println("Api ${api.name} got no movie on ${load.url}")
+ continue
+ }
+
+ validResults = loadLinks(api, load.dataUrl)
+ if (!validResults) continue
+ }
+ is TvSeriesLoadResponse -> {
+ val gotNoEpisodes = load.episodes.isEmpty()
+ if (gotNoEpisodes) {
+ println("Api ${api.name} got no episodes on ${load.url}")
+ continue
+ }
+
+ validResults = loadLinks(api, load.episodes.first().data)
+ if (!validResults) continue
+ }
+ }
+ break
+ }
+ if(!validResults) {
+ System.err.println("Api ${api.name} did not load on any")
+ }
+
+ return validResults
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider has not implemented .load")
+ }
+ logError(e)
+ return false
+ }
}
@Test
@@ -60,78 +158,7 @@ class ExampleInstrumentedTest {
println("Done providersExist")
}
- @Throws
- private inline fun testAllLayouts(
- activity: Activity,
- vararg layouts: Int
- ) {
-
- val bind = T::class.java.methods.first { it.name == "bind" }
- val inflater = LayoutInflater.from(activity)
- for (layout in layouts) {
- val root = inflater.inflate(layout, null, false)
- bind.invoke(null, root)
- }
- }
-
@Test
- @Throws
- fun layoutTest() {
- ActivityScenario.launch(MainActivity::class.java).use { scenario ->
- scenario.onActivity { activity: MainActivity ->
- // FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same
- //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
- //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
-
- // main cant be tested
- // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv)
- // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv)
- //testAllLayouts(activity, R.layout.activity_main_tv)
-
- testAllLayouts(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
- testAllLayouts(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
-
- // testAllLayouts(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
- // testAllLayouts(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
-
- testAllLayouts(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
- testAllLayouts(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
- testAllLayouts(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
-
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
-
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
-
- testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
- testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
-
- testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
- testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
-
- testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
- //testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
-
- testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
- testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
-
-
- // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
- // testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
-
- testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
- testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
- testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
-
- testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
- testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
- }
- }
- }
-
- @Test
- @Throws(AssertionError::class)
fun providerCorrectData() {
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
@@ -153,20 +180,66 @@ class ExampleInstrumentedTest {
@Test
fun providerCorrectHomepage() {
runBlocking {
- getAllProviders().toList().amap { api ->
- TestingUtils.testHomepage(api, TestingUtils.Logger())
+ getAllProviders().apmap { api ->
+ if (api.hasMainPage) {
+ try {
+ val homepage = api.getMainPage()
+ when {
+ homepage == null -> {
+ System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
+ }
+ homepage.items.isEmpty() -> {
+ System.err.println("Homepage provider ${api.name} does not contain any items!")
+ }
+ homepage.items.any { it.list.isEmpty() } -> {
+ System.err.println ("Homepage provider ${api.name} does not have any items on result!")
+ }
+ }
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
+ }
+ logError(e)
+ }
+ }
}
}
println("Done providerCorrectHomepage")
}
+// @Test
+// fun testSingleProvider() {
+// testSingleProviderApi(ThenosProvider())
+// }
+
@Test
- fun testAllProvidersCorrect() {
+ fun providerCorrect() {
runBlocking {
- TestingUtils.getDeferredProviderTests(
- this,
- getAllProviders(),
- ) { _, _ -> }
+ val invalidProvider = ArrayList>()
+ val providers = getAllProviders()
+ providers.apmap { api ->
+ try {
+ println("Trying $api")
+ if (testSingleProviderApi(api)) {
+ println("Success $api")
+ } else {
+ System.err.println("Error $api")
+ invalidProvider.add(Pair(api, null))
+ }
+ } catch (e: Exception) {
+ logError(e)
+ invalidProvider.add(Pair(api, e))
+ }
+ }
+ if(invalidProvider.isEmpty()) {
+ println("No Invalid providers! :D")
+ } else {
+ println("Invalid providers are: ")
+ for (provider in invalidProvider) {
+ println("${provider.first}")
+ }
+ }
}
+ println("Done providerCorrect")
}
}
diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png
index 8c374dd9..3c4e788c 100644
Binary files a/app/src/debug/ic_launcher-playstore.png and b/app/src/debug/ic_launcher-playstore.png differ
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png
index c947f526..bf8e595f 100644
Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
index c947f526..bf8e595f 100644
Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png
index e841896f..935b7108 100644
Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
index e841896f..935b7108 100644
Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_banner.png b/app/src/debug/res/mipmap-xhdpi/ic_banner.png
index 6e23cfcf..16c4fdd1 100644
Binary files a/app/src/debug/res/mipmap-xhdpi/ic_banner.png and b/app/src/debug/res/mipmap-xhdpi/ic_banner.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png
index c80f9a10..d62f3f79 100644
Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
index c80f9a10..d62f3f79 100644
Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
index f0b781bb..38d6ede0 100644
Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
index f0b781bb..38d6ede0 100644
Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
index d5fa9d70..81c5621b 100644
Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
index d5fa9d70..81c5621b 100644
Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 888be999..460a47ea 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,27 +1,18 @@
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.lagradost.cloudstream3">
-
+
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+ tools:targetApi="o">
+ android:supportsPictureInPicture="true">
@@ -97,20 +76,16 @@
-->
+
+
-
-
-
-
-
-
-
-
+
+
@@ -128,30 +103,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -165,21 +116,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
@@ -187,14 +123,13 @@
-
+ android:exported="true">
+
@@ -203,11 +138,6 @@
android:name=".ui.ControllerActivity"
android:exported="false" />
-
-
-#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/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png
index d959673a..88e4735c 100644
Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ
diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
index d6f978fe..93469451 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
@@ -4,18 +4,12 @@ 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.google.auto.service.AutoService
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.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
@@ -23,7 +17,6 @@ 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
@@ -31,29 +24,24 @@ 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
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"
+ "https://docs.google.com/forms/u/0/d/e/1FAIpQLSeFmyBChi6HF3IkhTVWPiDXJtxt8W0Hf4Agljm_0-0_QuEYFg/formResponse"
val data = mapOf(
- "entry.1993829403" to errorContent.toJSON()
+ "entry.134906550" to errorContent.toJSON()
)
thread { // to not run it on main thread
runBlocking {
suspendSafeApiCall {
- app.post(url, data = data)
- //println("Report response: $post")
+ val post = app.post(url, data = data)
+ println("Report response: $post")
}
}
}
@@ -66,6 +54,7 @@ class CustomReportSender : ReportSender {
}
}
+@AutoService(ReportSenderFactory::class)
class CustomSenderFactory : ReportSenderFactory {
override fun create(context: Context, config: CoreConfiguration): ReportSender {
return CustomReportSender()
@@ -76,40 +65,7 @@ class CustomSenderFactory : ReportSenderFactory {
}
}
-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
@@ -119,10 +75,10 @@ class AcraApplication : Application() {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
- reportContent = listOf(
+ reportContent = arrayOf(
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
- ReportField.STACK_TRACE,
+ ReportField.STACK_TRACE
)
// removed this due to bug when starting the app, moved it to when it actually crashes
@@ -135,8 +91,6 @@ class AcraApplication : Application() {
}
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()
@@ -146,17 +100,8 @@ class AcraApplication : Application() {
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)
}
@@ -203,14 +148,5 @@ class AcraApplication : Application() {
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()
- )
- }
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index ee3a5d12..0f54770f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -1,95 +1,40 @@
package com.lagradost.cloudstream3
-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.os.Build
-import android.util.DisplayMetrics
+import android.os.Looper
import android.util.Log
-import android.view.Gravity
-import android.view.KeyEvent
-import android.view.View
-import android.view.View.NO_ID
-import android.view.ViewGroup
+import android.view.*
+import android.widget.TextView
import android.widget.Toast
-import androidx.activity.ComponentActivity
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.MainThread
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
-import androidx.core.content.ContextCompat
-import androidx.core.view.children
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.AcraApplication.Companion.getKey
-import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
-import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
-import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType
-import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
-import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
-import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
-import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
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 kotlinx.coroutines.currentCoroutineContext
import org.schabi.newpipe.extractor.NewPipe
-import java.lang.ref.WeakReference
-import java.util.Locale
-import kotlin.math.max
-import kotlin.math.min
-
-enum class FocusDirection {
- Start,
- End,
- Up,
- Down,
-}
+import java.util.*
object CommonActivity {
-
- private var _activity: WeakReference? = null
- var activity
- get() = _activity?.get()
- private set(value) {
- _activity = WeakReference(value)
- }
-
- @MainThread
- fun setActivityInstance(newActivity: Activity?) {
- activity = newActivity
- }
-
@MainThread
fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
}
- val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
-
- // screenWidth and screenHeight does always
- // refer to the screen while in landscape mode
- val screenWidth: Int
- get() {
- return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
- }
- val screenHeight: Int
- get() {
- return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
- }
-
-
var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
@@ -100,32 +45,9 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair) -> Boolean)? = null
- private var currentToast: Toast? = null
- fun showToast(@StringRes message: Int, duration: Int? = null) {
- val act = activity ?: return
- act.runOnUiThread {
- showToast(act, act.getString(message), duration)
- }
- }
+ var currentToast: Toast? = null
- fun showToast(message: String?, duration: Int? = null) {
- val act = activity ?: return
- act.runOnUiThread {
- showToast(act, message, duration)
- }
- }
-
- fun showToast(message: UiText?, duration: Int? = null) {
- val act = activity ?: return
- if (message == null) return
- act.runOnUiThread {
- showToast(act, message.asString(act), duration)
- }
- }
-
-
- @MainThread
fun showToast(act: Activity?, text: UiText, duration: Int) {
if (act == null) return
text.asStringNull(act)?.let {
@@ -133,9 +55,7 @@ object CommonActivity {
}
}
- /** duration is Toast.LENGTH_SHORT if null*/
- @MainThread
- fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
+ fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
if (act == null) return
showToast(act, act.getString(message), duration)
}
@@ -143,7 +63,6 @@ object CommonActivity {
const val TAG = "COMPACT"
/** duration is Toast.LENGTH_SHORT if null*/
- @MainThread
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
if (act == null || message == null) {
Log.w(TAG, "invalid showToast act = $act message = $message")
@@ -156,36 +75,33 @@ object CommonActivity {
} catch (e: Exception) {
logError(e)
}
-
try {
- val binding = ToastBinding.inflate(act.layoutInflater)
- binding.text.text = message.trim()
+ val inflater =
+ act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+
+ val layout: View = inflater.inflate(
+ R.layout.toast,
+ act.findViewById(R.id.toast_layout_root) as ViewGroup?
+ )
+
+ val text = layout.findViewById(R.id.text) as TextView
+ text.text = message.trim()
- // custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
val toast = Toast(act)
- toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
- 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.duration = duration ?: Toast.LENGTH_SHORT
+ toast.view = layout
+ //https://github.com/PureWriter/ToastCompat
toast.show()
-
+ currentToast = toast
} catch (e: Exception) {
logError(e)
}
}
- /**
- * 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 locale = Locale(languageCode)
val resources: Resources = context.resources
val config = resources.configuration
Locale.setDefault(locale)
@@ -202,54 +118,18 @@ object CommonActivity {
setLocale(this, localeCode)
}
- fun init(act: Activity) {
- setActivityInstance(act)
-
- val componentActivity = activity as? ComponentActivity ?: return
-
+ fun init(act: Activity?) {
+ if (act == null) 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
+ act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
+ act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
+
+ act.updateLocale()
- componentActivity.updateLocale()
- componentActivity.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance())
-
- for (resumeApp in resumeApps) {
- resumeApp.launcher =
- componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
- val resultCode = result.resultCode
- val data = result.data
- if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
- val pos = resumeApp.getPosition(data)
- val dur = resumeApp.getDuration(data)
- if (dur > 0L && pos > 0L)
- DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
- removeKey(resumeApp.lastId)
- ResultFragment.updateUI()
- }
- }
- }
-
- // Ask for notification permissions on Android 13
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
- ContextCompat.checkSelfPermission(
- componentActivity,
- Manifest.permission.POST_NOTIFICATIONS
- ) != PackageManager.PERMISSION_GRANTED
- ) {
- val requestPermissionLauncher = componentActivity.registerForActivityResult(
- ActivityResultContracts.RequestPermission()
- ) { isGranted: Boolean ->
- Log.d(TAG, "Notification permission: $isGranted")
- }
- requestPermissionLauncher.launch(
- Manifest.permission.POST_NOTIFICATIONS
- )
- }
}
private fun Activity.enterPIPMode() {
@@ -277,57 +157,28 @@ object CommonActivity {
}
}
- 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) {
- loadThemes(act)
- }
- }
-
- private fun mapSystemTheme(act: Activity): Int {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- val currentNightMode =
- act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
- return when (currentNightMode) {
- Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme
- else -> R.style.AppTheme // Night mode is active, we're using dark theme
- }
- } else {
- return R.style.AppTheme
- }
- }
-
fun loadThemes(act: Activity?) {
if (act == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
val currentTheme =
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
- "System" -> mapSystemTheme(act)
"Black" -> R.style.AppTheme
"Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode
"AmoledLight" -> R.style.AmoledModeLight
- "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
- R.style.MonetMode else R.style.AppTheme
-
else -> R.style.AppTheme
}
val currentOverlayTheme =
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
"Normal" -> R.style.OverlayPrimaryColorNormal
- "DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
- "Orange" -> R.style.OverlayPrimaryColorOrange
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
"Maroon" -> R.style.OverlayPrimaryColorMaroon
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
"Grey" -> R.style.OverlayPrimaryColorGrey
"White" -> R.style.OverlayPrimaryColorWhite
- "CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
"Brown" -> R.style.OverlayPrimaryColorBrown
"Purple" -> R.style.OverlayPrimaryColorPurple
"Green" -> R.style.OverlayPrimaryColorGreen
@@ -336,13 +187,6 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink
- "Lavender" -> R.style.OverlayPrimaryColorLavender
- "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
- R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
-
- "Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
- R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
-
else -> R.style.OverlayPrimaryColorNormal
}
act.theme.applyStyle(currentTheme, true)
@@ -354,207 +198,120 @@ object CommonActivity {
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
}
- /** because we want closes find, aka when multiple have the same id, we go to parent
- until the correct one is found */
- private fun localLook(from: View, id: Int): View? {
- if (id == NO_ID) return null
- var currentLook: View = from
- // limit to 15 look depth
- for (i in 0..15) {
- currentLook.findViewById(id)?.let { return it }
- currentLook = (currentLook.parent as? View) ?: break
- }
- return null
- }
- /*var currentLook: View = view
- while (true) {
- val tmpNext = currentLook.findViewById(nextId)
- if (tmpNext != null) {
- next = tmpNext
- break
- }
- currentLook = currentLook.parent as? View ?: break
- }*/
-
- private fun View.hasContent(): Boolean {
- return isShown && when (this) {
- //is RecyclerView -> this.childCount > 0
- is ViewGroup -> this.childCount > 0
- else -> true
- }
- }
-
- /** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
- fun continueGetNextFocus(
- root: Any?,
- view: View,
- direction: FocusDirection,
- nextId: Int,
- depth: Int = 0
- ): View? {
- if (nextId == NO_ID) return null
-
- // do an initial search for the view, in case the localLook is too deep we can use this as
- // an early break and backup view
- var next =
- when (root) {
- is Activity -> root.findViewById(nextId)
- is View -> root.rootView.findViewById(nextId)
- else -> null
- } ?: return null
-
- next = localLook(view, nextId) ?: next
- val shown = next.hasContent()
-
- // 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.childCount > 0
- } ?: false
- if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
-
- // if not shown then continue because we will "skip" over views to get to a replacement
- if (!shown) {
- // we don't want a while true loop, so we let android decide if we find a recursive view
- if (next == view) return null
- return getNextFocus(root, next, direction, depth + 1)
- }
-
- (when (next) {
- is ChipGroup -> {
- next.children.firstOrNull { it.isFocusable && it.isShown }
- }
-
- is NavigationRailView -> {
- next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home)
- }
-
- else -> null
- })?.let {
- return it
- }
-
- // nothing wrong with the view found, return it
- return next
- }
-
- /** recursively looks for a next focus up to a depth of 10,
- * this is used to override the normal shit focus system
- * because this application has a lot of invisible views that messes with some tv devices*/
- fun getNextFocus(
- root: Any?,
+ private fun getNextFocus(
+ act: Activity?,
view: View?,
direction: FocusDirection,
depth: Int = 0
- ): View? {
- // if input is invalid let android decide + depth test to not crash if loop is found
- if (view == null || depth >= 10 || root == null) {
+ ): Int? {
+ if (view == null || depth >= 10 || act == null) {
return null
}
- var nextId = when (direction) {
- FocusDirection.Start -> {
- if (view.isRtl())
- view.nextFocusRightId
- else
- view.nextFocusLeftId
+ val nextId = when (direction) {
+ FocusDirection.Left -> {
+ view.nextFocusLeftId
}
-
FocusDirection.Up -> {
view.nextFocusUpId
}
-
- FocusDirection.End -> {
- if (view.isRtl())
- view.nextFocusLeftId
- else
- view.nextFocusRightId
+ FocusDirection.Right -> {
+ view.nextFocusRightId
}
-
FocusDirection.Down -> {
view.nextFocusDownId
}
}
- if (nextId == NO_ID) {
- // if not specified then use forward id
- nextId = view.nextFocusForwardId
- // if view is still not found to next focus then return and let android decide
- if (nextId == NO_ID)
- return null
+ return if (nextId != -1) {
+ val next = act.findViewById(nextId)
+ //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
+
+ if (next?.isShown == false) {
+ getNextFocus(act, next, direction, depth + 1)
+ } else {
+ if (depth == 0) {
+ null
+ } else {
+ nextId
+ }
+ }
+ } else {
+ null
}
- return continueGetNextFocus(root, view, direction, nextId, depth)
}
+ enum class FocusDirection {
+ Left,
+ Right,
+ Up,
+ Down,
+ }
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
+ //println("Keycode: $keyCode")
+ //showToast(
+ // this,
+ // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
+ // Toast.LENGTH_LONG
+ //)
+
+ // Tested keycodes on remote:
+ // KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
+ // KeyEvent.KEYCODE_MEDIA_REWIND
+ // KeyEvent.KEYCODE_MENU
+ // KeyEvent.KEYCODE_MEDIA_NEXT
+ // KeyEvent.KEYCODE_MEDIA_PREVIOUS
+ // KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
// 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 -> {
+ KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_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 -> {
+ KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
PlayerEventType.ShowMirrors
}
// OpenSubtitles shortcut
- KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
+ KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
-
- KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
+ KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
PlayerEventType.ShowSpeed
}
-
- KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
+ KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
PlayerEventType.Resize
}
-
- KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
+ KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_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)
@@ -567,67 +324,67 @@ object CommonActivity {
//}
}
- /** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null
- val currentFocus = act.currentFocus
-
event?.keyCode?.let { keyCode ->
- if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
- val nextView = when (keyCode) {
- KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.Start
- )
+ when (event.action) {
+ KeyEvent.ACTION_DOWN -> {
+ if (act.currentFocus != null) {
+ val next = when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Left
+ )
+ KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Right
+ )
+ KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Up
+ )
+ KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Down
+ )
- KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.End
- )
+ else -> null
+ }
- KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.Up
- )
+ if (next != null && next != -1) {
+ val nextView = act.findViewById(next)
+ if (nextView != null) {
+ nextView.requestFocus()
+ keyEventListener?.invoke(Pair(event, true))
+ return true
+ }
+ }
- KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.Down
- )
-
- else -> null
+ when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_CENTER -> {
+ if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
+ UIHelper.showInputMethod(act.currentFocus?.findFocus())
+ }
+ }
+ }
+ }
+ //println("Keycode: $keyCode")
+ //showToast(
+ // this,
+ // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
+ // Toast.LENGTH_LONG
+ //)
+ }
}
- // println("NEXT FOCUS : $nextView")
- if (nextView != null) {
- nextView.requestFocus()
- keyEventListener?.invoke(Pair(event, true))
- return true
- }
-
- if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
- (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
- ) {
- UIHelper.showInputMethod(act.currentFocus?.findFocus())
- }
-
- //println("Keycode: $keyCode")
- //showToast(
- // this,
- // "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
- // consumed. used in video player
if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true
}
return null
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
index 8da7ca38..379a91e4 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
@@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
import okhttp3.OkHttpClient
import okhttp3.RequestBody
-import okhttp3.RequestBody.Companion.toRequestBody
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response
@@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
- private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
+ private val client: OkHttpClient
override fun execute(request: Request): Response {
val httpMethod: String = request.httpMethod()
val url: String = request.url()
@@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
val dataToSend: ByteArray? = request.dataToSend()
var requestBody: RequestBody? = null
if (dataToSend != null) {
- requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
+ requestBody = RequestBody.create(null, dataToSend)
}
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)
@@ -51,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
companion object {
private const val USER_AGENT =
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
+ "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
private var instance: DownloaderTestImpl? = null
/**
@@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
return instance
}
}
+
+ init {
+ client = builder.readTimeout(30, TimeUnit.SECONDS).build()
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt
deleted file mode 100644
index 045a7963..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-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/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
similarity index 55%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
rename to app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index 50dd667b..67283de3 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -1,41 +1,35 @@
package com.lagradost.cloudstream3
+import android.annotation.SuppressLint
+import android.content.Context
+import android.net.Uri
+import android.util.Base64.encodeToString
+import androidx.annotation.WorkerThread
+import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
-import com.fasterxml.jackson.module.kotlin.kotlinModule
+import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
-import com.lagradost.cloudstream3.syncproviders.SyncIdName
-import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
+import com.lagradost.cloudstream3.ui.player.SubtitleData
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
-import com.lagradost.cloudstream3.utils.Coroutines.mainWork
-import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
-import com.lagradost.nicehttp.RequestBodyTypes
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor
-import okhttp3.MediaType.Companion.toMediaTypeOrNull
-import okhttp3.RequestBody.Companion.toRequestBody
-import java.net.URI
import java.text.SimpleDateFormat
import java.util.*
-import kotlin.io.encoding.Base64
-import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.absoluteValue
-
-/**
- * Defines the constant for the all languages preference, if this is set then it is
- * the equivalent of all languages being set
- **/
-const val AllLanguagesName = "universal"
+import kotlin.collections.MutableList
const val USER_AGENT =
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
-
-class ErrorLoadingException(message: String? = null) : Exception(message)
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
-val mapper = JsonMapper.builder().addModule(kotlinModule())
+val mapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
object APIHolder {
@@ -46,14 +40,11 @@ object APIHolder {
private const val defProvider = 0
- // ConcurrentModificationException is possible!!!
- val allProviders = threadSafeListOf()
+ val allProviders: MutableList = arrayListOf()
fun initAll() {
- synchronized(allProviders) {
- for (api in allProviders) {
- api.init()
- }
+ for (api in allProviders) {
+ api.init()
}
apiMap = null
}
@@ -62,52 +53,49 @@ object APIHolder {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
}
- var apis: List = threadSafeListOf()
+ var apis: List = arrayListOf()
var apiMap: Map? = null
fun addPluginMapping(plugin: MainAPI) {
- synchronized(apis) {
- apis = apis + plugin
- }
+ apis = apis + plugin
initMap(true)
}
fun removePluginMapping(plugin: MainAPI) {
- synchronized(apis) {
- apis = apis.filter { it != plugin }
- }
+ apis = apis.filter { it != plugin }
initMap(true)
}
private fun initMap(forcedUpdate: Boolean = false) {
- synchronized(apis) {
- if (apiMap == null || forcedUpdate)
- apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
- }
+ if (apiMap == null || forcedUpdate)
+ apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
}
fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null
- synchronized(allProviders) {
- initMap()
- synchronized(apis) {
- return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
- // Leave the ?. null check, it can crash regardless
- ?: allProviders.firstOrNull { it.name == apiName }
- }
- }
+ initMap()
+ return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
+ ?: allProviders.firstOrNull { it.name == apiName }
}
fun getApiFromUrlNull(url: String?): MainAPI? {
if (url == null) return null
- synchronized(allProviders) {
- allProviders.forEach { api ->
- if (url.startsWith(api.mainUrl)) return api
- }
+ for (api in allProviders) {
+ if (url.startsWith(api.mainUrl))
+ return api
}
return null
}
+ private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
+ return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "")
+ .hashCode()
+ }
+
+ fun LoadResponse.getId(): Int {
+ return getLoadResponseIdFromUrl(url, apiName)
+ }
+
/**
* Gets the website captcha token
* discovered originally by https://github.com/ahmedgamal17
@@ -123,9 +111,10 @@ object APIHolder {
// To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try {
- val uri = URI.create(url)
- val domain = base64Encode(
+ val uri = Uri.parse(url)
+ val domain = encodeToString(
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
+ 0
).replace("\n", "").replace("=", ".")
val vToken =
@@ -162,157 +151,150 @@ object APIHolder {
return null
}
- private var trackerCache: HashMap = hashMapOf()
+ fun Context.getApiSettings(): HashSet {
+ //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
- /** backwards compatibility, use getTracker4 instead */
- suspend fun getTracker(
- titles: List,
- types: Set?,
- year: Int?,
- ): Tracker? = getTracker(titles, types, year, false)
+ val hashSet = HashSet()
+ val activeLangs = getApiProviderLangSettings()
+ hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name })
- /**
- * Get anime tracker information based on title, year and type.
- * Both titles are attempted to be matched with both Romaji and English title.
- * Uses the anilist api.
- *
- * @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
- * @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
- * @param year Optional parameter to only get anime with a specific year
- **/
- suspend fun getTracker(
- titles: List,
- types: Set?,
- year: Int?,
- lessAccurate: Boolean
- ): Tracker? {
- return try {
- require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
+ /*val set = settingsManager.getStringSet(
+ this.getString(R.string.search_providers_list_key),
+ hashSet
+ )?.toHashSet() ?: hashSet
- val mainTitle = titles[0]
- val search =
- trackerCache[mainTitle]
- ?: searchAnilist(mainTitle)?.also {
- trackerCache[mainTitle] = it
- } ?: return null
-
- val res = search.data?.page?.media?.find { media ->
- val matchingYears = year == null || media.seasonYear == year
- val matchingTitles = media.title?.let { title ->
- titles.any { userTitle ->
- title.isMatchingTitles(userTitle)
- }
- } ?: false
-
- val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
- if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
- } ?: return null
-
- Tracker(
- res.idMal,
- res.id.toString(),
- res.coverImage?.extraLarge ?: res.coverImage?.large,
- res.bannerImage
- )
- } catch (t: Throwable) {
- logError(t)
- null
- }
- }
-
- private suspend fun searchAnilist(
- title: String?,
- ): AniSearch? {
- val query = """
- query (
- ${'$'}page: Int = 1
- ${'$'}search: String
- ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]
- ${'$'}type: MediaType
- ) {
- Page(page: ${'$'}page, perPage: 20) {
- media(
- search: ${'$'}search
- sort: ${'$'}sort
- type: ${'$'}type
- ) {
- id
- idMal
- title { romaji english }
- coverImage { extraLarge large }
- bannerImage
- seasonYear
- format
+ val list = HashSet()
+ for (name in set) {
+ val api = getApiFromNameNull(name) ?: continue
+ if (activeLangs.contains(api.lang)) {
+ list.add(name)
}
- }
+ }*/
+ //if (list.isEmpty()) return hashSet
+ //return list
+ return hashSet
+ }
+
+ fun Context.getApiDubstatusSettings(): HashSet {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ val hashSet = HashSet()
+ hashSet.addAll(DubStatus.values())
+ val list = settingsManager.getStringSet(
+ this.getString(R.string.display_sub_key),
+ hashSet.map { it.name }.toMutableSet()
+ ) ?: return hashSet
+
+ val names = DubStatus.values().map { it.name }.toHashSet()
+ //if(realSet.isEmpty()) return hashSet
+
+ return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet()
+ }
+
+ fun Context.getApiProviderLangSettings(): HashSet {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ val hashSet = HashSet()
+ hashSet.add("en") // def is only en
+ val list = settingsManager.getStringSet(
+ this.getString(R.string.provider_lang_key),
+ hashSet.toMutableSet()
+ )
+
+ if (list.isNullOrEmpty()) return hashSet
+ return list.toHashSet()
+ }
+
+ fun Context.getApiTypeSettings(): HashSet {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ val hashSet = HashSet()
+ hashSet.addAll(TvType.values())
+ val list = settingsManager.getStringSet(
+ this.getString(R.string.search_types_list_key),
+ hashSet.map { it.name }.toMutableSet()
+ )
+
+ if (list.isNullOrEmpty()) return hashSet
+
+ val names = TvType.values().map { it.name }.toHashSet()
+ val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet()
+ if (realSet.isEmpty()) return hashSet
+
+ return realSet
+ }
+
+ fun Context.updateHasTrailers() {
+ LoadResponse.isTrailersEnabled = getHasTrailers()
+ }
+
+ private fun Context.getHasTrailers(): Boolean {
+ if (this.isTvSettings()) return false
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
+ }
+
+ fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List {
+ val default = enumValues().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
+ val defaultSet = default.map { it.toString() }.toSet()
+ val currentPrefMedia = try {
+ PreferenceManager.getDefaultSharedPreferences(this)
+ .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet)
+ ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null }
+ } catch (e: Throwable) {
+ null
+ } ?: default
+ val langs = this.getApiProviderLangSettings()
+ val allApis = apis.filter { langs.contains(it.lang) }
+ .filter { api -> api.hasMainPage || !hasHomePageIsRequired }
+ return if (currentPrefMedia.isEmpty()) {
+ allApis
+ } else {
+ // Filter API depending on preferred media type
+ allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } }
}
- """.trimIndent().trim()
+ }
- val data = mapOf(
- "query" to query,
- "variables" to mapOf(
- "search" to title,
- "sort" to "SEARCH_MATCH",
- "type" to "ANIME",
- )
- ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
+ fun Context.filterSearchResultByFilmQuality(data: List): List {
+ // Filter results omitting entries with certain quality
+ if (data.isNotEmpty()) {
+ val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this)
+ ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf())
+ ?.mapNotNull { entry ->
+ entry.toIntOrNull() ?: return@mapNotNull null
+ } ?: listOf()
+ if (filteredSearchQuality.isNotEmpty()) {
+ return data.filter { item ->
+ val searchQualVal = item.quality?.ordinal ?: -1
+ //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}")
+ !filteredSearchQuality.contains(searchQualVal)
+ }
+ }
+ }
+ return data
+ }
- return app.post("https://graphql.anilist.co", requestBody = data)
- .parsedSafe()
+ fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList {
+ // Filter results omitting entries with certain quality
+ if (data.list.isNotEmpty()) {
+ val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this)
+ ?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf())
+ ?.mapNotNull { entry ->
+ entry.toIntOrNull() ?: return@mapNotNull null
+ } ?: listOf()
+ if (filteredSearchQuality.isNotEmpty()) {
+ return HomePageList(
+ name = data.name,
+ isHorizontalImages = data.isHorizontalImages,
+ list = data.list.filter { item ->
+ val searchQualVal = item.quality?.ordinal ?: -1
+ //Log.i("filterSearch", "QuickSearch item => ${item.toJson()}")
+ !filteredSearchQuality.contains(searchQualVal)
+ }
+ )
+ }
+ }
+ return data
}
}
-/*
-// THIS IS WORK IN PROGRESS API
-interface ITag {
- val name: UiText
-}
-
-data class SimpleTag(override val name: UiText, val data: String) : ITag
-
-enum class SelectType {
- SingleSelect,
- MultiSelect,
- MultiSelectAndExclude,
-}
-
-enum class SelectValue {
- Selected,
- Excluded,
-}
-
-interface GenreSelector {
- val title: UiText
- val id : Int
-}
-
-data class TagSelector(
- override val title: UiText,
- override val id : Int,
- val tags: Set,
- val defaultTags : Set = setOf(),
- val selectType: SelectType = SelectType.SingleSelect,
-) : GenreSelector
-
-data class BoolSelector(
- override val title: UiText,
- override val id : Int,
-
- val defaultValue : Boolean = false,
-) : GenreSelector
-
-data class InputField(
- override val title: UiText,
- override val id : Int,
-
- val hint : UiText? = null,
-) : GenreSelector
-
-// This response describes how a user might filter the homepage or search results
-data class GenreResponse(
- val searchSelectors : List,
- val filterSelectors: List = searchSelectors
-) */
/*
0 = Site not good
@@ -333,32 +315,17 @@ data class ProvidersInfoJson(
@JsonProperty("status") var status: Int,
)
-data class SettingsJson(
- @JsonProperty("enableAdult") var enableAdult: Boolean = false,
-)
-
data class MainPageData(
val name: String,
val data: String,
- val horizontalImages: Boolean = false
)
data class MainPageRequest(
val name: String,
val data: String,
- val horizontalImages: Boolean,
- //TODO genre selection or smth
)
-fun mainPage(url: String, name: String, horizontalImages: Boolean = false): MainPageData {
- return MainPageData(name = name, data = url, horizontalImages = horizontalImages)
-}
-
-fun mainPageOf(vararg elements: MainPageData): List {
- return elements.toList()
-}
-
/** return list of MainPageData with url to name, make for more readable code */
fun mainPageOf(vararg elements: Pair): List {
return elements.map { (url, name) -> MainPageData(name = name, data = url) }
@@ -367,7 +334,7 @@ fun mainPageOf(vararg elements: Pair): List {
fun newHomePageResponse(
name: String,
list: List,
- hasNext: Boolean? = null,
+ hasNext: Boolean? = null
): HomePageResponse {
return HomePageResponse(
listOf(HomePageList(name, list)),
@@ -375,17 +342,6 @@ fun newHomePageResponse(
)
}
-fun newHomePageResponse(
- data: MainPageRequest,
- list: List,
- hasNext: Boolean? = null,
-): HomePageResponse {
- return HomePageResponse(
- listOf(HomePageList(data.name, list, data.horizontalImages)),
- hasNext = hasNext ?: list.isNotEmpty()
- )
-}
-
fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse {
return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty())
}
@@ -398,7 +354,6 @@ fun newHomePageResponse(list: List, hasNext: Boolean? = null): Hom
abstract class MainAPI {
companion object {
var overrideData: HashMap? = null
- var settingsForProvider: SettingsJson = SettingsJson()
}
fun init() {
@@ -420,19 +375,7 @@ abstract class MainAPI {
open var storedCredentials: String? = null
open var canBeOverridden: Boolean = true
- /** if this is turned on then it will request the homepage one after the other,
- used to delay if they block many request at the same time*/
- open var sequentialMainPage: Boolean = false
-
- /** in milliseconds, this can be used to add more delay between homepage requests
- * on first load if sequentialMainPage is turned on */
- open var sequentialMainPageDelay: Long = 0L
-
- /** in milliseconds, this can be used to add more delay between homepage requests when scrolling */
- open var sequentialMainPageScrollDelay: Long = 0L
-
- /** used to keep track when last homepage request was in unixtime ms */
- var lastHomepageRequest: Long = 0L
+ //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id
open var lang = "en" // ISO_639_1 check SubtitleHelper
@@ -448,26 +391,12 @@ abstract class MainAPI {
/**Used for testing and can be used to disable the providers if WebView is not available*/
open val usesWebView = false
- /** Determines which plugin a given provider is from. This is the full path to the plugin. */
+ /** Determines which plugin a given provider is from */
var sourcePlugin: String? = null
open val hasMainPage = false
open val hasQuickSearch = false
- /**
- * A set of which ids the provider can open with getLoadUrl()
- * If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
- * an Imdb class which inherits from SyncId.
- *
- * getLoadUrl() is then used to get page url based on that ID.
- *
- * Example:
- * "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
- *
- * This is used to launch pages from personal lists or recommendations using IDs.
- **/
- open val supportedSyncNames = setOf()
-
open val supportedTypes = setOf(
TvType.Movie,
TvType.TvSeries,
@@ -479,10 +408,9 @@ abstract class MainAPI {
open val vpnStatus = VPNStatus.None
open val providerType = ProviderType.DirectProvider
- //emptyList() //
- open val mainPage = listOf(MainPageData("", "", false))
+ open val mainPage = listOf(MainPageData("", ""))
- // @WorkerThread
+ @WorkerThread
open suspend fun getMainPage(
page: Int,
request: MainPageRequest,
@@ -490,17 +418,17 @@ abstract class MainAPI {
throw NotImplementedError()
}
- // @WorkerThread
+ @WorkerThread
open suspend fun search(query: String): List? {
throw NotImplementedError()
}
- // @WorkerThread
+ @WorkerThread
open suspend fun quickSearch(query: String): List? {
throw NotImplementedError()
}
- // @WorkerThread
+ @WorkerThread
/**
* Based on data from search() or getMainPage() it generates a LoadResponse,
* basically opening the info page from a link.
@@ -518,13 +446,13 @@ abstract class MainAPI {
* This function might be updated to include exoplayer timestamps etc in the future
* if the need arises.
* */
- // @WorkerThread
+ @WorkerThread
open suspend fun extractorVerifierJob(extractorData: String?) {
throw NotImplementedError()
}
/**Callback is fired once a link is found, will return true if method is executed successfully*/
- // @WorkerThread
+ @WorkerThread
open suspend fun loadLinks(
data: String,
isCasting: Boolean,
@@ -538,29 +466,34 @@ abstract class MainAPI {
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return null
}
-
- /**
- * Get the load() url based on a sync ID like IMDb or MAL.
- * Only contains SyncIds based on supportedSyncUrls.
- **/
- open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
- return null
- }
}
/** Might need a different implementation for desktop*/
+@SuppressLint("NewApi")
fun base64Decode(string: String): String {
return String(base64DecodeArray(string), Charsets.ISO_8859_1)
}
-@OptIn(ExperimentalEncodingApi::class)
+
+@SuppressLint("NewApi")
fun base64DecodeArray(string: String): ByteArray {
- return Base64.decode(string)
+ return try {
+ android.util.Base64.decode(string, android.util.Base64.DEFAULT)
+ } catch (e: Exception) {
+ Base64.getDecoder().decode(string)
+ }
}
-@OptIn(ExperimentalEncodingApi::class)
+
+@SuppressLint("NewApi")
fun base64Encode(array: ByteArray): String {
- return Base64.encode(array)
+ return try {
+ String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1)
+ } catch (e: Exception) {
+ String(Base64.getEncoder().encode(array))
+ }
}
+class ErrorLoadingException(message: String? = null) : Exception(message)
+
fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) {
return null
@@ -594,6 +527,10 @@ fun sortUrls(urls: Set): List {
return urls.sortedBy { t -> -t.quality }
}
+fun sortSubs(subs: Set): List {
+ return subs.sortedBy { it.name }
+}
+
fun capitalizeString(str: String): String {
return capitalizeStringNullable(str) ?: str
}
@@ -615,20 +552,6 @@ fun fixTitle(str: String): String {
}
}
-/**
- * Get rhino context in a safe way as it needs to be initialized on the main thread.
- * Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
- * Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
- **/
-suspend fun getRhinoContext(): org.mozilla.javascript.Context {
- return Coroutines.mainWork {
- val rhino = org.mozilla.javascript.Context.enter()
- rhino.initSafeStandardObjects()
- rhino.optimizationLevel = -1
- rhino
- }
-}
-
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
fun imdbUrlToId(url: String): String? {
return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1)
@@ -677,25 +600,7 @@ enum class TvType(value: Int?) {
AsianDrama(9),
Live(10),
NSFW(11),
- Others(12),
- Music(13),
- AudioBook(14),
-
- /** Wont load the built in player, make your own interaction */
- CustomMedia(15),
-}
-
-public enum class AutoDownloadMode(val value: Int) {
- Disable(0),
- FilterByLang(1),
- All(2),
- NsfwOnly(3)
- ;
-
- companion object {
- infix fun getEnum(value: Int): AutoDownloadMode? =
- AutoDownloadMode.values().firstOrNull { it.value == value }
- }
+ Others(12)
}
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
@@ -986,11 +891,8 @@ data class TvSeriesSearchResponse(
) : SearchResponse
data class TrailerData(
- val extractorUrl: String,
- val referer: String?,
- val raw: Boolean,
- //var mirros: List,
- //var subtitles: List = emptyList(),
+ var mirros: List,
+ var subtitles: List = emptyList(),
)
interface LoadResponse {
@@ -1012,30 +914,14 @@ interface LoadResponse {
var syncData: MutableMap
var posterHeaders: Map?
var backgroundPosterUrl: String?
- var contentRating: String?
companion object {
- var malIdPrefix = "" //malApi.idPrefix
- var aniListIdPrefix = "" //aniListApi.idPrefix
- var simklIdPrefix = "" //simklApi.idPrefix
+ private val malIdPrefix = malApi.idPrefix
+ private val aniListIdPrefix = aniListApi.idPrefix
var isTrailersEnabled = true
- /**
- * The ID string is a way to keep a collection of services in one single ID using a map
- * This adds a database service (like imdb) to the string and returns the new string.
- */
- fun addIdToString(idString: String?, database: SimklSyncServices, id: String?): String? {
- if (id == null) return idString
- return (readIdFromString(idString) + mapOf(database to id)).toJson()
- }
-
- /** Read the id string to get all other ids */
- fun readIdFromString(idString: String?): Map {
- return tryParseJson(idString) ?: return emptyMap()
- }
-
fun LoadResponse.isMovie(): Boolean {
- return this.type.isMovieType() || this is MovieLoadResponse
+ return this.type.isMovieType()
}
@JvmName("addActorNames")
@@ -1053,20 +939,6 @@ interface LoadResponse {
this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) }
}
- /**
- * Internal helper function to add simkl ids from other databases.
- */
- private fun LoadResponse.addSimklId(
- database: SimklSyncServices,
- id: String?
- ) {
- normalSafeApiCall {
- this.syncData[simklIdPrefix] =
- addIdToString(this.syncData[simklIdPrefix], database, id.toString())
- ?: return@normalSafeApiCall
- }
- }
-
@JvmName("addActorsOnly")
fun LoadResponse.addActors(actors: List?) {
this.actors = actors?.map { actor -> ActorData(actor) }
@@ -1080,30 +952,12 @@ interface LoadResponse {
return this.syncData[aniListIdPrefix]
}
- fun LoadResponse.getImdbId(): String? {
- return normalSafeApiCall {
- readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Imdb]
- }
- }
-
- fun LoadResponse.getTMDbId(): String? {
- return normalSafeApiCall {
- readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Tmdb]
- }
- }
-
fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString()
- this.addSimklId(SimklSyncServices.Mal, id.toString())
}
fun LoadResponse.addAniListId(id: Int?) {
this.syncData[aniListIdPrefix] = (id ?: return).toString()
- this.addSimklId(SimklSyncServices.AniList, id.toString())
- }
-
- fun LoadResponse.addSimklId(id: Int?) {
- this.addSimklId(SimklSyncServices.Simkl, id.toString())
}
fun LoadResponse.addImdbUrl(url: String?) {
@@ -1117,8 +971,7 @@ interface LoadResponse {
addRaw: Boolean = false
) {
if (!isTrailersEnabled || trailerUrl.isNullOrBlank()) return
- this.trailers.add(TrailerData(trailerUrl, referer, addRaw))
- /*val links = arrayListOf()
+ val links = arrayListOf()
val subs = arrayListOf()
if (!loadExtractor(
trailerUrl,
@@ -1142,13 +995,12 @@ interface LoadResponse {
)
} else {
this.trailers.add(TrailerData(links, subs))
- }*/
+ }
}
- /*
fun LoadResponse.addTrailer(newTrailers: List) {
trailers.addAll(newTrailers.map { TrailerData(listOf(it)) })
- }*/
+ }
suspend fun LoadResponse.addTrailer(
trailerUrls: List?,
@@ -1156,8 +1008,7 @@ interface LoadResponse {
addRaw: Boolean = false
) {
if (!isTrailersEnabled || trailerUrls == null) return
- trailers.addAll(trailerUrls.map { TrailerData(it, referer, addRaw) })
- /*val trailers = trailerUrls.filter { it.isNotBlank() }.amap { trailerUrl ->
+ val trailers = trailerUrls.filter { it.isNotBlank() }.apmap { trailerUrl ->
val links = arrayListOf()
val subs = arrayListOf()
if (!loadExtractor(
@@ -1180,12 +1031,11 @@ interface LoadResponse {
links to subs
}
}.map { (links, subs) -> TrailerData(links, subs) }
- this.trailers.addAll(trailers)*/
+ this.trailers.addAll(trailers)
}
fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync
- this.addSimklId(SimklSyncServices.Imdb, id)
}
fun LoadResponse.addTrackId(id: String?) {
@@ -1198,7 +1048,6 @@ interface LoadResponse {
fun LoadResponse.addTMDbId(id: String?) {
// TODO add TMDb sync
- this.addSimklId(SimklSyncServices.Tmdb, id)
}
fun LoadResponse.addRating(text: String?) {
@@ -1220,43 +1069,18 @@ interface LoadResponse {
fun getDurationFromString(input: String?): Int? {
val cleanInput = input?.trim()?.replace(" ", "") ?: return null
- //Use first as sometimes the text passes on the 2 other Regex, but failed to provide accurate return value
- Regex("(\\d+\\shr)|(\\d+\\shour)|(\\d+\\smin)|(\\d+\\ssec)").findAll(input).let { values ->
- var seconds = 0
- values.forEach {
- val time_text = it.value
- if (time_text.isNotBlank()) {
- val time = time_text.filter { s -> s.isDigit() }.trim().toInt()
- val scale = time_text.filter { s -> !s.isDigit() }.trim()
- //println("Scale: $scale")
- val timeval = when (scale) {
- "hr", "hour" -> time * 60 * 60
- "min" -> time * 60
- "sec" -> time
- else -> 0
- }
- seconds += timeval
- }
- }
- if (seconds > 0) {
- return seconds / 60
- }
- }
Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 3) {
val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull()
- if (minutes != null && hours != null) {
- return hours * 60 + minutes
- }
+ return if (minutes != null && hours != null) {
+ hours * 60 + minutes
+ } else null
}
}
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 2) {
- val return_value = values[1].toIntOrNull()
- if (return_value != null) {
- return return_value
- }
+ return values[1].toIntOrNull()
}
}
return null
@@ -1274,33 +1098,15 @@ fun LoadResponse?.isAnimeBased(): Boolean {
fun TvType?.isEpisodeBased(): Boolean {
if (this == null) return false
- return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
+ return (this == TvType.TvSeries || this == TvType.Anime)
}
+
data class NextAiring(
val episode: Int,
val unixTime: Long,
- val season: Int? = null,
-) {
- /**
- * Secondary constructor for backwards compatibility without season.
- * TODO Remove this constructor after there is a new stable release and extensions are updated to support season.
- */
- constructor(
- episode: Int,
- unixTime: Long,
- ) : this(
- episode,
- unixTime,
- null
- )
-}
+)
-/**
- * @param season To be mapped with episode season, not shown in UI if displaySeason is defined
- * @param name To be shown next to the season like "Season $displaySeason $name" but if displaySeason is null then "$name"
- * @param displaySeason What to be displayed next to the season name, if null then the name is the only thing shown.
- * */
data class SeasonData(
val season: Int,
val name: String? = null,
@@ -1311,16 +1117,6 @@ interface EpisodeResponse {
var showStatus: ShowStatus?
var nextAiring: NextAiring?
var seasonNames: List?
- fun getLatestEpisodes(): Map
-
- /** Count all episodes in all previous seasons up until this episode to get a total count.
- * Example:
- * Season 1: 10 episodes.
- * Season 2: 6 episodes.
- *
- * getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13
- * */
- fun getTotalEpisodeIndex(episode: Int, season: Int): Int
}
@JvmName("addSeasonNamesString")
@@ -1358,55 +1154,7 @@ data class TorrentLoadResponse(
override var syncData: MutableMap = mutableMapOf(),
override var posterHeaders: Map? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : LoadResponse {
- /**
- * Secondary constructor for backwards compatibility without contentRating.
- * Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
- */
- constructor(
- name: String,
- url: String,
- apiName: String,
- magnet: String?,
- torrent: String?,
- plot: String?,
- type: TvType = TvType.Torrent,
- posterUrl: String? = null,
- year: Int? = null,
- rating: Int? = null,
- tags: List? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- name,
- url,
- apiName,
- magnet,
- torrent,
- plot,
- type,
- posterUrl,
- year,
- rating,
- tags,
- duration,
- trailers,
- recommendations,
- actors,
- comingSoon,
- syncData,
- posterHeaders,
- backgroundPosterUrl,
- null
- )
-}
+) : LoadResponse
data class AnimeLoadResponse(
var engName: String? = null,
@@ -1437,97 +1185,11 @@ data class AnimeLoadResponse(
override var nextAiring: NextAiring? = null,
override var seasonNames: List? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : LoadResponse, EpisodeResponse {
- override fun getLatestEpisodes(): Map {
- return episodes.map { (status, episodes) ->
- val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
- .takeUnless { it == Int.MIN_VALUE }
- status to episodes
- .filter { it.season == maxSeason }
- .maxOfOrNull { it.episode ?: Int.MIN_VALUE }
- .takeUnless { it == Int.MIN_VALUE }
- }.toMap()
- }
+) : LoadResponse, EpisodeResponse
- override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
- val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
-
- return this.episodes.maxOf { (_, episodes) ->
- episodes.count { episodeData ->
- // Prioritize display season as actual season may be something random to fit multiple seasons into one.
- val episodeSeason =
- displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
- // Count all episodes from season 1 to below the current season.
- episodeSeason in 1..> = mutableMapOf(),
- showStatus: ShowStatus? = null,
- plot: String? = null,
- tags: List? = null,
- synonyms: List? = null,
- rating: Int? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- nextAiring: NextAiring? = null,
- seasonNames: List? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- engName,
- japName,
- name,
- url,
- apiName,
- type,
- posterUrl,
- year,
- episodes,
- showStatus,
- plot,
- tags,
- synonyms,
- rating,
- duration,
- trailers,
- recommendations,
- actors,
- comingSoon,
- syncData,
- posterHeaders,
- nextAiring,
- seasonNames,
- backgroundPosterUrl,
- null
- )
-}
-
-/**
- * If episodes already exist appends the list.
- * */
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List?) {
if (episodes.isNullOrEmpty()) return
- this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes
+ this.episodes[status] = episodes
}
suspend fun MainAPI.newAnimeLoadResponse(
@@ -1571,36 +1233,7 @@ data class LiveStreamLoadResponse(
override var syncData: MutableMap = mutableMapOf(),
override var posterHeaders: Map? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : LoadResponse {
- /**
- * Secondary constructor for backwards compatibility without contentRating.
- * Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
- */
- constructor(
- name: String,
- url: String,
- apiName: String,
- dataUrl: String,
- posterUrl: String? = null,
- year: Int? = null,
- plot: String? = null,
- type: TvType = TvType.Live,
- rating: Int? = null,
- tags: List? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- name, url, apiName, dataUrl, posterUrl, year, plot, type, rating, tags, duration, trailers,
- recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
- )
-}
+) : LoadResponse
data class MovieLoadResponse(
override var name: String,
@@ -1623,36 +1256,7 @@ data class MovieLoadResponse(
override var syncData: MutableMap = mutableMapOf(),
override var posterHeaders: Map? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : LoadResponse {
- /**
- * Secondary constructor for backwards compatibility without contentRating.
- * Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
- */
- constructor(
- name: String,
- url: String,
- apiName: String,
- type: TvType,
- dataUrl: String,
- posterUrl: String? = null,
- year: Int? = null,
- plot: String? = null,
- rating: Int? = null,
- tags: List? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
- recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
- )
-}
+) : LoadResponse
suspend fun MainAPI.newMovieLoadResponse(
name: String,
@@ -1700,17 +1304,7 @@ suspend fun MainAPI.newMovieLoadResponse(
builder.initializer()
return builder
}
-/** Episode information that will be passed to LoadLinks function & showed on UI
- * @property data string used as main LoadLinks fun parameter.
- * @property name Name of the Episode.
- * @property season Season number.
- * @property episode Episode number.
- * @property posterUrl URL of Episode's poster image.
- * @property rating Episode rating.
- * @property date Episode air date, see addDate.
- * @property runTime Episode runtime in seconds.
- * @see[addDate]
- * */
+
data class Episode(
var data: String,
var name: String? = null,
@@ -1720,25 +1314,7 @@ data class Episode(
var rating: Int? = null,
var description: String? = null,
var date: Long? = null,
- var runTime: Int? = null,
-) {
- /**
- * Secondary constructor for backwards compatibility without runTime.
- * TODO Remove this constructor after there is a new stable release and extensions are updated to support runTime.
- */
- constructor(
- data: String,
- name: String? = null,
- season: Int? = null,
- episode: Int? = null,
- posterUrl: String? = null,
- rating: Int? = null,
- description: String? = null,
- date: Long? = null,
- ) : this(
- data, name, season, episode, posterUrl, rating, description, date, null
- )
-}
+)
fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
try {
@@ -1780,28 +1356,6 @@ fun MainAPI.newEpisode(
return builder
}
-interface IDownloadableMinimum {
- val url: String
- val referer: String
- val headers: Map
-}
-
-fun IDownloadableMinimum.getId(): Int {
- return url.hashCode()
-}
-
-/**
- * Set of sync services simkl is compatible with.
- * Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id
- */
-enum class SimklSyncServices(val originalName: String) {
- Simkl("simkl"),
- Imdb("imdb"),
- Tmdb("tmdb"),
- AniList("anilist"),
- Mal("mal"),
-}
-
data class TvSeriesLoadResponse(
override var name: String,
override var url: String,
@@ -1826,81 +1380,7 @@ data class TvSeriesLoadResponse(
override var nextAiring: NextAiring? = null,
override var seasonNames: List? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : LoadResponse, EpisodeResponse {
- override fun getLatestEpisodes(): Map {
- val maxSeason =
- episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
- val max = episodes
- .filter { it.season == maxSeason }
- .maxOfOrNull { it.episode ?: Int.MIN_VALUE }
- .takeUnless { it == Int.MIN_VALUE }
- return mapOf(DubStatus.None to max)
- }
-
- override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
- val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
-
- return episodes.count { episodeData ->
- // Prioritize display season as actual season may be something random to fit multiple seasons into one.
- val episodeSeason =
- displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
- // Count all episodes from season 1 to below the current season.
- episodeSeason in 1..,
- posterUrl: String? = null,
- year: Int? = null,
- plot: String? = null,
- showStatus: ShowStatus? = null,
- rating: Int? = null,
- tags: List? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- nextAiring: NextAiring? = null,
- seasonNames: List? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- name,
- url,
- apiName,
- type,
- episodes,
- posterUrl,
- year,
- plot,
- showStatus,
- rating,
- tags,
- duration,
- trailers,
- recommendations,
- actors,
- comingSoon,
- syncData,
- posterHeaders,
- nextAiring,
- seasonNames,
- backgroundPosterUrl,
- null
- )
-}
+) : LoadResponse, EpisodeResponse
suspend fun MainAPI.newTvSeriesLoadResponse(
name: String,
@@ -1932,74 +1412,3 @@ fun fetchUrls(text: String?): List {
fun String?.toRatingInt(): Int? =
this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt()
-
-data class Tracker(
- val malId: Int? = null,
- val aniId: String? = null,
- val image: String? = null,
- val cover: String? = null,
-)
-
-data class AniSearch(
- @JsonProperty("data") var data: Data? = Data()
-) {
- data class Data(
- @JsonProperty("Page") var page: Page? = Page()
- ) {
- data class Page(
- @JsonProperty("media") var media: ArrayList = arrayListOf()
- ) {
- data class Media(
- @JsonProperty("title") var title: Title? = null,
- @JsonProperty("id") var id: Int? = null,
- @JsonProperty("idMal") var idMal: Int? = null,
- @JsonProperty("seasonYear") var seasonYear: Int? = null,
- @JsonProperty("format") var format: String? = null,
- @JsonProperty("coverImage") var coverImage: CoverImage? = null,
- @JsonProperty("bannerImage") var bannerImage: String? = null,
- ) {
- data class CoverImage(
- @JsonProperty("extraLarge") var extraLarge: String? = null,
- @JsonProperty("large") var large: String? = null,
- )
-
- data class Title(
- @JsonProperty("romaji") var romaji: String? = null,
- @JsonProperty("english") var english: String? = null,
- ) {
- fun isMatchingTitles(title: String?): Boolean {
- if (title == null) return false
- return english.equals(title, true) || romaji.equals(title, true)
- }
- }
- }
- }
- }
-}
-
-/**
- * used for the getTracker() method
- **/
-enum class TrackerType {
- MOVIE,
- TV,
- TV_SHORT,
- ONA,
- OVA,
- SPECIAL,
- MUSIC;
-
- companion object {
- fun getTypes(type: TvType): Set {
- return when (type) {
- TvType.Movie -> setOf(MOVIE)
- TvType.AnimeMovie -> setOf(MOVIE)
- TvType.TvSeries -> setOf(TV, TV_SHORT)
- TvType.Anime -> setOf(TV, TV_SHORT, ONA, OVA)
- TvType.OVA -> setOf(OVA, SPECIAL, ONA)
- TvType.Others -> setOf(MUSIC)
- else -> emptySet()
- }
- }
- }
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 5408d2a8..236c7912 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -1,38 +1,18 @@
package com.lagradost.cloudstream3
-import android.animation.ValueAnimator
import android.content.ComponentName
-import android.content.Context
import android.content.Intent
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.KeyEvent
import android.view.Menu
import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
import android.view.WindowManager
-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.constraintlayout.widget.ConstraintLayout
-import androidx.core.view.children
-import androidx.core.view.isGone
-import androidx.core.view.isInvisible
import androidx.core.view.isVisible
-import androidx.core.view.marginStart
-import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
@@ -41,440 +21,122 @@ import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.LinearSnapHelper
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.gms.cast.framework.CastContext
-import com.google.android.gms.cast.framework.Session
-import com.google.android.gms.cast.framework.SessionManager
-import com.google.android.gms.cast.framework.SessionManagerListener
-import com.google.android.material.bottomnavigation.BottomNavigationView
-import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.google.android.gms.cast.framework.*
import com.google.android.material.navigationrail.NavigationRailView
-import com.google.android.material.snackbar.Snackbar
-import com.google.common.collect.Comparators.min
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis
+import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.initAll
-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.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
-import com.lagradost.cloudstream3.CommonActivity.screenHeight
-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.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.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.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.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.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
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.download.DOWNLOAD_NAVIGATE_TO
-import com.lagradost.cloudstream3.ui.home.HomeViewModel
-import com.lagradost.cloudstream3.ui.library.LibraryViewModel
-import com.lagradost.cloudstream3.ui.player.BasicLink
-import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
-import com.lagradost.cloudstream3.ui.player.LinkGenerator
-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.result.ResultFragment
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.isLayout
-import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
-import com.lagradost.cloudstream3.utils.ApkInstaller
-import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings
-import com.lagradost.cloudstream3.utils.AppContextUtils.html
-import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable
-import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr
-import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable
-import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
-import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
-import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers
-import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
-import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
-import com.lagradost.cloudstream3.utils.BackupUtils.backup
+import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
+import com.lagradost.cloudstream3.utils.AppUtils.loadCache
+import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
+import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
-import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
+import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
-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.DataStoreHelper.setViewPos
import com.lagradost.cloudstream3.utils.Event
+import com.lagradost.cloudstream3.utils.IOnBackPressed
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.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.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.toPx
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
-import com.lagradost.cloudstream3.utils.fcast.FcastManager
-import com.lagradost.safefile.SafeFile
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
+import com.lagradost.nicehttp.Requests
+import com.lagradost.nicehttp.ResponseParser
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.fragment_result_swipe.*
import java.io.File
-import java.lang.ref.WeakReference
import java.net.URI
-import java.net.URLDecoder
-import java.nio.charset.Charset
-import kotlin.math.abs
-import kotlin.math.absoluteValue
-import kotlin.system.exitProcess
+import kotlin.reflect.KClass
-//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/
-//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
+const val VLC_PACKAGE = "org.videolan.vlc"
+const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
+val VLC_COMPONENT: ComponentName =
+ ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
+const val VLC_REQUEST_CODE = 42
-// https://www.webvideocaster.com/integrations
+const val VLC_FROM_START = -1
+const val VLC_FROM_PROGRESS = -2
+const val VLC_EXTRA_POSITION_OUT = "extra_position"
+const val VLC_EXTRA_DURATION_OUT = "extra_duration"
+const val VLC_LAST_ID_KEY = "vlc_last_open_id"
-//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
+// Short name for requests client to make it nicer to use
-class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
+var app = Requests(responseParser = object : ResponseParser {
+ val mapper: ObjectMapper = jacksonObjectMapper().configure(
+ DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
+ false
+ )
+
+ override fun parse(text: String, kClass: KClass): T {
+ return mapper.readValue(text, kClass.java)
+ }
+
+ override fun parseSafe(text: String, kClass: KClass): T? {
+ return try {
+ mapper.readValue(text, kClass.java)
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ override fun writeValueAsString(obj: Any): String {
+ return mapper.writeValueAsString(obj)
+ }
+}).apply {
+ defaultHeaders = mapOf("user-agent" to USER_AGENT)
+}
+
+class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
- const val VLC_PACKAGE = "org.videolan.vlc"
- const val MPV_PACKAGE = "is.xyz.mpv"
- const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
-
- val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
- val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
-
- //TODO REFACTOR AF
- open class ResultResume(
- val packageString: String,
- val action: String = Intent.ACTION_VIEW,
- val position: String? = null,
- val duration: String? = null,
- var launcher: ActivityResultLauncher? = null,
- ) {
- val defaultTime = -1L
-
- val lastId get() = "${packageString}_last_open_id"
- suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
- val intent = Intent(action)
-
- if (id != null)
- setKey(lastId, id)
- else
- removeKey(lastId)
-
- intent.setPackage(packageString)
- callback.invoke(intent)
- launcher?.launch(intent)
- }
-
- open fun getPosition(intent: Intent?): Long {
- return defaultTime
- }
-
- open fun getDuration(intent: Intent?): Long {
- return defaultTime
- }
- }
-
- val VLC = object : ResultResume(
- VLC_PACKAGE,
- // Android 13 intent restrictions fucks up specifically launching the VLC player
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- "org.videolan.vlc.player.result"
- } else {
- Intent.ACTION_VIEW
- },
- "extra_position",
- "extra_duration",
- ) {
- override fun getPosition(intent: Intent?): Long {
- return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
- }
-
- override fun getDuration(intent: Intent?): Long {
- return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
- }
- }
-
- val MPV = object : ResultResume(
- MPV_PACKAGE,
- //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
- position = "position",
- duration = "duration",
- ) {
- override fun getPosition(intent: Intent?): Long {
- return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong()
- ?: defaultTime
- }
-
- override fun getDuration(intent: Intent?): Long {
- return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong()
- ?: defaultTime
- }
- }
-
- val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
-
- val resumeApps = arrayOf(
- VLC, MPV, WEB_VIDEO
- )
-
-
const val TAG = "MAINACT"
- const val ANIMATED_OUTLINE: Boolean = false
- var lastError: String? = null
+ var context : MainActivity? = null
- private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY"
-
- /**
- * Transient files to delete on application exit.
- * Deletes files on onDestroy().
- */
- private var filesToDelete: Set
- // This needs to be persistent because the application may exit without calling onDestroy.
- get() = getKey>(FILE_DELETE_KEY) ?: setOf()
- private set(value) = setKey(FILE_DELETE_KEY, value)
-
- /**
- * Add file to delete on Exit.
- */
- fun deleteFileOnExit(file: File) {
- filesToDelete = filesToDelete + file.path
- }
-
- /**
- * Setting this will automatically enter the query in the search
- * next time the search fragment is opened.
- * This variable will clear itself after one use. Null does nothing.
- *
- * This is a very bad solution but I was unable to find a better one.
- **/
- var nextSearchQuery: String? = null
-
- /**
- * Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
- * Boolean signifies if stuff should be force reloaded (true if force reload, false if reload when necessary).
- *
- * The force reloading are used for plugin development to instantly reload the page on deployWithAdb
- * */
val afterPluginsLoadedEvent = Event()
val mainPluginsLoadedEvent =
Event() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event()
-
- // kinda shitty solution, but cant com main->home otherwise for popups
- val bookmarksUpdatedEvent = Event()
-
- /**
- * Used by DataStoreHelper to fully reload home when switching accounts
- */
- val reloadHomeEvent = Event()
-
- /**
- * Used by DataStoreHelper to fully reload library when switching accounts
- */
- val reloadLibraryEvent = Event()
-
-
- /**
- * @return true if the str has launched an app task (be it successful or not)
- * @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
- * */
- fun handleAppIntentUrl(
- activity: FragmentActivity?,
- str: String?,
- isWebview: Boolean
- ): Boolean =
- with(activity) {
- // TODO MUCH BETTER HANDLING
-
- // Invalid URIs can crash
- fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
-
- if (str != null && this != null) {
- if (str.startsWith("https://cs.repo")) {
- val realUrl = "https://" + str.substringAfter("?")
- println("Repository url: $realUrl")
- loadRepository(realUrl)
- return true
- } else if (str.contains(APP_STRING)) {
- for (api in OAuth2Apis) {
- if (str.contains("/${api.redirectUrl}")) {
- ioSafe {
- Log.i(TAG, "handleAppIntent $str")
- 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
- }
- }
- }
- return true
- }
- }
- // 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:") {
- PluginManager.hotReloadAllLocalPlugins(activity)
- }
- } else if (safeURI(str)?.scheme == APP_STRING_REPO) {
- val url = str.replaceFirst(APP_STRING_REPO, "https")
- loadRepository(url)
- return true
- } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) {
- val query = str.substringAfter("$APP_STRING_SEARCH://")
- nextSearchQuery =
- try {
- URLDecoder.decode(query, "UTF-8")
- } catch (t: Throwable) {
- logError(t)
- query
- }
- // Use both navigation views to support both layouts.
- // It might be better to use the QuickSearch.
- activity?.findViewById(R.id.nav_view)?.selectedItemId =
- R.id.navigation_search
- activity?.findViewById(R.id.nav_rail_view)?.selectedItemId =
- R.id.navigation_search
- } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
- val uri = Uri.parse(str)
- val name = uri.getQueryParameter("name")
- val url = URLDecoder.decode(uri.authority, "UTF-8")
-
- navigate(
- R.id.global_to_navigation_player,
- GeneratorPlayer.newInstance(
- LinkGenerator(
- listOf(BasicLink(url, name)),
- extract = true,
- )
- )
- )
- } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
- val id =
- str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
- ?: return false
- ioSafe {
- val resumeWatchingCard =
- HomeViewModel.getResumeWatching()?.firstOrNull { it.id == id }
- ?: return@ioSafe
- activity.loadSearchResult(
- resumeWatchingCard,
- START_ACTION_RESUME_LATEST
- )
- }
- } else if (!isWebview) {
- if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
- this.navigate(R.id.navigation_downloads)
- return true
- } else {
- synchronized(apis) {
- for (api in apis) {
- if (str.startsWith(api.mainUrl)) {
- loadResult(str, api.name)
- return true
- }
- }
- }
- }
- }
- }
- return false
- }
- }
-
- var lastPopup: SearchResponse? = null
- fun loadPopup(result: SearchResponse, load: Boolean = true) {
- lastPopup = result
- val syncName = syncViewModel.syncName(result.apiName)
-
- // based on apiName we decide on if it is a local list or not, this is because
- // we want to show a bit of extra UI to sync apis
- if (result is SyncAPI.LibraryItem && syncName != null) {
- isLocalList = false
- syncViewModel.setSync(syncName, result.syncId)
- syncViewModel.updateMetaAndUser()
- } else {
- isLocalList = true
- syncViewModel.clear()
- }
-
- if (load) {
- viewModel.load(
- this, result.url, result.apiName, false, if (getApiDubstatusSettings()
- .contains(DubStatus.Dubbed)
- ) DubStatus.Dubbed else DubStatus.Subbed, null
- )
- } else {
- viewModel.loadSmall(result)
- }
}
override fun onColorSelected(dialogId: Int, color: Int) {
@@ -488,7 +150,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateLocale() // android fucks me by chaining lang when rotating the phone
- updateTheme(this) // Update if system theme
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
@@ -499,7 +160,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this.hideKeyboard()
// Fucks up anime info layout since that has its own layout
- binding?.castMiniControllerHolder?.isVisible =
+ cast_mini_controller_holder?.isVisible =
!listOf(
R.id.navigation_results_phone,
R.id.navigation_results_tv,
@@ -509,7 +170,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
val isNavVisible = listOf(
R.id.navigation_home,
R.id.navigation_search,
- R.id.navigation_library,
R.id.navigation_downloads,
R.id.navigation_settings,
R.id.navigation_download_child,
@@ -519,98 +179,30 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_settings_updates,
R.id.navigation_settings_ui,
R.id.navigation_settings_account,
- R.id.navigation_settings_providers,
+ R.id.navigation_settings_lang,
R.id.navigation_settings_general,
R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins,
- R.id.navigation_test_providers,
).contains(destination.id)
-
- val dontPush = listOf(
- R.id.navigation_home,
- R.id.navigation_search,
- R.id.navigation_results_phone,
- R.id.navigation_results_tv,
- R.id.navigation_player,
- R.id.navigation_quick_search,
- ).contains(destination.id)
-
- binding?.navHostFragment?.apply {
- val params = layoutParams as ConstraintLayout.LayoutParams
- val push =
- if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
-
- if (!this.isLtr()) {
- params.setMargins(
- params.leftMargin,
- params.topMargin,
- push,
- params.bottomMargin
- )
- } else {
- params.setMargins(
- push,
- params.topMargin,
- params.rightMargin,
- params.bottomMargin
- )
- }
-
- layoutParams = params
- }
-
val landscape = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
true
}
-
Configuration.ORIENTATION_PORTRAIT -> {
- isLayout(TV or EMULATOR)
+ false
}
-
else -> {
false
}
}
- binding?.apply {
- navRailView.isVisible = isNavVisible && landscape
- navView.isVisible = isNavVisible && !landscape
-
- /**
- * We need to make sure if we return to a sub-fragment,
- * the correct navigation item is selected so that it does not
- * highlight the wrong one in UI.
- */
- when (destination.id) {
- 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,
- R.id.navigation_chrome_subtitles,
- R.id.navigation_settings_player,
- R.id.navigation_settings_updates,
- R.id.navigation_settings_ui,
- R.id.navigation_settings_account,
- R.id.navigation_settings_providers,
- R.id.navigation_settings_general,
- R.id.navigation_settings_extensions,
- R.id.navigation_settings_plugins,
- R.id.navigation_test_providers
- ) -> {
- navRailView.menu.findItem(R.id.navigation_settings).isChecked = true
- navView.menu.findItem(R.id.navigation_settings).isChecked = true
- }
- }
- }
+ nav_view?.isVisible = isNavVisible && !landscape
+ nav_rail_view?.isVisible = isNavVisible && landscape
}
//private var mCastSession: CastSession? = null
- var mSessionManager: SessionManager? = null
+ lateinit var mSessionManager: SessionManager
private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() }
private inner class SessionManagerListenerImpl : SessionManagerListener {
@@ -646,11 +238,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
override fun onResume() {
super.onResume()
- afterPluginsLoadedEvent += ::onAllPluginsLoaded
- setActivityInstance(this)
try {
if (isCastApiAvailable()) {
- mSessionManager?.addSessionManagerListener(mSessionManagerListener)
+ //mCastSession = mSessionManager.currentCastSession
+ mSessionManager.addSessionManagerListener(mSessionManagerListener)
}
} catch (e: Exception) {
logError(e)
@@ -659,14 +250,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
override fun onPause() {
super.onPause()
-
- // Start any delayed updates
- if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
- Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
- }
try {
if (isCastApiAvailable()) {
- mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
+ mSessionManager.removeSessionManagerListener(mSessionManagerListener)
//mCastSession = null
}
} catch (e: Exception) {
@@ -674,10 +260,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- val response = CommonActivity.dispatchKeyEvent(this, event)
- if (response != null)
- return response
+
+ override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+ CommonActivity.dispatchKeyEvent(this, event)?.let {
+ return it
+ }
return super.dispatchKeyEvent(event)
}
@@ -693,32 +280,53 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
onUserLeaveHint(this)
}
- private fun showConfirmExitDialog() {
- val builder: AlertDialog.Builder = AlertDialog.Builder(this)
- 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) { _, _ -> }
+ private fun backPressed() {
+ this.window?.navigationBarColor =
+ this.colorFromAttribute(R.attr.primaryGrayBackground)
+ this.updateLocale()
+ super.onBackPressed()
+ this.updateLocale()
+ }
+
+ override fun onBackPressed() {
+ ((supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.childFragmentManager?.primaryNavigationFragment as? IOnBackPressed)?.onBackPressed()
+ ?.let { runNormal ->
+ if (runNormal) backPressed()
+ } ?: run {
+ backPressed()
}
- builder.show().setDefaultFocus()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (requestCode == VLC_REQUEST_CODE) {
+ if (resultCode == RESULT_OK && data != null) {
+ val pos: Long =
+ data.getLongExtra(
+ VLC_EXTRA_POSITION_OUT,
+ -1
+ ) //Last position in media when player exited
+ val dur: Long =
+ data.getLongExtra(
+ VLC_EXTRA_DURATION_OUT,
+ -1
+ ) //Last position in media when player exited
+ val id = getKey(VLC_LAST_ID_KEY)
+ println("SET KEY $id at $pos / $dur")
+ if (dur > 0 && pos > 0) {
+ setViewPos(id, pos, dur)
+ }
+ removeKey(VLC_LAST_ID_KEY)
+ ResultFragment.updateUI()
+ }
+ }
+ super.onActivityResult(requestCode, resultCode, data)
}
override fun onDestroy() {
- filesToDelete.forEach { path ->
- val result = File(path).deleteRecursively()
- if (result) {
- Log.d(TAG, "Deleted temporary file: $path")
- } else {
- Log.d(TAG, "Failed to delete temporary file: $path")
- }
- }
- filesToDelete = setOf()
val broadcastIntent = Intent()
broadcastIntent.action = "restart_service"
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
this.sendBroadcast(broadcastIntent)
- afterPluginsLoadedEvent -= ::onAllPluginsLoaded
super.onDestroy()
}
@@ -731,7 +339,56 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (intent == null) return
val str = intent.dataString
loadCache()
- handleAppIntentUrl(this, str, false)
+ if (str != null) {
+ if (str.startsWith("https://cs.repo")) {
+ val realUrl = "https://" + str.substringAfter("?")
+ println("Repository url: $realUrl")
+ loadRepository(realUrl)
+ } else if (str.contains(appString)) {
+ for (api in OAuth2Apis) {
+ if (str.contains("/${api.redirectUrl}")) {
+ val activity = this
+ ioSafe {
+ Log.i(TAG, "handleAppIntent $str")
+ val isSuccessful = api.handleRedirect(str)
+
+ if (isSuccessful) {
+ Log.i(TAG, "authenticated ${api.name}")
+ } else {
+ Log.i(TAG, "failed to authenticate ${api.name}")
+ }
+
+ activity.runOnUiThread {
+ try {
+ showToast(
+ activity,
+ getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
+ api.name
+ )
+ )
+ } catch (e: Exception) {
+ logError(e) // format might fail
+ }
+ }
+ }
+ }
+ }
+ } else if (URI(str).scheme == appStringRepo) {
+ val url = str.replaceFirst(appStringRepo, "https")
+ loadRepository(url)
+ } else {
+ if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
+ this.navigate(R.id.navigation_downloads)
+ } else {
+ for (api in apis) {
+ if (str.startsWith(api.mainUrl)) {
+ loadResult(str, api.name)
+ break
+ }
+ }
+ }
+ }
+ }
}
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
@@ -759,772 +416,69 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- private val pluginsLock = Mutex()
- private fun onAllPluginsLoaded(success: Boolean = false) {
- ioSafe {
- pluginsLock.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
- })
- }
- }
- }
- // it.hashCode() is not enough to make sure they are distinct
- apis =
- allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
- APIHolder.apiMap = null
- } catch (e: Exception) {
- logError(e)
- }
- }
- }
- }
- }
-
- lateinit var viewModel: ResultViewModel2
- lateinit var syncViewModel: SyncViewModel
- private var libraryViewModel: LibraryViewModel? = null
-
- /** kinda dirty, however it signals that we should use the watch status as sync or not*/
- var isLocalList: Boolean = false
- override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
-
- viewModel = ViewModelProvider(this)[ResultViewModel2::class.java]
- syncViewModel = ViewModelProvider(this)[SyncViewModel::class.java]
-
- return super.onCreateView(name, context, attrs)
- }
-
- private fun hidePreviewPopupDialog() {
- bottomPreviewPopup.dismissSafe(this)
- bottomPreviewPopup = null
- bottomPreviewBinding = null
- }
-
- private var bottomPreviewPopup: BottomSheetDialog? = null
- private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
- private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
- val ret = (bottomPreviewBinding ?: run {
- val builder =
- BottomSheetDialog(this)
- val binding: BottomResultviewPreviewBinding =
- BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false)
- bottomPreviewBinding = binding
- builder.setContentView(binding.root)
- builder.setOnDismissListener {
- bottomPreviewPopup = null
- bottomPreviewBinding = null
- viewModel.clear()
- }
- builder.setCanceledOnTouchOutside(true)
- builder.show()
- bottomPreviewPopup = builder
- binding
- })
-
- return ret
- }
-
- var binding: ActivityMainBinding? = null
-
- object TvFocus {
- data class FocusTarget(
- val width: Int,
- val height: Int,
- val x: Float,
- val y: Float,
- ) {
- companion object {
- fun lerp(a: FocusTarget, b: FocusTarget, lerp: Float): FocusTarget {
- val ilerp = 1 - lerp
- return FocusTarget(
- width = (a.width * ilerp + b.width * lerp).toInt(),
- height = (a.height * ilerp + b.height * lerp).toInt(),
- x = a.x * ilerp + b.x * lerp,
- y = a.y * ilerp + b.y * lerp
- )
- }
- }
- }
-
- var last: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
- var current: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
-
- var focusOutline: WeakReference = WeakReference(null)
- var lastFocus: WeakReference = WeakReference(null)
- private val layoutListener: View.OnLayoutChangeListener =
- View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- // shitty fix for layouts
- lastFocus.get()?.apply {
- updateFocusView(
- this, same = true
- )
- postDelayed({
- updateFocusView(
- lastFocus.get(), same = false
- )
- }, 300)
- }
- }
- private val attachListener: View.OnAttachStateChangeListener =
- object : View.OnAttachStateChangeListener {
- override fun onViewAttachedToWindow(v: View) {
- updateFocusView(v)
- }
-
- override fun onViewDetachedFromWindow(v: View) {
- // removes the focus view but not the listener as updateFocusView(null) will remove the listener
- focusOutline.get()?.isVisible = false
- }
- }
- /*private val scrollListener = object : RecyclerView.OnScrollListener() {
- override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- super.onScrolled(recyclerView, dx, dy)
- current = current.copy(x = current.x + dx, y = current.y + dy)
- setTargetPosition(current)
- }
- }*/
-
- private fun setTargetPosition(target: FocusTarget) {
- focusOutline.get()?.apply {
- layoutParams = layoutParams?.apply {
- width = target.width
- height = target.height
- }
-
- translationX = target.x
- translationY = target.y
- bringToFront()
- }
- }
-
- private var animator: ValueAnimator? = null
-
- /** if this is enabled it will keep the focus unmoving
- * during listview move */
- private const val NO_MOVE_LIST: Boolean = false
-
- /** If this is enabled then it will try to move the
- * listview focus to the left instead of center */
- private const val LEFTMOST_MOVE_LIST: Boolean = true
-
- private val reflectedScroll by lazy {
- try {
- RecyclerView::class.java.declaredMethods.firstOrNull {
- it.name == "scrollStep"
- }?.also { it.isAccessible = true }
- } catch (t: Throwable) {
- null
- }
- }
-
- @MainThread
- fun updateFocusView(newFocus: View?, same: Boolean = false) {
- val focusOutline = focusOutline.get() ?: return
- val lastView = lastFocus.get()
- val exactlyTheSame = lastView == newFocus && newFocus != null
- if (!exactlyTheSame) {
- lastView?.removeOnLayoutChangeListener(layoutListener)
- lastView?.removeOnAttachStateChangeListener(attachListener)
- (lastView?.parent as? RecyclerView)?.apply {
- removeOnLayoutChangeListener(layoutListener)
- //removeOnScrollListener(scrollListener)
- }
- }
-
- val wasGone = focusOutline.isGone
-
- val visible =
- newFocus != null && newFocus.measuredHeight > 0 && newFocus.measuredWidth > 0 && newFocus.isShown && newFocus.tag != "tv_no_focus_tag"
- focusOutline.isVisible = visible
-
- if (newFocus != null) {
- lastFocus = WeakReference(newFocus)
- val parent = newFocus.parent
- var targetDx = 0
- if (parent is RecyclerView) {
- val layoutManager = parent.layoutManager
- if (layoutManager is LinearListLayout && layoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
- val dx =
- LinearSnapHelper().calculateDistanceToFinalSnap(layoutManager, newFocus)
- ?.get(0)
-
- if (dx != null) {
- val rdx = if (LEFTMOST_MOVE_LIST) {
- // this makes the item the leftmost in ltr, instead of center
- val diff =
- ((layoutManager.width - layoutManager.paddingStart - newFocus.measuredWidth) / 2) - newFocus.marginStart
- dx + if (parent.isRtl()) {
- -diff
- } else {
- diff
- }
- } else {
- if (dx > 0) dx else 0
- }
-
- if (!NO_MOVE_LIST) {
- parent.smoothScrollBy(rdx, 0)
- } else {
- val smoothScroll = reflectedScroll
- if (smoothScroll == null) {
- parent.smoothScrollBy(rdx, 0)
- } else {
- try {
- // this is very fucked but because it is a protected method to
- // be able to compute the scroll I use reflection, scroll, then
- // scroll back, then smooth scroll and set the no move
- val out = IntArray(2)
- smoothScroll.invoke(parent, rdx, 0, out)
- val scrolledX = out[0]
- if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
- smoothScroll.invoke(parent, -rdx, 0, out)
- parent.smoothScrollBy(scrolledX, 0)
- if (NO_MOVE_LIST) targetDx = scrolledX
- }
- } catch (t: Throwable) {
- parent.smoothScrollBy(rdx, 0)
- }
- }
- }
- }
- }
- }
-
- val out = IntArray(2)
- newFocus.getLocationInWindow(out)
- val (screenX, screenY) = out
- var (x, y) = screenX.toFloat() to screenY.toFloat()
- val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY
-
- if (!newFocus.isLtr()) {
- x = x - focusOutline.rootView.width + newFocus.measuredWidth
- }
- x -= targetDx
-
- // out of bounds = 0,0
- if (screenX == 0 && screenY == 0) {
- focusOutline.isVisible = false
- }
- if (!exactlyTheSame) {
- (newFocus.parent as? RecyclerView)?.apply {
- addOnLayoutChangeListener(layoutListener)
- //addOnScrollListener(scrollListener)
- }
- newFocus.addOnLayoutChangeListener(layoutListener)
- newFocus.addOnAttachStateChangeListener(attachListener)
- }
- val start = FocusTarget(
- x = currentX,
- y = currentY,
- width = focusOutline.measuredWidth,
- height = focusOutline.measuredHeight
- )
- val end = FocusTarget(
- x = x,
- y = y,
- width = newFocus.measuredWidth,
- height = newFocus.measuredHeight
- )
-
- // if they are the same within then snap, aka scrolling
- val deltaMinX = min(end.width / 2, 60.toPx)
- val deltaMinY = min(end.height / 2, 60.toPx)
- if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMinX && (start.y - end.y).absoluteValue < deltaMinY) {
- animator?.cancel()
- last = start
- current = end
- setTargetPosition(end)
- return
- }
-
- // if running then "reuse"
- if (animator?.isRunning == true) {
- current = end
- return
- } else {
- animator?.cancel()
- }
-
-
- last = start
- current = end
-
- // if previously gone, then tp
- if (wasGone) {
- setTargetPosition(current)
- return
- }
-
- // animate between a and b
- animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
- startDelay = 0
- duration = 200
- addUpdateListener { animation ->
- val animatedValue = animation.animatedValue as Float
- val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
- setTargetPosition(target)
- }
- start()
- }
-
- // post check
- if (!same) {
- newFocus.postDelayed({
- updateFocusView(lastFocus.get(), same = true)
- }, 200)
- }
-
- /*
-
- the following is working, but somewhat bad code code
-
- if (!wasGone) {
- (focusOutline.parent as? ViewGroup)?.let {
- TransitionManager.endTransitions(it)
- TransitionManager.beginDelayedTransition(
- it,
- TransitionSet().addTransition(ChangeBounds())
- .addTransition(ChangeTransform())
- .setDuration(100)
- )
- }
- }
-
- focusOutline.layoutParams = focusOutline.layoutParams?.apply {
- width = newFocus.measuredWidth
- height = newFocus.measuredHeight
- }
- focusOutline.translationX = x.toFloat()
- focusOutline.translationY = y.toFloat()*/
- }
- }
- }
-
- 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?) {
+ context = this
app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(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 =
- settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
-
- MainAPI.settingsForProvider = settingsForProvider
-
loadThemes(this)
updateLocale()
super.onCreate(savedInstanceState)
try {
if (isCastApiAvailable()) {
- CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
+ mSessionManager = CastContext.getSharedInstance(this).sessionManager
}
- } catch (t: Throwable) {
- logError(t)
+ } catch (e: Exception) {
+ logError(e)
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
- updateTv()
- // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
- normalSafeApiCall {
- val appVer = BuildConfig.VERSION_NAME
- val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
- if (appVer != lastAppAutoBackup) {
- setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
- normalSafeApiCall {
- backup(this)
- }
- normalSafeApiCall {
- // Recompile oat on new version
- PluginManager.deleteAllOatFiles(this)
- }
- }
- }
-
- // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
- binding = try {
- if (isLayout(TV or EMULATOR)) {
- val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
- setContentView(newLocalBinding.root)
-
- if (isLayout(TV) && ANIMATED_OUTLINE) {
- TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
- newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
- TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
- }
- newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
- TvFocus.updateFocusView(newFocus)
- }
- } else {
- newLocalBinding.focusOutline.isVisible = false
- }
-
- 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_info_btt,
- R.id.home_preview_hidden_next_focus,
- R.id.home_preview_hidden_prev_focus,
- R.id.result_play_movie_button,
- R.id.result_play_series_button,
- R.id.result_resume_series_button,
- R.id.result_play_trailer_button,
- R.id.result_bookmark_Button,
- R.id.result_favorite_Button,
- R.id.result_subscribe_Button,
- R.id.result_search_Button,
- R.id.result_episodes_show_button,
- )
-
- newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
- if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
- centerView(newFocus)
- }
- }
-
- ActivityMainBinding.bind(newLocalBinding.root) // this may crash
- } else {
- val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
- setContentView(newLocalBinding.root)
- newLocalBinding
- }
- } catch (t: Throwable) {
- showToast(txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG)
- null
- }
-
- changeStatusBarState(isLayout(EMULATOR))
-
- /** Biometric stuff for users without accounts **/
- val noAccounts = settingsManager.getBoolean(
- getString(R.string.skip_startup_account_select_key),
- false
- ) || accounts.count() <= 1
-
- if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) {
- if (deviceHasPasswordPinLock(this)) {
- startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
-
- promptInfo?.let { prompt ->
- biometricPrompt?.authenticate(prompt)
- }
-
- // hide background while authenticating, Sorry moms & dads 🙏
- binding?.navHostFragment?.isInvisible = true
- }
- }
-
- // Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
- if (this.getKey(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
- main {
- if (checkGithubConnectivity()) {
- this.setKey(getString(R.string.jsdelivr_proxy_key), false)
- } else {
- this.setKey(getString(R.string.jsdelivr_proxy_key), true)
- showSnackbar(
- this@MainActivity,
- R.string.jsdelivr_enabled,
- Snackbar.LENGTH_LONG,
- R.string.revert
- ) { setKey(getString(R.string.jsdelivr_proxy_key), false) }
- }
- }
- }
-
- ioSafe { SafeFile.check(this@MainActivity) }
-
- if (PluginManager.checkSafeModeFile()) {
- normalSafeApiCall {
- showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
- }
- } else if (lastError == null) {
- ioSafe {
- DataStoreHelper.currentHomePage?.let { homeApi ->
- mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
- } ?: run {
- mainPluginsLoadedEvent.invoke(false)
- }
-
- ioSafe {
- if (settingsManager.getBoolean(
- getString(R.string.auto_update_plugins_key),
- true
- )
- ) {
- PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
- } else {
- loadAllOnlinePlugins(this@MainActivity)
- }
-
- //Automatically download not existing plugins, using mode specified.
- val autoDownloadPlugin = AutoDownloadMode.getEnum(
- settingsManager.getInt(
- getString(R.string.auto_download_plugins_key),
- 0
- )
- ) ?: AutoDownloadMode.Disable
- if (autoDownloadPlugin != AutoDownloadMode.Disable) {
- PluginManager.downloadNotExistingPluginsAndLoad(
- this@MainActivity,
- autoDownloadPlugin
- )
- }
- }
-
- ioSafe {
- PluginManager.loadAllLocalPlugins(this@MainActivity, false)
- }
- }
+ if (isTvSettings()) {
+ setContentView(R.layout.activity_main_tv)
} else {
- val builder: AlertDialog.Builder = AlertDialog.Builder(this)
- builder.setTitle(R.string.safe_mode_title)
- builder.setMessage(R.string.safe_mode_description)
- builder.apply {
- setPositiveButton(R.string.safe_mode_crash_info) { _, _ ->
- val tbBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
- tbBuilder.setTitle(R.string.safe_mode_title)
- tbBuilder.setMessage(lastError)
- tbBuilder.show()
- }
-
- setNegativeButton("Ok") { _, _ -> }
- }
- builder.show().setDefaultFocus()
+ setContentView(R.layout.activity_main)
}
+ changeStatusBarState(isEmulatorSettings())
- fun setUserData(status: Resource?) {
- if (isLocalList) return
- bottomPreviewBinding?.apply {
- when (status) {
- is Resource.Success -> {
- resultviewPreviewBookmark.isEnabled = true
- resultviewPreviewBookmark.setText(status.value.status.stringRes)
- resultviewPreviewBookmark.setIconResource(status.value.status.iconRes)
- }
-
- is Resource.Failure -> {
- resultviewPreviewBookmark.isEnabled = false
- resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24)
- resultviewPreviewBookmark.text = status.errorString
- }
-
- else -> {
- resultviewPreviewBookmark.isEnabled = false
- resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24)
- resultviewPreviewBookmark.setText(R.string.loading)
- }
- }
+ ioSafe {
+ getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
+ mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
+ } ?: run {
+ mainPluginsLoadedEvent.invoke(false)
}
- }
- fun setWatchStatus(state: WatchType?) {
- if (!isLocalList || state == null) return
-
- bottomPreviewBinding?.resultviewPreviewBookmark?.apply {
- setIconResource(state.iconRes)
- setText(state.stringRes)
+ if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) {
+ PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
+ } else {
+ PluginManager.loadAllOnlinePlugins(this@MainActivity)
}
- }
- fun setSubscribeStatus(state: Boolean?) {
- bottomPreviewBinding?.resultviewPreviewSubscribe?.apply {
- if (state != null) {
- val drawable = if (state) {
- R.drawable.ic_baseline_notifications_active_24
- } else {
- R.drawable.baseline_notifications_none_24
- }
- setImageResource(drawable)
- }
- isVisible = state != null
+ PluginManager.loadAllLocalPlugins(this@MainActivity)
- setOnClickListener {
- viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
- if (newStatus == null) return@toggleSubscriptionStatus
-
- val message = if (newStatus) {
- // Kinda icky to have this here, but it works.
- SubscriptionWorkManager.enqueuePeriodicWork(context)
- R.string.subscription_new
- } else {
- R.string.subscription_deleted
- }
-
- val name = (viewModel.page.value as? Resource.Success)?.value?.title
- ?: txt(R.string.no_data).asStringNull(context) ?: ""
- showToast(txt(message, name), Toast.LENGTH_SHORT)
+ // 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.newInstance().apply {
+ name = custom.name
+ lang = custom.lang
+ mainUrl = custom.url.trimEnd('/')
+ canBeOverridden = false
+ })
+ }
}
}
+ apis = allProviders.distinctBy { it }
+ APIHolder.apiMap = null
+ } catch (e: Exception) {
+ logError(e)
}
- }
- observe(viewModel.watchStatus, ::setWatchStatus)
- observe(syncViewModel.userData, ::setUserData)
- observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus)
-
- observeNullable(viewModel.page) { resource ->
- if (resource == null) {
- hidePreviewPopupDialog()
- return@observeNullable
- }
- when (resource) {
- is Resource.Failure -> {
- showToast(R.string.error)
- viewModel.clear()
- hidePreviewPopupDialog()
- }
-
- is Resource.Loading -> {
- showPreviewPopupDialog().apply {
- resultviewPreviewLoading.isVisible = true
- resultviewPreviewResult.isVisible = false
- resultviewPreviewLoadingShimmer.startShimmer()
- }
- }
-
- is Resource.Success -> {
- val d = resource.value
- showPreviewPopupDialog().apply {
- resultviewPreviewLoading.isVisible = false
- resultviewPreviewResult.isVisible = true
- resultviewPreviewLoadingShimmer.stopShimmer()
-
- resultviewPreviewTitle.text = d.title
-
- resultviewPreviewMetaType.setText(d.typeText)
- resultviewPreviewMetaYear.setText(d.yearText)
- resultviewPreviewMetaDuration.setText(d.durationText)
- resultviewPreviewMetaRating.setText(d.ratingText)
-
- resultviewPreviewDescription.setTextHtml(d.plotText)
- resultviewPreviewPoster.setImage(
- d.posterImage ?: d.posterBackgroundImage
- )
-
- setUserData(syncViewModel.userData.value)
- setWatchStatus(viewModel.watchStatus.value)
- setSubscribeStatus(viewModel.subscribeStatus.value)
-
- resultviewPreviewBookmark.setOnClickListener {
- //viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
- if (isLocalList) {
- val value = viewModel.watchStatus.value ?: WatchType.NONE
-
- this@MainActivity.showBottomDialog(
- WatchType.entries.map { getString(it.stringRes) }.toList(),
- value.ordinal,
- this@MainActivity.getString(R.string.action_add_to_bookmarks),
- showApply = false,
- {}) {
- viewModel.updateWatchStatus(
- WatchType.entries[it],
- this@MainActivity
- )
- }
- } else {
- val value =
- (syncViewModel.userData.value as? Resource.Success)?.value?.status
- ?: SyncWatchType.NONE
-
- this@MainActivity.showBottomDialog(
- SyncWatchType.entries.map { getString(it.stringRes) }.toList(),
- value.ordinal,
- this@MainActivity.getString(R.string.action_add_to_bookmarks),
- showApply = false,
- {}) {
- syncViewModel.setStatus(SyncWatchType.entries[it].internalId)
- syncViewModel.publishUserData()
- }
- }
- }
-
- observeNullable(viewModel.favoriteStatus) observeFavoriteStatus@{ isFavorite ->
- resultviewPreviewFavorite.isVisible = isFavorite != null
- if (isFavorite == null) return@observeFavoriteStatus
-
- val drawable = if (isFavorite) {
- R.drawable.ic_baseline_favorite_24
- } else {
- R.drawable.ic_baseline_favorite_border_24
- }
-
- resultviewPreviewFavorite.setImageResource(drawable)
- }
-
- resultviewPreviewFavorite.setOnClickListener {
- viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? ->
- if (newStatus == null) return@toggleFavoriteStatus
-
- val message = if (newStatus) {
- R.string.favorite_added
- } else {
- R.string.favorite_removed
- }
-
- val name = (viewModel.page.value as? Resource.Success)?.value?.title
- ?: txt(R.string.no_data).asStringNull(this@MainActivity) ?: ""
- showToast(txt(message, name), Toast.LENGTH_SHORT)
- }
- }
-
- if (isLayout(PHONE)) // dont want this clickable on tv layout
- resultviewPreviewDescription.setOnClickListener { view ->
- view.context?.let { ctx ->
- val builder: AlertDialog.Builder =
- AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
- builder.setMessage(d.plotText.asString(ctx).html())
- .setTitle(d.plotHeaderText.asString(ctx))
- .show()
- }
- }
-
- resultviewPreviewMoreInfo.setOnClickListener {
- viewModel.clear()
- hidePreviewPopupDialog()
- lastPopup?.let {
- loadSearchResult(it)
- }
- }
- }
- }
- }
+ afterPluginsLoadedEvent.invoke(true)
}
// ioSafe {
@@ -1542,34 +496,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
for (api in accountManagers) {
api.init()
}
+ }
- inAppAuths.amap { api ->
+ ioSafe {
+ inAppAuths.apmap { 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
- libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java]
- libraryViewModel?.currentApiName?.observe(this@MainActivity) {
- val syncAPI = libraryViewModel?.currentSyncApi
- Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}")
- val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) {
- R.drawable.library_icon
- } else {
- syncAPI?.icon ?: R.drawable.library_icon
- }
-
- binding?.apply {
- navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon)
- navView.menu.findItem(R.id.navigation_library)?.setIcon(icon)
- }
- }
- }
}
SearchResultBuilder.updateCache(this)
@@ -1577,9 +513,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
- apis = synchronized(allProviders) {
- allProviders.distinctBy { it }
- }
+ apis = allProviders.distinctBy { it }
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@@ -1589,28 +523,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
-
- navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
- // Intercept search and add a query
- updateNavBar(navDestination)
- if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
- bundle?.apply {
- this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
- }
- }
-
- 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)
/*navOptions = NavOptions.Builder()
@@ -1621,47 +533,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
.setPopExitAnim(R.anim.nav_pop_exit)
.setPopUpTo(navController.graph.startDestination, false)
.build()*/
+ nav_view?.setupWithNavController(navController)
+ val nav_rail = findViewById(R.id.nav_rail_view)
+ nav_rail?.setupWithNavController(navController)
- val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
-
- binding?.navView?.apply {
- itemRippleColor = rippleColor
- itemActiveIndicatorColor = rippleColor
- setupWithNavController(navController)
- setOnItemSelectedListener { item ->
- onNavDestinationSelected(
- item,
- navController
- )
- }
+ nav_rail?.setOnItemSelectedListener { item ->
+ onNavDestinationSelected(
+ item,
+ navController
+ )
}
-
- binding?.navRailView?.apply {
- itemRippleColor = rippleColor
- itemActiveIndicatorColor = rippleColor
- setupWithNavController(navController)
- if (isLayout(TV or EMULATOR)) {
- background?.alpha = 200
- } else {
- background?.alpha = 255
- }
-
- setOnItemSelectedListener { item ->
- onNavDestinationSelected(
- item,
- navController
- )
- }
-
- fun noFocus(view: View) {
- view.tag = view.context.getString(R.string.tv_no_focus_tag)
- (view as? ViewGroup)?.let {
- for (child in it.children) {
- noFocus(child)
- }
- }
- }
- noFocus(this)
+ nav_view?.setOnItemSelectedListener { item ->
+ onNavDestinationSelected(
+ item,
+ navController
+ )
+ }
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ updateNavBar(destination)
}
loadCache()
@@ -1684,12 +573,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
true
}*/
+ val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
+ nav_view?.itemRippleColor = rippleColor
+ nav_rail?.itemRippleColor = rippleColor
+ nav_rail?.itemActiveIndicatorColor = rippleColor
+ nav_view?.itemActiveIndicatorColor = rippleColor
if (!checkWrite()) {
requestRW()
if (checkWrite()) return
}
- //CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
+ CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
// THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION
//if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) {
@@ -1756,15 +650,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
- synchronized(allProviders) {
- for (api in allProviders) {
- providersAndroidManifestString += "\n"
- }
+ for (api in allProviders) {
+ providersAndroidManifestString += "\n"
}
+
println(providersAndroidManifestString)
}
@@ -1774,15 +667,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
runAutoUpdate()
}
- FcastManager().init(this, false)
-
APIRepository.dubStatusActive = getApiDubstatusSettings()
try {
// this ensures that no unnecessary space is taken
loadCache()
File(filesDir, "exoplayer").deleteRecursively() // old cache
- deleteFileOnExit(File(cacheDir, "exoplayer")) // current cache
+ File(cacheDir, "exoplayer").deleteOnExit() // current cache
} catch (e: Exception) {
logError(e)
}
@@ -1792,11 +683,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
migrateResumeWatching()
}
- getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage ->
- DataStoreHelper.currentHomePage = homepage
- removeKey(USER_SELECTED_HOMEPAGE_API)
- }
-
try {
if (getKey(HAS_DONE_SETUP_KEY, false) != true) {
navController.navigate(R.id.navigation_setup_language)
@@ -1812,53 +698,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
} catch (e: Exception) {
logError(e)
+ } finally {
+ setKey(HAS_DONE_SETUP_KEY, true)
}
// Used to check current focus for TV
// main {
// while (true) {
-// delay(5000)
+// delay(1000)
// println("Current focus: $currentFocus")
-// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
// }
// }
- onBackPressedDispatcher.addCallback(
- this,
- object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- window?.navigationBarColor = colorFromAttribute(R.attr.primaryGrayBackground)
- updateLocale()
-
- // 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 **/
- override fun onAuthenticationSuccess() {
- // make background (nav host fragment) visible again
- binding?.navHostFragment?.isInvisible = false
- }
-
- override fun onAuthenticationError() {
- finish()
- }
-
- suspend fun checkGithubConnectivity(): Boolean {
- return try {
- app.get(
- "https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck",
- timeout = 5
- ).text.trim() == "ok"
- } catch (t: Throwable) {
- false
- }
- }
-}
\ No newline at end of file
+}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
similarity index 72%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt
rename to app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
index 46955427..badb6631 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
@@ -1,7 +1,8 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.mvvm.logError
-import kotlinx.coroutines.*
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
/*
@@ -25,25 +26,10 @@ fun Iterable.pmap(
return ArrayList(destination)
}*/
-
-@OptIn(DelicateCoroutinesApi::class)
-suspend fun Map.amap(f: suspend (Map.Entry) -> R): List =
- with(CoroutineScope(GlobalScope.coroutineContext)) {
- map { async { f(it) } }.map { it.await() }
- }
-
fun Map.apmap(f: suspend (Map.Entry) -> R): List = runBlocking {
map { async { f(it) } }.map { it.await() }
}
-
-@OptIn(DelicateCoroutinesApi::class)
-suspend fun List.amap(f: suspend (A) -> B): List =
- with(CoroutineScope(GlobalScope.coroutineContext)) {
- map { async { f(it) } }.map { it.await() }
- }
-
-
fun List.apmap(f: suspend (A) -> B): List = runBlocking {
map { async { f(it) } }.map { it.await() }
}
@@ -52,12 +38,6 @@ fun List.apmapIndexed(f: suspend (index: Int, A) -> B): List = runB
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
}
-@OptIn(DelicateCoroutinesApi::class)
-suspend fun List.amapIndexed(f: suspend (index: Int, A) -> B): List =
- with(CoroutineScope(GlobalScope.coroutineContext)) {
- mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
- }
-
// run code in parallel
/*fun argpmap(
vararg transforms: () -> R,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
new file mode 100644
index 00000000..fe46791b
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
@@ -0,0 +1,40 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.base64Decode
+import com.lagradost.cloudstream3.utils.*
+
+class Acefile : ExtractorApi() {
+ override val name = "Acefile"
+ override val mainUrl = "https://acefile.co"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val sources = mutableListOf()
+ app.get(url).document.select("script").map { script ->
+ if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
+ val data = getAndUnpack(script.data())
+ val id = data.substringAfter("{\"id\":\"").substringBefore("\",")
+ val key = data.substringAfter("var nfck=\"").substringBefore("\";")
+ app.get("https://acefile.co/local/$id?key=$key").text.let {
+ base64Decode(
+ it.substringAfter("JSON.parse(atob(\"").substringBefore("\"))")
+ ).let { res ->
+ sources.add(
+ ExtractorLink(
+ name,
+ name,
+ res.substringAfter("\"file\":\"").substringBefore("\","),
+ "$mainUrl/",
+ Qualities.Unknown.value,
+ headers = mapOf("range" to "bytes=0-")
+ )
+ )
+ }
+ }
+ }
+ }
+ return sources
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
similarity index 94%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
index 4bed3169..cf16f200 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
@@ -7,9 +7,9 @@ import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
-open class AsianLoad : ExtractorApi() {
+class AsianLoad : ExtractorApi() {
override var name = "AsianLoad"
- override var mainUrl = "https://asianhdplay.pro"
+ override var mainUrl = "https://asianembed.io"
override val requiresReferer = true
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
@@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() {
return extractedLinksList
}
}
-}
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
index 44e700b1..cae77322 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
@@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
-open class Blogger : ExtractorApi() {
+class Blogger : ExtractorApi() {
override val name = "Blogger"
override val mainUrl = "https://www.blogger.com"
override val requiresReferer = false
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
similarity index 92%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
index 71fa7066..d4f87f4c 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
@@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-open class BullStream : ExtractorApi() {
+class BullStream : ExtractorApi() {
override val name = "BullStream"
override val mainUrl = "https://bullstream.xyz"
override val requiresReferer = false
@@ -18,7 +18,7 @@ open class BullStream : ExtractorApi() {
?: return null
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
- //println("shiv : $m3u8")
+ println("shiv : $m3u8")
return M3u8Helper.generateM3u8(
name,
m3u8,
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
similarity index 62%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
index 370dcaca..c5eaf40e 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
@@ -7,26 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay
-class D0000d : DoodLaExtractor() {
- override var mainUrl = "https://d0000d.com"
-}
-
-class D000dCom : DoodLaExtractor() {
- override var mainUrl = "https://d000d.com"
-}
-
-class DoodstreamCom : DoodLaExtractor() {
- override var mainUrl = "https://doodstream.com"
-}
-
-class Dooood : DoodLaExtractor() {
- override var mainUrl = "https://dooood.com"
-}
-
-class DoodWfExtractor : DoodLaExtractor() {
- override var mainUrl = "https://dood.wf"
-}
-
class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx"
}
@@ -54,9 +34,6 @@ class DoodWsExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.ws"
}
-class DoodYtExtractor : DoodLaExtractor() {
- override var mainUrl = "https://dood.yt"
-}
open class DoodLaExtractor : ExtractorApi() {
override var name = "DoodStream"
@@ -68,14 +45,13 @@ open class DoodLaExtractor : ExtractorApi() {
}
override suspend fun getUrl(url: String, referer: String?): List? {
- val newUrl= url.replace(mainUrl, "https://d0000d.com")
- val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/...
- val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
- val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
+ val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
+ val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
+ val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0)
return listOf(
ExtractorLink(
- this.name,
+ trueUrl,
this.name,
trueUrl,
mainUrl,
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
similarity index 66%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
index 3e38b446..4a9f2f52 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
@@ -16,7 +16,26 @@ open class Evoload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List {
- val id = url.replace("https://evoload.io/e/", "") // wanted media id
+ val lang = url.substring(0, 2)
+ val flag =
+ if (lang == "vo") {
+ " \uD83C\uDDEC\uD83C\uDDE7"
+ }
+ else if (lang == "vf"){
+ " \uD83C\uDDE8\uD83C\uDDF5"
+ } else {
+ ""
+ }
+
+ val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
+ url
+ } else {
+ url.substring(2, url.length)
+ }
+ //println(lang)
+ //println(cleaned_url)
+
+ val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
@@ -25,9 +44,9 @@ open class Evoload : ExtractorApi() {
return listOf(
ExtractorLink(
name,
- name,
+ name + flag,
link,
- url,
+ cleaned_url,
Qualities.Unknown.value,
)
)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
new file mode 100644
index 00000000..16b109be
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
@@ -0,0 +1,39 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.apmap
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
+
+class Fastream: ExtractorApi() {
+ override var mainUrl = "https://fastream.to"
+ override var name = "Fastream"
+ override val requiresReferer = false
+
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
+ val sources = mutableListOf()
+ val response = app.post("$mainUrl/dl",
+ data = mapOf(
+ Pair("op","embed"),
+ Pair("file_code",id),
+ Pair("auto","1")
+ )).document
+ response.select("script").apmap { script ->
+ if (script.data().contains("sources")) {
+ val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
+ val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
+ generateM3u8(
+ name,
+ m3u8,
+ mainUrl
+ ).forEach { link ->
+ sources.add(link)
+ }
+ }
+ }
+ return sources
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
new file mode 100644
index 00000000..5c8af1c5
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
@@ -0,0 +1,38 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+
+class Filesim : ExtractorApi() {
+ override val name = "Filesim"
+ override val mainUrl = "https://files.im"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val sources = mutableListOf()
+ with(app.get(url).document) {
+ this.select("script").map { script ->
+ if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
+ val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
+ tryParseJson>("[$data]")?.map {
+ M3u8Helper.generateM3u8(
+ name,
+ it.file,
+ "$mainUrl/",
+ ).forEach { m3uData -> sources.add(m3uData) }
+ }
+ }
+ }
+ }
+ return sources
+ }
+
+ private data class ResponseSource(
+ @JsonProperty("file") val file: String,
+ @JsonProperty("type") val type: String?,
+ @JsonProperty("label") val label: String?
+ )
+
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
similarity index 72%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
index 52c45096..e36a03d3 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
@@ -3,9 +3,9 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.M3u8Helper
-open class GMPlayer : ExtractorApi() {
+class GMPlayer : ExtractorApi() {
override val name = "GM Player"
override val mainUrl = "https://gmplayer.xyz"
override val requiresReferer = true
@@ -25,16 +25,11 @@ open class GMPlayer : ExtractorApi() {
data = mapOf("hash" to id, "r" to ref)
).parsed().videoSource ?: return null
- return listOf(
- ExtractorLink(
- this.name,
- this.name,
- m3u8,
- ref,
- Qualities.Unknown.value,
- headers = mapOf("accept" to "*/*"),
- isM3u8 = true
- )
+ return M3u8Helper.generateM3u8(
+ name,
+ m3u8,
+ ref,
+ headers = mapOf("accept" to "*/*")
)
}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
new file mode 100644
index 00000000..57435161
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
@@ -0,0 +1,36 @@
+package com.lagradost.cloudstream3.extractors
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+
+open class GuardareStream : ExtractorApi() {
+ override var name = "Guardare"
+ override var mainUrl = "https://guardare.stream"
+ override val requiresReferer = false
+
+ data class GuardareJsonData (
+ @JsonProperty("data") val data : List,
+ )
+
+ data class GuardareData (
+ @JsonProperty("file") val file : String,
+ @JsonProperty("label") val label : String,
+ @JsonProperty("type") val type : String
+ )
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text
+ val jsonvideodata = AppUtils.parseJson(response)
+ return jsonvideodata.data.map {
+ ExtractorLink(
+ it.file+".${it.type}",
+ this.name,
+ it.file+".${it.type}",
+ mainUrl,
+ it.label.filter{ it.isDigit() }.toInt(),
+ false
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
index bfd7cae5..f5dde774 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Neonime7n : Hxfile() {
override val name = "Neonime7n"
- override val mainUrl = "https://neonime.fun"
+ override val mainUrl = "https://7njctn.neonime.watch"
override val redirect = false
}
@@ -19,7 +19,7 @@ class Neonime8n : Hxfile() {
class KotakAnimeid : Hxfile() {
override val name = "KotakAnimeid"
- override val mainUrl = "https://nontonanimeid.bio"
+ override val mainUrl = "https://kotakanimeid.com"
override val requiresReferer = true
}
@@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() {
@JsonProperty("label") val label: String?
)
-}
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
new file mode 100644
index 00000000..52fc5532
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
@@ -0,0 +1,46 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.getQualityFromName
+
+class Linkbox : ExtractorApi() {
+ override val name = "Linkbox"
+ override val mainUrl = "https://www.linkbox.to"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val id = url.substringAfter("id=")
+ val sources = mutableListOf()
+
+ app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe()?.data?.rList?.map { link ->
+ sources.add(
+ ExtractorLink(
+ name,
+ name,
+ link.url,
+ url,
+ getQualityFromName(link.resolution)
+ )
+ )
+ }
+
+ return sources
+ }
+
+ data class RList(
+ @JsonProperty("url") val url: String,
+ @JsonProperty("resolution") val resolution: String?,
+ )
+
+ data class Data(
+ @JsonProperty("rList") val rList: List?,
+ )
+
+ data class Responses(
+ @JsonProperty("data") val data: Data?,
+ )
+
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mcloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mcloud.kt
new file mode 100644
index 00000000..29d98557
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mcloud.kt
@@ -0,0 +1,7 @@
+package com.lagradost.cloudstream3.extractors
+
+open class Mcloud : WcoStream() {
+ override var name = "Mcloud"
+ override var mainUrl = "https://mcloud.to"
+ override val requiresReferer = true
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
new file mode 100644
index 00000000..68a4a103
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
@@ -0,0 +1,34 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.getAndUnpack
+
+class Mp4Upload : ExtractorApi() {
+ override var name = "Mp4Upload"
+ override var mainUrl = "https://www.mp4upload.com"
+ private val srcRegex = Regex("""player\.src\("(.*?)"""")
+ override val requiresReferer = true
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ with(app.get(url)) {
+ getAndUnpack(this.text).let { unpackedText ->
+ val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
+ srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
+ return listOf(
+ ExtractorLink(
+ name,
+ name,
+ link,
+ url,
+ quality ?: Qualities.Unknown.value,
+ )
+ )
+ }
+ }
+ }
+ return null
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
similarity index 96%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
index c7f4ac76..0c0b5c68 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
@@ -7,9 +7,9 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
-open class MultiQuality : ExtractorApi() {
+class MultiQuality : ExtractorApi() {
override var name = "MultiQuality"
- override var mainUrl = "https://anihdplay.com"
+ override var mainUrl = "https://gogo-play.net"
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
private val urlRegex = Regex("""(.*?)([^/]+$)""")
@@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() {
return extractedLinksList
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt
new file mode 100644
index 00000000..70e87fbf
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt
@@ -0,0 +1,67 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+
+data class DataOptionsJson (
+ @JsonProperty("flashvars") var flashvars : Flashvars? = Flashvars(),
+)
+data class Flashvars (
+ @JsonProperty("metadata") var metadata : String? = null,
+ @JsonProperty("hlsManifestUrl") var hlsManifestUrl : String? = null, //m3u8
+)
+
+data class MetadataOkru (
+ @JsonProperty("videos") var videos: ArrayList = arrayListOf(),
+)
+
+data class Videos (
+ @JsonProperty("name") var name : String,
+ @JsonProperty("url") var url : String,
+ @JsonProperty("seekSchema") var seekSchema : Int? = null,
+ @JsonProperty("disallowed") var disallowed : Boolean? = null
+)
+
+class OkRuHttps: OkRu(){
+ override var mainUrl = "https://ok.ru"
+}
+
+open class OkRu : ExtractorApi() {
+ override var name = "Okru"
+ override var mainUrl = "http://ok.ru"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ val doc = app.get(url).document
+ val sources = ArrayList()
+ val datajson = doc.select("div[data-options]").attr("data-options")
+ if (datajson.isNotBlank()) {
+ val main = parseJson(datajson)
+ val metadatajson = parseJson(main.flashvars?.metadata!!)
+ val servers = metadatajson.videos
+ servers.forEach {
+ val quality = it.name.uppercase()
+ .replace("MOBILE","144p")
+ .replace("LOWEST","240p")
+ .replace("LOW","360p")
+ .replace("SD","480p")
+ .replace("HD","720p")
+ .replace("FULL","1080p")
+ .replace("QUAD","1440p")
+ .replace("ULTRA","4k")
+ val extractedurl = it.url.replace("\\\\u0026", "&")
+ sources.add(ExtractorLink(
+ name,
+ name = this.name,
+ extractedurl,
+ url,
+ getQualityFromName(quality),
+ isM3u8 = false
+ ))
+ }
+ }
+ return sources
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
similarity index 90%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
index 4163cd94..cc743d5e 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
@@ -1,11 +1,10 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
-import com.lagradost.cloudstream3.amap
+import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor
@@ -15,7 +14,7 @@ import org.jsoup.Jsoup
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
* If they diverge it'd be better to make them separate.
* */
-open class Pelisplus(val mainUrl: String) {
+class Pelisplus(val mainUrl: String) {
val name: String = "Vidstream"
private fun getExtractorUrl(id: String): String {
@@ -36,7 +35,7 @@ open class Pelisplus(val mainUrl: String) {
callback: (ExtractorLink) -> Unit
): Boolean {
try {
- normalApis.amap { api ->
+ normalApis.apmap { api ->
val url = api.getExtractorUrl(id)
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
}
@@ -52,8 +51,8 @@ open class Pelisplus(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P")
//a[download]
- pageDoc.select(".dowload > a")?.amap { element ->
- val href = element.attr("href") ?: return@amap
+ pageDoc.select(".dowload > a")?.apmap { element ->
+ val href = element.attr("href") ?: return@apmap
val qual = if (element.text()
.contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
@@ -67,7 +66,7 @@ open class Pelisplus(val mainUrl: String) {
href,
page.url,
getQualityFromName(qual),
- type = INFER_TYPE
+ element.attr("href").contains(".m3u8")
)
)
}
@@ -85,7 +84,7 @@ open class Pelisplus(val mainUrl: String) {
//val name = element.text()
// Matches vidstream links with extractors
- extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
+ extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
index cc34781c..849f5fc8 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
@@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
-open class Solidfiles : ExtractorApi() {
+class Solidfiles : ExtractorApi() {
override val name = "Solidfiles"
override val mainUrl = "https://www.solidfiles.com"
override val requiresReferer = false
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
similarity index 81%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
index 702501a1..6153a7c1 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
@@ -7,12 +7,10 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-open class Minoplres : ExtractorApi() {
-
- override val name = "Minoplres" // formerly SpeedoStream
+class SpeedoStream : ExtractorApi() {
+ override val name = "SpeedoStream"
+ override val mainUrl = "https://speedostream.com"
override val requiresReferer = true
- override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
- private val hostUrl = "https://minoplres.xyz"
override suspend fun getUrl(url: String, referer: String?): List {
val sources = mutableListOf()
@@ -24,7 +22,7 @@ open class Minoplres : ExtractorApi() {
M3u8Helper.generateM3u8(
name,
it.file,
- "$hostUrl/",
+ "$mainUrl/",
).forEach { m3uData -> sources.add(m3uData) }
}
}
@@ -35,4 +33,6 @@ open class Minoplres : ExtractorApi() {
private data class File(
@JsonProperty("file") val file: String,
)
-}
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
new file mode 100644
index 00000000..da3ef278
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
@@ -0,0 +1,126 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper
+
+class Ssbstream : StreamSB() {
+ override var mainUrl = "https://ssbstream.net"
+}
+
+class SBfull : StreamSB() {
+ override var mainUrl = "https://sbfull.com"
+}
+
+class StreamSB1 : StreamSB() {
+ override var mainUrl = "https://sbplay1.com"
+}
+
+class StreamSB2 : StreamSB() {
+ override var mainUrl = "https://sbplay2.com"
+}
+
+class StreamSB3 : StreamSB() {
+ override var mainUrl = "https://sbplay3.com"
+}
+
+class StreamSB4 : StreamSB() {
+ override var mainUrl = "https://cloudemb.com"
+}
+
+class StreamSB5 : StreamSB() {
+ override var mainUrl = "https://sbplay.org"
+}
+
+class StreamSB6 : StreamSB() {
+ override var mainUrl = "https://embedsb.com"
+}
+
+class StreamSB7 : StreamSB() {
+ override var mainUrl = "https://pelistop.co"
+}
+
+class StreamSB8 : StreamSB() {
+ override var mainUrl = "https://streamsb.net"
+}
+
+class StreamSB9 : StreamSB() {
+ override var mainUrl = "https://sbplay.one"
+}
+
+class StreamSB10 : StreamSB() {
+ override var mainUrl = "https://sbplay2.xyz"
+}
+
+// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
+// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
+open class StreamSB : ExtractorApi() {
+ override var name = "StreamSB"
+ override var mainUrl = "https://watchsb.com"
+ override val requiresReferer = false
+
+ private val hexArray = "0123456789ABCDEF".toCharArray()
+
+ private fun bytesToHex(bytes: ByteArray): String {
+ val hexChars = CharArray(bytes.size * 2)
+ for (j in bytes.indices) {
+ val v = bytes[j].toInt() and 0xFF
+
+ hexChars[j * 2] = hexArray[v ushr 4]
+ hexChars[j * 2 + 1] = hexArray[v and 0x0F]
+ }
+ return String(hexChars)
+ }
+
+ data class Subs (
+ @JsonProperty("file") val file: String,
+ @JsonProperty("label") val label: String,
+ )
+
+ data class StreamData (
+ @JsonProperty("file") val file: String,
+ @JsonProperty("cdn_img") val cdnImg: String,
+ @JsonProperty("hash") val hash: String,
+ @JsonProperty("subs") val subs: List?,
+ @JsonProperty("length") val length: String,
+ @JsonProperty("id") val id: String,
+ @JsonProperty("title") val title: String,
+ @JsonProperty("backup") val backup: String,
+ )
+
+ data class Main (
+ @JsonProperty("stream_data") val streamData: StreamData,
+ @JsonProperty("status_code") val statusCode: Int,
+ )
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ val regexID = Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|\\/e\\/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
+ val id = regexID.findAll(url).map {
+ it.value.replace(Regex("(embed-|\\/e\\/)"),"")
+ }.first()
+ val bytes = id.toByteArray()
+ val bytesToHex = bytesToHex(bytes)
+ val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
+ val headers = mapOf(
+ "watchsb" to "streamsb",
+ )
+ val urltext = app.get(master,
+ headers = headers,
+ allowRedirects = false
+ ).text
+ val mapped = urltext.let { parseJson(it) }
+ val testurl = app.get(mapped.streamData.file, headers = headers).text
+ // val urlmain = mapped.streamData.file.substringBefore("/hls/")
+ if (urltext.contains("m3u8") && testurl.contains("EXTM3U"))
+ return M3u8Helper.generateM3u8(
+ name,
+ mapped.streamData.file,
+ url,
+ headers = headers
+ )
+ return null
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
similarity index 68%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
index 2ee98c65..af436ff3 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
@@ -5,19 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
-class StreamTapeNet : StreamTape() {
- override var mainUrl = "https://streamtape.net"
-}
-
-class StreamTapeXyz : StreamTape() {
- override var mainUrl = "https://streamtape.xyz"
-}
-
-class ShaveTape : StreamTape(){
- override var mainUrl = "https://shavetape.cash"
-}
-
-open class StreamTape : ExtractorApi() {
+class StreamTape : ExtractorApi() {
override var name = "StreamTape"
override var mainUrl = "https://streamtape.com"
override val requiresReferer = false
@@ -28,8 +16,7 @@ open class StreamTape : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List? {
with(app.get(url)) {
linkRegex.find(this.text)?.let {
- val extractedUrl =
- "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3)}"
+ val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
return listOf(
ExtractorLink(
name,
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
index c7689c58..2765ae17 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.JsUnpacker
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URI
-open class Streamhub : ExtractorApi() {
+class Streamhub : ExtractorApi() {
override var mainUrl = "https://streamhub.to"
override var name = "Streamhub"
override val requiresReferer = false
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
similarity index 73%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
index e70cae6b..955345aa 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
@@ -11,22 +11,19 @@ data class Files(
@JsonProperty("label") val label: String? = null,
)
-open class Supervideo : ExtractorApi() {
+ open class Supervideo : ExtractorApi() {
override var name = "Supervideo"
- override var mainUrl = "https://supervideo.cc"
+ override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List? {
val extractedLinksList: MutableList = mutableListOf()
val response = app.get(url).text
val jstounpack = Regex("eval((.|\\n)*?)").find(response)?.groups?.get(1)?.value
val unpacjed = JsUnpacker(jstounpack).unpack()
- val extractedUrl =
- unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString()
- .replace("file", """"file"""").replace("label", """"label"""")
- .substringBeforeLast(",")
+ val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",")
val parsedlinks = parseJson>(extractedUrl)
parsedlinks.forEach { data ->
- if (data.label.isNullOrBlank()) { // mp4 links (with labels) are slow. Use only m3u8 link.
+ if (data.label.isNullOrBlank()){ // mp4 links (with labels) are slow. Use only m3u8 link.
M3u8Helper.generateM3u8(
name,
data.id,
@@ -37,6 +34,8 @@ open class Supervideo : ExtractorApi() {
}
}
}
+
+
return extractedLinksList
}
}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
index 13aa48c6..d721dea8 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
@@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
val jsonvideodata = parseJson(response)
return jsonvideodata.data.map {
ExtractorLink(
- this.name,
+ it.file+".${it.type}",
this.name,
it.file+".${it.type}",
mainUrl,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
new file mode 100644
index 00000000..20bd69ba
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
@@ -0,0 +1,41 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+
+class Cinestart: Tomatomatela() {
+ override var name = "Cinestart"
+ override var mainUrl = "https://cinestart.net"
+ override val details = "vr.php?v="
+}
+
+open class Tomatomatela : ExtractorApi() {
+ override var name = "Tomatomatela"
+ override var mainUrl = "https://tomatomatela.com"
+ override val requiresReferer = false
+ private data class Tomato (
+ @JsonProperty("status") val status: Int,
+ @JsonProperty("file") val file: String
+ )
+ open val details = "details.php?v="
+ override suspend fun getUrl(url: String, referer: String?): List