mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Compare commits
7 commits
master
...
Merge-with
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eac7c2b2e3 | ||
|
|
eb47f372b2 |
||
|
|
6a767df0eb |
||
|
|
58763708f7 | ||
|
|
b08608d31a | ||
|
|
830fd7ee55 | ||
|
|
c7c1f12dad |
723 changed files with 17548 additions and 50273 deletions
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
|
|
@ -80,13 +80,13 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
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.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
||||||
required: true
|
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.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -2,7 +2,7 @@ blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Request a new provider or report bug with an existing provider
|
- name: Request a new provider or report bug with an existing provider
|
||||||
url: https://github.com/recloudstream
|
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 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.
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/5Hus6fM
|
url: https://discord.gg/5Hus6fM
|
||||||
about: Join our discord for faster support on smaller issues.
|
about: Join our discord for faster support on smaller issues.
|
||||||
|
|
|
||||||
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
|
@ -27,7 +27,9 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
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.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
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
|
||||||
|
|
|
||||||
BIN
.github/downloads.jpg
vendored
Normal file
BIN
.github/downloads.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
.github/home.jpg
vendored
Normal file
BIN
.github/home.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
23
.github/locales.py
vendored
23
.github/locales.py
vendored
|
|
@ -1,8 +1,6 @@
|
||||||
import re
|
import re
|
||||||
import glob
|
import glob
|
||||||
import requests
|
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"
|
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
||||||
|
|
@ -47,23 +45,4 @@ open(SETTINGS_PATH, "w+",encoding='utf-8').write(
|
||||||
"\n" +
|
"\n" +
|
||||||
END_MARKER +
|
END_MARKER +
|
||||||
after_src
|
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'<?xml version="1.0" encoding="utf-8"?>\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}")
|
|
||||||
BIN
.github/player.jpg
vendored
Normal file
BIN
.github/player.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
.github/results.jpg
vendored
Normal file
BIN
.github/results.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
.github/search.jpg
vendored
Normal file
BIN
.github/search.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
16
.github/workflows/build_to_archive.yml
vendored
16
.github/workflows/build_to_archive.yml
vendored
|
|
@ -19,23 +19,23 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- name: Generate access token (archive)
|
- name: Generate access token (archive)
|
||||||
id: generate_archive_token
|
id: generate_archive_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '11'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
@ -56,9 +56,7 @@ jobs:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
- uses: actions/checkout@v3
|
||||||
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
token: ${{ steps.generate_archive_token.outputs.token }}
|
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||||
|
|
|
||||||
11
.github/workflows/generate_dokka.yml
vendored
11
.github/workflows/generate_dokka.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
|
@ -42,14 +42,13 @@ jobs:
|
||||||
cd $GITHUB_WORKSPACE/dokka/
|
cd $GITHUB_WORKSPACE/dokka/
|
||||||
rm -rf "./-cloudstream"
|
rm -rf "./-cloudstream"
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 11
|
||||||
distribution: 'adopt'
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v2
|
||||||
|
|
||||||
- name: Generate Dokka
|
- name: Generate Dokka
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
8
.github/workflows/issue_action.yml
vendored
8
.github/workflows/issue_action.yml
vendored
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
comment-body: '${index}. ${similarity} #${number}'
|
comment-body: '${index}. ${similarity} #${number}'
|
||||||
- name: Label if possible duplicate
|
- name: Label if possible duplicate
|
||||||
if: steps.similarity.outputs.similar-issues-found =='true'
|
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate_token.outputs.token }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
labels: ["possible duplicate"]
|
labels: ["possible duplicate"]
|
||||||
})
|
})
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Automatically close issues that dont follow the issue template
|
- name: Automatically close issues that dont follow the issue template
|
||||||
uses: lucasbento/auto-close-issues@v1.0.2
|
uses: lucasbento/auto-close-issues@v1.0.2
|
||||||
with:
|
with:
|
||||||
|
|
@ -68,7 +68,7 @@ jobs:
|
||||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||||
- name: Label if mentions provider
|
- name: Label if mentions provider
|
||||||
if: steps.provider_check.outputs.name != 'none'
|
if: steps.provider_check.outputs.name != 'none'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate_token.outputs.token }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|
|
||||||
15
.github/workflows/prerelease.yml
vendored
15
.github/workflows/prerelease.yml
vendored
|
|
@ -18,16 +18,16 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '11'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
@ -43,14 +43,11 @@ jobs:
|
||||||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: |
|
run: |
|
||||||
./gradlew assemblePrerelease build androidSourcesJar
|
./gradlew assemblePrerelease makeJar androidSourcesJar
|
||||||
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
|
|
||||||
env:
|
env:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIGNING_STORE_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
|
- name: Create pre-release
|
||||||
uses: "marvinpinto/action-automatic-releases@latest"
|
uses: "marvinpinto/action-automatic-releases@latest"
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
10
.github/workflows/pull_request.yml
vendored
10
.github/workflows/pull_request.yml
vendored
|
|
@ -6,18 +6,18 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 11
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '11'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: ./gradlew assemblePrereleaseDebug
|
run: ./gradlew assemblePrereleaseDebug
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: pull-request-build
|
name: pull-request-build
|
||||||
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||||
|
|
|
||||||
13
.github/workflows/update_locales.yml
vendored
13
.github/workflows/update_locales.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: Fix locale issues
|
name: Update locale lists
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
@ -9,7 +9,7 @@ on:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: "locale"
|
group: "locale-list"
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -18,17 +18,14 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream"
|
repository: "recloudstream/cloudstream"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
pip3 install lxml
|
|
||||||
- name: Edit files
|
- name: Edit files
|
||||||
run: |
|
run: |
|
||||||
python3 .github/locales.py
|
python3 .github/locales.py
|
||||||
|
|
@ -38,5 +35,5 @@ jobs:
|
||||||
git config --local user.name "recloudstream[bot]"
|
git config --local user.name "recloudstream[bot]"
|
||||||
git add .
|
git add .
|
||||||
# "echo" returns true so the build succeeds, even if no changed files
|
# "echo" returns true so the build succeeds, even if no changed files
|
||||||
git commit -m 'chore(locales): fix locale issues' || echo
|
git commit -m 'update list of locales' || echo
|
||||||
git push
|
git push
|
||||||
|
|
|
||||||
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="17" />
|
<bytecodeTargetLevel target="11" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
7
.idea/gradle.xml
generated
7
.idea/gradle.xml
generated
|
|
@ -4,16 +4,17 @@
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
<option name="delegatedBuild" value="true" />
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="gradleJvm" value="11" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/library" />
|
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveExternalAnnotations" value="false" />
|
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -9,11 +9,15 @@
|
||||||
+ **AdFree**, No ads whatsoever
|
+ **AdFree**, No ads whatsoever
|
||||||
+ No tracking/analytics
|
+ No tracking/analytics
|
||||||
+ Bookmarks
|
+ Bookmarks
|
||||||
+ Phone and TV support
|
+ Download and stream movies, tv-shows and anime
|
||||||
+ Chromecast
|
+ Chromecast
|
||||||
+ Extension system for personal customization
|
|
||||||
|
### Screenshots:
|
||||||
|
|
||||||
|
<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/results.jpg" height="400"/>
|
||||||
|
<img src="./.github/player.jpg" height="200"/>
|
||||||
|
|
||||||
### Supported languages:
|
### Supported languages:
|
||||||
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||||
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -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})
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
import com.android.build.gradle.api.BaseVariantOutput
|
||||||
import org.jetbrains.dokka.gradle.DokkaTask
|
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.io.ByteArrayOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("com.google.devtools.ksp")
|
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
id("kotlin-android-extensions")
|
||||||
id("org.jetbrains.dokka")
|
id("org.jetbrains.dokka")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,7 +19,7 @@ fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||||
workingDir = projectDir
|
workingDir = projectDir
|
||||||
commandLine = this@execute.split(Regex("\\s"))
|
commandLine = this@execute.split(Regex("\\s"))
|
||||||
standardOutput = baot
|
standardOutput = baot
|
||||||
}.exitValue == 0)
|
}.exitValue == 0)
|
||||||
String(baot.toByteArray()).trim()
|
String(baot.toByteArray()).trim()
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
@ -29,21 +28,9 @@ android {
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests.isReturnDefaultValues = true
|
unitTests.isReturnDefaultValues = true
|
||||||
}
|
}
|
||||||
|
|
||||||
viewBinding {
|
|
||||||
enable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
/* disable this for now
|
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
path("CMakeLists.txt")
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (prereleaseStoreFile != null) {
|
create("prerelease") {
|
||||||
create("prerelease") {
|
if (prereleaseStoreFile != null) {
|
||||||
storeFile = file(prereleaseStoreFile)
|
storeFile = file(prereleaseStoreFile)
|
||||||
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||||
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||||
|
|
@ -52,44 +39,33 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 34
|
compileSdk = 33
|
||||||
buildToolsVersion = "34.0.0"
|
buildToolsVersion = "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.lagradost.cloudstream3"
|
applicationId = "com.lagradost.cloudstream3"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 33 /* Android 14 is Fu*ked
|
targetSdk = 33
|
||||||
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
|
||||||
versionCode = 64
|
versionCode = 57
|
||||||
versionName = "4.4.0"
|
versionName = "4.0.0"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
|
|
||||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
|
||||||
resValue("bool", "is_prerelease", "false")
|
resValue("bool", "is_prerelease", "false")
|
||||||
|
|
||||||
// Reads local.properties
|
buildConfigField(
|
||||||
val localProperties = gradleLocalProperties(rootDir)
|
"String",
|
||||||
|
"BUILDDATE",
|
||||||
|
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||||
|
)
|
||||||
|
|
||||||
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"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
ksp {
|
kapt {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
includeCompileClasspath = true
|
||||||
arg("exportSchema", "true")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,21 +74,14 @@ android {
|
||||||
isDebuggable = false
|
isDebuggable = false
|
||||||
isMinifyEnabled = false
|
isMinifyEnabled = false
|
||||||
isShrinkResources = false
|
isShrinkResources = false
|
||||||
proguardFiles(
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
isDebuggable = true
|
isDebuggable = true
|
||||||
applicationIdSuffix = ".debug"
|
applicationIdSuffix = ".debug"
|
||||||
proguardFiles(
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions.add("state")
|
flavorDimensions.add("state")
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("stable") {
|
create("stable") {
|
||||||
|
|
@ -124,31 +93,25 @@ android {
|
||||||
resValue("bool", "is_prerelease", "true")
|
resValue("bool", "is_prerelease", "true")
|
||||||
buildConfigField("boolean", "BETA", "true")
|
buildConfigField("boolean", "BETA", "true")
|
||||||
applicationIdSuffix = ".prerelease"
|
applicationIdSuffix = ".prerelease"
|
||||||
if (signingConfigs.names.contains("prerelease")) {
|
signingConfig = signingConfigs.getByName("prerelease")
|
||||||
signingConfig = signingConfigs.getByName("prerelease")
|
|
||||||
} else {
|
|
||||||
logger.warn("No prerelease signing config!")
|
|
||||||
}
|
|
||||||
versionNameSuffix = "-PRE"
|
versionNameSuffix = "-PRE"
|
||||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
||||||
|
}
|
||||||
lint {
|
lint {
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace = "com.lagradost.cloudstream3"
|
namespace = "com.lagradost.cloudstream3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,132 +120,122 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Testing
|
implementation("com.google.android.mediahome:video:1.0.0")
|
||||||
testImplementation("junit:junit:4.13.2")
|
implementation("androidx.test.ext:junit-ktx:1.1.3")
|
||||||
testImplementation("org.json:json:20240303")
|
testImplementation("org.json:json:20180813")
|
||||||
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.8.0")
|
||||||
implementation("androidx.core:core-ktx:1.13.1")
|
implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
|
||||||
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
|
// dont change this to 1.6.0 it looks ugly af
|
||||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
implementation("com.google.android.material:material:1.5.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.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.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
// Glide Module
|
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
||||||
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
|
// Exoplayer
|
||||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
|
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
|
||||||
implementation("com.google.guava:guava:33.2.1-android")
|
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
|
||||||
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
|
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
|
||||||
|
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
|
||||||
|
|
||||||
// Media 3 (ExoPlayer)
|
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
||||||
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
|
// Bug reports
|
||||||
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
implementation("ch.acra:acra-core:5.8.4")
|
||||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
implementation("ch.acra:acra-toast:5.8.4")
|
||||||
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)
|
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
||||||
implementation("ch.acra:acra-core:5.11.3")
|
//either for java sources:
|
||||||
implementation("ch.acra:acra-toast:5.11.3")
|
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
|
||||||
|
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||||
|
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
||||||
|
implementation("org.mozilla:rhino:1.7.13")
|
||||||
|
|
||||||
|
// 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.4.2")
|
||||||
|
// To fix SSL fuckery on android 9
|
||||||
|
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||||
|
// 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")
|
||||||
|
|
||||||
// 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("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
|
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||||
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
|
||||||
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
|
// slow af yt
|
||||||
implementation("androidx.work:work-runtime:2.9.0")
|
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
|
||||||
|
|
||||||
implementation(project(":library") {
|
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
|
||||||
// There does not seem to be a good way of getting the android flavor.
|
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
|
||||||
val isDebug = gradle.startParameter.taskRequests.any { task ->
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||||
task.args.any { arg ->
|
|
||||||
arg.contains("debug", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.extra.set("isDebug", isDebug)
|
// Library/extensions searching with Levenshtein distance
|
||||||
})
|
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||||
|
|
||||||
|
// color pallette for images -> colors
|
||||||
|
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Jar>("androidSourcesJar") {
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
archiveClassifier.set("sources")
|
archiveClassifier.set("sources")
|
||||||
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copyJar") {
|
// this is used by the gradlew plugin
|
||||||
from(
|
tasks.register("makeJar", Copy::class) {
|
||||||
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
|
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||||
"../library/build/libs"
|
into("build")
|
||||||
)
|
include("classes.jar")
|
||||||
into("build/app-classes")
|
dependsOn("build")
|
||||||
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<Jar>("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<KotlinCompile> {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<DokkaTask>().configureEach {
|
tasks.withType<DokkaTask>().configureEach {
|
||||||
|
|
@ -295,7 +248,6 @@ tasks.withType<DokkaTask>().configureEach {
|
||||||
|
|
||||||
// URL showing where the source code can be accessed through the web browser
|
// 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"))
|
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
|
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||||
remoteLineSuffix.set("#L")
|
remoteLineSuffix.set("#L")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,155 @@
|
||||||
package com.lagradost.cloudstream3
|
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.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.viewbinding.ViewBinding
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
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.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.TestingUtils
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
* 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)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ExampleInstrumentedTest {
|
class ExampleInstrumentedTest {
|
||||||
private fun getAllProviders(): Array<MainAPI> {
|
//@Test
|
||||||
println("Providers: ${APIHolder.allProviders.size}")
|
//fun useAppContext() {
|
||||||
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
|
// // Context of the app under test.
|
||||||
|
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||||
|
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
|
||||||
|
//}
|
||||||
|
|
||||||
|
private fun getAllProviders(): List<MainAPI> {
|
||||||
|
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<SearchResponse>? = 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
|
@Test
|
||||||
|
|
@ -60,78 +158,7 @@ class ExampleInstrumentedTest {
|
||||||
println("Done providersExist")
|
println("Done providersExist")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws
|
|
||||||
private inline fun <reified T : ViewBinding> 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
|
@Test
|
||||||
@Throws
|
|
||||||
fun layoutTest() {
|
|
||||||
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
|
||||||
scenario.onActivity { activity: MainActivity ->
|
|
||||||
// FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same
|
|
||||||
//testAllLayouts<FragmentHomeHeadBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
|
|
||||||
//testAllLayouts<FragmentHomeHeadTvBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
|
|
||||||
|
|
||||||
// main cant be tested
|
|
||||||
// testAllLayouts<ActivityMainTvBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
|
||||||
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
|
||||||
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv)
|
|
||||||
|
|
||||||
testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
|
|
||||||
testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
|
|
||||||
|
|
||||||
// testAllLayouts<FragmentResultBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
|
|
||||||
// testAllLayouts<FragmentResultTvBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
|
|
||||||
|
|
||||||
testAllLayouts<PlayerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
|
||||||
testAllLayouts<PlayerCustomLayoutTvBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
|
||||||
testAllLayouts<TrailerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
|
||||||
|
|
||||||
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
|
||||||
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
|
||||||
|
|
||||||
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
|
||||||
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
|
||||||
|
|
||||||
testAllLayouts<FragmentHomeBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
|
|
||||||
testAllLayouts<FragmentHomeTvBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
|
|
||||||
|
|
||||||
testAllLayouts<FragmentSearchBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
|
|
||||||
testAllLayouts<FragmentSearchTvBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
|
|
||||||
|
|
||||||
testAllLayouts<HomeResultGridBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
|
|
||||||
//testAllLayouts<HomeResultGridExpandedBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
|
|
||||||
|
|
||||||
testAllLayouts<SearchResultGridExpandedBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
|
|
||||||
testAllLayouts<SearchResultGridBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
|
|
||||||
|
|
||||||
|
|
||||||
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
|
||||||
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
|
||||||
|
|
||||||
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
|
||||||
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
|
||||||
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
|
||||||
|
|
||||||
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
|
||||||
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(AssertionError::class)
|
|
||||||
fun providerCorrectData() {
|
fun providerCorrectData() {
|
||||||
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
||||||
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
||||||
|
|
@ -153,20 +180,68 @@ class ExampleInstrumentedTest {
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getAllProviders().toList().amap { api ->
|
getAllProviders().amap { api ->
|
||||||
TestingUtils.testHomepage(api, TestingUtils.Logger())
|
if (api.hasMainPage) {
|
||||||
|
try {
|
||||||
|
val f = api.mainPage.first()
|
||||||
|
val homepage =
|
||||||
|
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
|
||||||
|
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")
|
println("Done providerCorrectHomepage")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Test
|
||||||
|
// fun testSingleProvider() {
|
||||||
|
// testSingleProviderApi(ThenosProvider())
|
||||||
|
// }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testAllProvidersCorrect() {
|
fun providerCorrect() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
TestingUtils.getDeferredProviderTests(
|
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
||||||
this,
|
val providers = getAllProviders()
|
||||||
getAllProviders(),
|
providers.amap { 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API -->
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
||||||
|
|
@ -14,14 +14,8 @@
|
||||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
||||||
|
|
||||||
<!-- Required for getting arbitrary Aniyomi packages -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
|
||||||
|
|
||||||
|
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
||||||
<!-- Fixes android tv fuckery -->
|
<!-- Fixes android tv fuckery -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
|
|
@ -41,11 +35,9 @@
|
||||||
<application
|
<application
|
||||||
android:name=".AcraApplication"
|
android:name=".AcraApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:appCategory="video"
|
android:appCategory="video"
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
|
|
@ -53,7 +45,7 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="o">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
|
@ -69,9 +61,7 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:screenOrientation="userLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true">
|
||||||
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
|
||||||
android:launchMode="singleTask">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
@ -97,20 +87,16 @@
|
||||||
-->
|
-->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
<intent-filter android:exported="true">
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<intent-filter>
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
|
|
||||||
<data android:scheme="cloudstreamplayer" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
@ -165,21 +151,6 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.account.AccountSelectActivity"
|
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter android:exported="true">
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.EasterEggMonke"
|
android:name=".ui.EasterEggMonke"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
@ -187,14 +158,13 @@
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter android:exported="false">
|
<intent-filter android:exported="true">
|
||||||
<action android:name="restart_service" />
|
<action android:name="restart_service" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:name=".services.VideoDownloadService"
|
android:name=".services.VideoDownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
@ -204,7 +174,6 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:name=".utils.PackageInstallerService"
|
android:name=".utils.PackageInstallerService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#include <jni.h>
|
|
||||||
#include <csignal>
|
|
||||||
#include <android/log.h>
|
|
||||||
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
|
|
@ -8,14 +8,12 @@ import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
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.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
|
|
@ -34,26 +32,27 @@ import org.acra.sender.ReportSenderFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
|
import java.lang.Exception
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
class CustomReportSender : ReportSender {
|
class CustomReportSender : ReportSender {
|
||||||
// Sends all your crashes to google forms
|
// Sends all your crashes to google forms
|
||||||
override fun send(context: Context, errorContent: CrashReportData) {
|
override fun send(context: Context, errorContent: CrashReportData) {
|
||||||
println("Sending report")
|
println("Sending report")
|
||||||
val url =
|
val url =
|
||||||
"https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
|
"https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse"
|
||||||
val data = mapOf(
|
val data = mapOf(
|
||||||
"entry.1993829403" to errorContent.toJSON()
|
"entry.753293084" to errorContent.toJSON()
|
||||||
)
|
)
|
||||||
|
|
||||||
thread { // to not run it on main thread
|
thread { // to not run it on main thread
|
||||||
runBlocking {
|
runBlocking {
|
||||||
suspendSafeApiCall {
|
suspendSafeApiCall {
|
||||||
app.post(url, data = data)
|
val post = app.post(url, data = data)
|
||||||
//println("Report response: $post")
|
println("Report response: $post")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +65,7 @@ class CustomReportSender : ReportSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AutoService(ReportSenderFactory::class)
|
||||||
class CustomSenderFactory : ReportSenderFactory {
|
class CustomSenderFactory : ReportSenderFactory {
|
||||||
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
||||||
return CustomReportSender()
|
return CustomReportSender()
|
||||||
|
|
@ -82,8 +82,14 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
ACRA.errorReporter.handleException(error)
|
ACRA.errorReporter.handleException(error)
|
||||||
try {
|
try {
|
||||||
PrintStream(errorFile).use { ps ->
|
PrintStream(errorFile).use { ps ->
|
||||||
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
|
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
||||||
ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
|
ps.println(
|
||||||
|
String.format(
|
||||||
|
"Fatal exception on thread %s (%d)",
|
||||||
|
thread.name,
|
||||||
|
thread.id
|
||||||
|
)
|
||||||
|
)
|
||||||
error.printStackTrace(ps)
|
error.printStackTrace(ps)
|
||||||
}
|
}
|
||||||
} catch (ignored: FileNotFoundException) {
|
} catch (ignored: FileNotFoundException) {
|
||||||
|
|
@ -98,16 +104,12 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
}
|
}
|
||||||
|
|
||||||
class AcraApplication : Application() {
|
class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
}.also {
|
})
|
||||||
exceptionHandler = it
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(it)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
|
|
@ -119,10 +121,10 @@ class AcraApplication : Application() {
|
||||||
buildConfigClass = BuildConfig::class.java
|
buildConfigClass = BuildConfig::class.java
|
||||||
reportFormat = StringFormat.JSON
|
reportFormat = StringFormat.JSON
|
||||||
|
|
||||||
reportContent = listOf(
|
reportContent = arrayOf(
|
||||||
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
||||||
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
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
|
// removed this due to bug when starting the app, moved it to when it actually crashes
|
||||||
|
|
@ -135,8 +137,6 @@ class AcraApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var exceptionHandler: ExceptionHandler? = null
|
|
||||||
|
|
||||||
/** Use to get activity from Context */
|
/** Use to get activity from Context */
|
||||||
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
||||||
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
||||||
|
|
@ -146,17 +146,8 @@ class AcraApplication : Application() {
|
||||||
get() = _context?.get()
|
get() = _context?.get()
|
||||||
private set(value) {
|
private set(value) {
|
||||||
_context = WeakReference(value)
|
_context = WeakReference(value)
|
||||||
setContext(WeakReference(value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
|
||||||
return context?.getKey(path, valueType)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T : Any> setKeyClass(path: String, value: T) {
|
|
||||||
context?.setKey(path, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeKeys(folder: String): Int? {
|
fun removeKeys(folder: String): Int? {
|
||||||
return context?.removeKeys(folder)
|
return context?.removeKeys(folder)
|
||||||
}
|
}
|
||||||
|
|
@ -208,9 +199,10 @@ class AcraApplication : Application() {
|
||||||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||||
openBrowser(
|
openBrowser(
|
||||||
url,
|
url,
|
||||||
isLayout(TV or EMULATOR),
|
isTvSettings(),
|
||||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -5,16 +5,11 @@ import android.app.Activity
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Gravity
|
import android.view.*
|
||||||
import android.view.KeyEvent
|
import android.widget.TextView
|
||||||
import android.view.View
|
|
||||||
import android.view.View.NO_ID
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -23,21 +18,15 @@ import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.children
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.gms.cast.framework.CastSession
|
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.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
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.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
|
|
@ -45,50 +34,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import java.lang.ref.WeakReference
|
import java.util.*
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
enum class FocusDirection {
|
|
||||||
Start,
|
|
||||||
End,
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
object CommonActivity {
|
object CommonActivity {
|
||||||
|
|
||||||
private var _activity: WeakReference<Activity>? = null
|
|
||||||
var activity
|
|
||||||
get() = _activity?.get()
|
|
||||||
private set(value) {
|
|
||||||
_activity = WeakReference(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun setActivityInstance(newActivity: Activity?) {
|
|
||||||
activity = newActivity
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun Activity?.getCastSession(): CastSession? {
|
fun Activity?.getCastSession(): CastSession? {
|
||||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
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 canEnterPipMode: Boolean = false
|
||||||
var canShowPipMode: Boolean = false
|
var canShowPipMode: Boolean = false
|
||||||
|
|
@ -100,32 +53,9 @@ object CommonActivity {
|
||||||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||||
|
|
||||||
private var currentToast: Toast? = null
|
|
||||||
|
|
||||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
var currentToast: Toast? = null
|
||||||
val act = activity ?: return
|
|
||||||
act.runOnUiThread {
|
|
||||||
showToast(act, act.getString(message), duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
fun showToast(act: Activity?, text: UiText, duration: Int) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
text.asStringNull(act)?.let {
|
text.asStringNull(act)?.let {
|
||||||
|
|
@ -156,19 +86,25 @@ object CommonActivity {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val binding = ToastBinding.inflate(act.layoutInflater)
|
val inflater =
|
||||||
binding.text.text = message.trim()
|
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
|
val layout: View = inflater.inflate(
|
||||||
|
R.layout.toast,
|
||||||
|
act.findViewById<View>(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)
|
val toast = Toast(act)
|
||||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
|
||||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
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.
|
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||||
currentToast = toast
|
toast.view = layout
|
||||||
|
//https://github.com/PureWriter/ToastCompat
|
||||||
toast.show()
|
toast.show()
|
||||||
|
currentToast = toast
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
@ -202,25 +138,22 @@ object CommonActivity {
|
||||||
setLocale(this, localeCode)
|
setLocale(this, localeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(act: Activity) {
|
fun init(act: ComponentActivity?) {
|
||||||
setActivityInstance(act)
|
if (act == null) return
|
||||||
|
|
||||||
val componentActivity = activity as? ComponentActivity ?: return
|
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
//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
|
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||||
canShowPipMode =
|
canShowPipMode =
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
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
|
act.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.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||||
|
|
||||||
componentActivity.updateLocale()
|
act.updateLocale()
|
||||||
componentActivity.updateTv()
|
act.updateTv()
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||||
|
|
||||||
for (resumeApp in resumeApps) {
|
for (resumeApp in resumeApps) {
|
||||||
resumeApp.launcher =
|
resumeApp.launcher =
|
||||||
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val resultCode = result.resultCode
|
val resultCode = result.resultCode
|
||||||
val data = result.data
|
val data = result.data
|
||||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||||
|
|
@ -237,11 +170,11 @@ object CommonActivity {
|
||||||
// Ask for notification permissions on Android 13
|
// Ask for notification permissions on Android 13
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
componentActivity,
|
act,
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
val requestPermissionLauncher = act.registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted: Boolean ->
|
) { isGranted: Boolean ->
|
||||||
Log.d(TAG, "Notification permission: $isGranted")
|
Log.d(TAG, "Notification permission: $isGranted")
|
||||||
|
|
@ -277,57 +210,30 @@ 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?) {
|
fun loadThemes(act: Activity?) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
||||||
|
|
||||||
val currentTheme =
|
val currentTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
||||||
"System" -> mapSystemTheme(act)
|
|
||||||
"Black" -> R.style.AppTheme
|
"Black" -> R.style.AppTheme
|
||||||
"Light" -> R.style.LightMode
|
"Light" -> R.style.LightMode
|
||||||
"Amoled" -> R.style.AmoledMode
|
"Amoled" -> R.style.AmoledMode
|
||||||
"AmoledLight" -> R.style.AmoledModeLight
|
"AmoledLight" -> R.style.AmoledModeLight
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.MonetMode else R.style.AppTheme
|
R.style.MonetMode else R.style.AppTheme
|
||||||
|
|
||||||
else -> R.style.AppTheme
|
else -> R.style.AppTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentOverlayTheme =
|
val currentOverlayTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||||
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
|
||||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||||
"Orange" -> R.style.OverlayPrimaryColorOrange
|
|
||||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||||
"White" -> R.style.OverlayPrimaryColorWhite
|
"White" -> R.style.OverlayPrimaryColorWhite
|
||||||
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
|
||||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||||
|
|
@ -336,13 +242,10 @@ object CommonActivity {
|
||||||
"Banana" -> R.style.OverlayPrimaryColorBanana
|
"Banana" -> R.style.OverlayPrimaryColorBanana
|
||||||
"Party" -> R.style.OverlayPrimaryColorParty
|
"Party" -> R.style.OverlayPrimaryColorParty
|
||||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||||
"Lavender" -> R.style.OverlayPrimaryColorLavender
|
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
else -> R.style.OverlayPrimaryColorNormal
|
else -> R.style.OverlayPrimaryColorNormal
|
||||||
}
|
}
|
||||||
act.theme.applyStyle(currentTheme, true)
|
act.theme.applyStyle(currentTheme, true)
|
||||||
|
|
@ -354,179 +257,101 @@ object CommonActivity {
|
||||||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
) // 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
|
private fun getNextFocus(
|
||||||
until the correct one is found */
|
act: Activity?,
|
||||||
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<View?>(id)?.let { return it }
|
|
||||||
currentLook = (currentLook.parent as? View) ?: break
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
/*var currentLook: View = view
|
|
||||||
while (true) {
|
|
||||||
val tmpNext = currentLook.findViewById<View?>(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<View?>(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?,
|
|
||||||
view: View?,
|
view: View?,
|
||||||
direction: FocusDirection,
|
direction: FocusDirection,
|
||||||
depth: Int = 0
|
depth: Int = 0
|
||||||
): View? {
|
): Int? {
|
||||||
// if input is invalid let android decide + depth test to not crash if loop is found
|
if (view == null || depth >= 10 || act == null) {
|
||||||
if (view == null || depth >= 10 || root == null) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextId = when (direction) {
|
val nextId = when (direction) {
|
||||||
FocusDirection.Start -> {
|
FocusDirection.Left -> {
|
||||||
if (view.isRtl())
|
view.nextFocusLeftId
|
||||||
view.nextFocusRightId
|
|
||||||
else
|
|
||||||
view.nextFocusLeftId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusDirection.Up -> {
|
FocusDirection.Up -> {
|
||||||
view.nextFocusUpId
|
view.nextFocusUpId
|
||||||
}
|
}
|
||||||
|
FocusDirection.Right -> {
|
||||||
FocusDirection.End -> {
|
view.nextFocusRightId
|
||||||
if (view.isRtl())
|
|
||||||
view.nextFocusLeftId
|
|
||||||
else
|
|
||||||
view.nextFocusRightId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FocusDirection.Down -> {
|
FocusDirection.Down -> {
|
||||||
view.nextFocusDownId
|
view.nextFocusDownId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextId == NO_ID) {
|
return if (nextId != -1) {
|
||||||
// if not specified then use forward id
|
val next = act.findViewById<View?>(nextId)
|
||||||
nextId = view.nextFocusForwardId
|
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
|
||||||
// if view is still not found to next focus then return and let android decide
|
|
||||||
if (nextId == NO_ID)
|
if (next?.isShown == false) {
|
||||||
return null
|
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?) {
|
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
|
// 149 keycode_numpad 5
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
PlayerEventType.SeekForward
|
PlayerEventType.SeekForward
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
PlayerEventType.SeekBack
|
PlayerEventType.SeekBack
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
||||||
PlayerEventType.NextEpisode
|
PlayerEventType.NextEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
||||||
PlayerEventType.PrevEpisode
|
PlayerEventType.PrevEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||||
PlayerEventType.Pause
|
PlayerEventType.Pause
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||||
PlayerEventType.Play
|
PlayerEventType.Play
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||||
PlayerEventType.Lock
|
PlayerEventType.Lock
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
||||||
PlayerEventType.ToggleHide
|
PlayerEventType.ToggleHide
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
|
||||||
PlayerEventType.ToggleMute
|
PlayerEventType.ToggleMute
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||||
PlayerEventType.ShowMirrors
|
PlayerEventType.ShowMirrors
|
||||||
}
|
}
|
||||||
|
|
@ -534,27 +359,21 @@ object CommonActivity {
|
||||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||||
PlayerEventType.SearchSubtitlesOnline
|
PlayerEventType.SearchSubtitlesOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||||
PlayerEventType.ShowSpeed
|
PlayerEventType.ShowSpeed
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||||
PlayerEventType.Resize
|
PlayerEventType.Resize
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
|
||||||
PlayerEventType.SkipOp
|
PlayerEventType.SkipOp
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
|
||||||
PlayerEventType.SkipCurrentChapter
|
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
|
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
|
PlayerEventType.PlayPauseToggle
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}?.let { playerEvent ->
|
}?.let { playerEvent ->
|
||||||
playerEventListener?.invoke(playerEvent)
|
playerEventListener?.invoke(playerEvent)
|
||||||
|
|
@ -567,64 +386,64 @@ object CommonActivity {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** overrides focus and custom key events */
|
|
||||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||||
if (act == null) return null
|
if (act == null) return null
|
||||||
val currentFocus = act.currentFocus
|
|
||||||
|
|
||||||
event?.keyCode?.let { keyCode ->
|
event?.keyCode?.let { keyCode ->
|
||||||
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
|
when (event.action) {
|
||||||
val nextView = when (keyCode) {
|
KeyEvent.ACTION_DOWN -> {
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
if (act.currentFocus != null) {
|
||||||
act,
|
val next = when (keyCode) {
|
||||||
currentFocus,
|
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||||
FocusDirection.Start
|
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(
|
else -> null
|
||||||
act,
|
}
|
||||||
currentFocus,
|
|
||||||
FocusDirection.End
|
|
||||||
)
|
|
||||||
|
|
||||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
if (next != null && next != -1) {
|
||||||
act,
|
val nextView = act.findViewById<View?>(next)
|
||||||
currentFocus,
|
if (nextView != null) {
|
||||||
FocusDirection.Up
|
nextView.requestFocus()
|
||||||
)
|
keyEventListener?.invoke(Pair(event, true))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
when (keyCode) {
|
||||||
act,
|
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||||
currentFocus,
|
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
|
||||||
FocusDirection.Down
|
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||||
)
|
}
|
||||||
|
}
|
||||||
else -> null
|
}
|
||||||
|
}
|
||||||
|
//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) {
|
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
import org.schabi.newpipe.extractor.downloader.Request
|
import org.schabi.newpipe.extractor.downloader.Request
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
|
|
@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
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 {
|
override fun execute(request: Request): Response {
|
||||||
val httpMethod: String = request.httpMethod()
|
val httpMethod: String = request.httpMethod()
|
||||||
val url: String = request.url()
|
val url: String = request.url()
|
||||||
|
|
@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
val dataToSend: ByteArray? = request.dataToSend()
|
val dataToSend: ByteArray? = request.dataToSend()
|
||||||
var requestBody: RequestBody? = null
|
var requestBody: RequestBody? = null
|
||||||
if (dataToSend != null) {
|
if (dataToSend != null) {
|
||||||
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
|
requestBody = RequestBody.create(null, dataToSend)
|
||||||
}
|
}
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody).url(url)
|
||||||
|
|
@ -51,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val USER_AGENT =
|
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
|
private var instance: DownloaderTestImpl? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.api.Log
|
import android.util.Log
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.base64Decode
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
open 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<ExtractorLink> {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
||||||
|
|
||||||
open class AsianLoad : ExtractorApi() {
|
open class AsianLoad : ExtractorApi() {
|
||||||
override var name = "AsianLoad"
|
override var name = "AsianLoad"
|
||||||
override var mainUrl = "https://asianhdplay.pro"
|
override var mainUrl = "https://asianembed.io"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
||||||
|
|
@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() {
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
open class ByteShare : ExtractorApi() {
|
open class ByteShare : ExtractorApi() {
|
||||||
override val name = "ByteShare"
|
override val name = "ByteShare"
|
||||||
override val mainUrl = "https://byteshare.to"
|
override val mainUrl = "https://byteshare.net"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
|
@ -20,4 +20,4 @@ open class ByteShare : ExtractorApi() {
|
||||||
)
|
)
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
import android.util.Log
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
|
||||||
open class Cda: ExtractorApi() {
|
open class Cda: ExtractorApi() {
|
||||||
|
|
@ -6,19 +6,13 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class Geodailymotion : Dailymotion() {
|
|
||||||
override val name = "GeoDailymotion"
|
|
||||||
override val mainUrl = "https://geo.dailymotion.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Dailymotion : ExtractorApi() {
|
open class Dailymotion : ExtractorApi() {
|
||||||
override val mainUrl = "https://www.dailymotion.com"
|
override val mainUrl = "https://www.dailymotion.com"
|
||||||
override val name = "Dailymotion"
|
override val name = "Dailymotion"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
private val baseUrl = "https://www.dailymotion.com"
|
|
||||||
|
|
||||||
@Suppress("RegExpSimplifiable")
|
@Suppress("RegExpSimplifiable")
|
||||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||||
|
|
@ -32,68 +26,68 @@ open class Dailymotion : ExtractorApi() {
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val embedUrl = getEmbedUrl(url) ?: return
|
val embedUrl = getEmbedUrl(url) ?: return
|
||||||
val req = app.get(embedUrl)
|
val doc = app.get(embedUrl).document
|
||||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||||
val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return
|
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return
|
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
||||||
val id = getVideoId(embedUrl) ?: return
|
val id = getVideoId(embedUrl) ?: return
|
||||||
val dmV1st = config.dmInternalData.v1st
|
val dmV1st = config.dmInternalData.v1st
|
||||||
val dmTs = config.dmInternalData.ts
|
val dmTs = config.dmInternalData.ts
|
||||||
val embedder = config.context.embedder
|
val metaDataUrl =
|
||||||
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
val cookies = mapOf(
|
||||||
|
"v1st" to dmV1st,
|
||||||
|
"dmvk" to config.context.dmvk,
|
||||||
|
"ts" to dmTs.toString()
|
||||||
|
)
|
||||||
|
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
||||||
.parsedSafe<MetaData>() ?: return
|
.parsedSafe<MetaData>() ?: return
|
||||||
metaData.qualities.forEach { (_, video) ->
|
metaData.qualities.forEach { (key, video) ->
|
||||||
video.forEach {
|
video.forEach {
|
||||||
getStream(it.url, this.name, callback)
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
"$name $key",
|
||||||
|
it.url,
|
||||||
|
"",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String? {
|
private fun getEmbedUrl(url: String): String? {
|
||||||
if (url.contains("/embed/") || url.contains("/video/")) {
|
if (url.contains("/embed/")) {
|
||||||
return url
|
return url
|
||||||
|
}
|
||||||
|
val vid = getVideoId(url) ?: return null
|
||||||
|
return "$mainUrl/embed/video/$vid"
|
||||||
}
|
}
|
||||||
if (url.contains("geo.dailymotion.com")) {
|
|
||||||
val videoId = url.substringAfter("video=")
|
|
||||||
return "$baseUrl/embed/video/$videoId"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getVideoId(url: String): String? {
|
private fun getVideoId(url: String): String? {
|
||||||
val path = URL(url).path
|
val path = URL(url).path
|
||||||
val id = path.substringAfter("/video/")
|
val id = path.substringAfter("video/")
|
||||||
if (id.matches(videoIdRegex)) {
|
if (id.matches(videoIdRegex)) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getStream(
|
|
||||||
streamLink: String,
|
|
||||||
name: String,
|
|
||||||
callback: (ExtractorLink) -> Unit
|
|
||||||
) {
|
|
||||||
return generateM3u8(
|
|
||||||
name,
|
|
||||||
streamLink,
|
|
||||||
"",
|
|
||||||
).forEach(callback)
|
|
||||||
}
|
|
||||||
data class Config(
|
data class Config(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
val dmInternalData: InternalData
|
val dmInternalData: InternalData
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InternalData(
|
data class InternalData(
|
||||||
val ts: Long,
|
val ts: Int,
|
||||||
val v1st: String
|
val v1st: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Context(
|
data class Context(
|
||||||
@JsonProperty("access_token") val accessToken: String?,
|
@JsonProperty("access_token") val accessToken: String?,
|
||||||
val embedder: String?,
|
val dmvk: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MetaData(
|
data class MetaData(
|
||||||
|
|
@ -7,22 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import kotlinx.coroutines.delay
|
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() {
|
class DoodWfExtractor : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dood.wf"
|
override var mainUrl = "https://dood.wf"
|
||||||
}
|
}
|
||||||
|
|
@ -54,9 +38,6 @@ class DoodWsExtractor : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dood.ws"
|
override var mainUrl = "https://dood.ws"
|
||||||
}
|
}
|
||||||
|
|
||||||
class DoodYtExtractor : DoodLaExtractor() {
|
|
||||||
override var mainUrl = "https://dood.yt"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class DoodLaExtractor : ExtractorApi() {
|
open class DoodLaExtractor : ExtractorApi() {
|
||||||
override var name = "DoodStream"
|
override var name = "DoodStream"
|
||||||
|
|
@ -68,14 +49,13 @@ open class DoodLaExtractor : ExtractorApi() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val newUrl= url.replace(mainUrl, "https://d0000d.com")
|
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
|
||||||
val response0 = app.get(newUrl).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 md5 ="https://d0000d.com"+(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 trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
|
|
||||||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
trueUrl,
|
||||||
this.name,
|
this.name,
|
||||||
trueUrl,
|
trueUrl,
|
||||||
mainUrl,
|
mainUrl,
|
||||||
|
|
@ -16,7 +16,26 @@ open class Evoload : ExtractorApi() {
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
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 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 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)
|
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
|
||||||
|
|
@ -25,9 +44,9 @@ open class Evoload : ExtractorApi() {
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
name,
|
name + flag,
|
||||||
link,
|
link,
|
||||||
url,
|
cleaned_url,
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class FileMoon : Filesim() {
|
||||||
|
override val mainUrl = "https://filemoon.to"
|
||||||
|
override val name = "FileMoon"
|
||||||
|
}
|
||||||
|
|
||||||
|
open 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?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
with(app.get(url).document) {
|
||||||
|
this.select("script").forEach { script ->
|
||||||
|
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||||
|
val data = getAndUnpack(script.data())
|
||||||
|
val foundData = Regex("""sources:\[(.*?)]""").find(data)?.groupValues?.get(1) ?: return@forEach
|
||||||
|
val fixedData = foundData.replace("file:", """"file":""")
|
||||||
|
|
||||||
|
parseJson<List<ResponseSource>>("[$fixedData]").forEach {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
it.file,
|
||||||
|
"$mainUrl/",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
URI(it.file).path.endsWith(".m3u8")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ResponseSource(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("type") val type: String?,
|
||||||
|
@JsonProperty("label") val label: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,10 +2,14 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
|
import java.security.DigestException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
class DatabaseGdrive2 : Gdriveplayer() {
|
class DatabaseGdrive2 : Gdriveplayer() {
|
||||||
override var mainUrl = "https://databasegdriveplayer.co"
|
override var mainUrl = "https://databasegdriveplayer.co"
|
||||||
|
|
@ -61,6 +65,78 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
?.data()?.let { getAndUnpack(it) }
|
?.data()?.let { getAndUnpack(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.decodeHex(): ByteArray {
|
||||||
|
check(length % 2 == 0) { "Must have an even length" }
|
||||||
|
return chunked(2)
|
||||||
|
.map { it.toInt(16).toByte() }
|
||||||
|
.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/41434590/8166854
|
||||||
|
private fun GenerateKeyAndIv(
|
||||||
|
password: ByteArray,
|
||||||
|
salt: ByteArray,
|
||||||
|
hashAlgorithm: String = "MD5",
|
||||||
|
keyLength: Int = 32,
|
||||||
|
ivLength: Int = 16,
|
||||||
|
iterations: Int = 1
|
||||||
|
): List<ByteArray>? {
|
||||||
|
|
||||||
|
val md = MessageDigest.getInstance(hashAlgorithm)
|
||||||
|
val digestLength = md.digestLength
|
||||||
|
val targetKeySize = keyLength + ivLength
|
||||||
|
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
|
||||||
|
val generatedData = ByteArray(requiredLength)
|
||||||
|
var generatedLength = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
md.reset()
|
||||||
|
|
||||||
|
while (generatedLength < targetKeySize) {
|
||||||
|
if (generatedLength > 0)
|
||||||
|
md.update(
|
||||||
|
generatedData,
|
||||||
|
generatedLength - digestLength,
|
||||||
|
digestLength
|
||||||
|
)
|
||||||
|
|
||||||
|
md.update(password)
|
||||||
|
md.update(salt, 0, 8)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
|
||||||
|
for (i in 1 until iterations) {
|
||||||
|
md.update(generatedData, generatedLength, digestLength)
|
||||||
|
md.digest(generatedData, generatedLength, digestLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedLength += digestLength
|
||||||
|
}
|
||||||
|
return listOf(
|
||||||
|
generatedData.copyOfRange(0, keyLength),
|
||||||
|
generatedData.copyOfRange(keyLength, targetKeySize)
|
||||||
|
)
|
||||||
|
} catch (e: DigestException) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cryptoAESHandler(
|
||||||
|
data: AesData,
|
||||||
|
pass: ByteArray,
|
||||||
|
encrypt: Boolean = true
|
||||||
|
): String? {
|
||||||
|
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
|
||||||
|
return if (!encrypt) {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
String(cipher.doFinal(base64DecodeArray(data.ct)))
|
||||||
|
} else {
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
|
||||||
|
base64Encode(cipher.doFinal(data.ct.toByteArray()))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun Regex.first(str: String): String? {
|
private fun Regex.first(str: String): String? {
|
||||||
return find(str)?.groupValues?.getOrNull(1)
|
return find(str)?.groupValues?.getOrNull(1)
|
||||||
}
|
}
|
||||||
|
|
@ -78,14 +154,14 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
val document = app.get(url).document
|
val document = app.get(url).document
|
||||||
|
|
||||||
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
||||||
val data = Regex("data='(\\S+?)'").first(eval) ?: return
|
val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
|
||||||
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||||
?.split(Regex("\\D+"))
|
?.split(Regex("\\D+"))
|
||||||
?.joinToString("") {
|
?.joinToString("") {
|
||||||
Char(it.toInt()).toString()
|
Char(it.toInt()).toString()
|
||||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||||
?: throw ErrorLoadingException("can't find password")
|
?: throw ErrorLoadingException("can't find password")
|
||||||
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
|
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||||
|
|
||||||
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||||
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
||||||
|
|
@ -118,6 +194,12 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class AesData(
|
||||||
|
@JsonProperty("ct") val ct: String,
|
||||||
|
@JsonProperty("iv") val iv: String,
|
||||||
|
@JsonProperty("s") val s: String
|
||||||
|
)
|
||||||
|
|
||||||
data class Tracks(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("kind") val kind: String,
|
@JsonProperty("kind") val kind: String,
|
||||||
|
|
@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
|
||||||
jsonVideoData.data.forEach {
|
jsonVideoData.data.forEach {
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
it.file + ".${it.type}",
|
||||||
this.name,
|
this.name,
|
||||||
it.file + ".${it.type}",
|
it.file + ".${it.type}",
|
||||||
mainUrl,
|
mainUrl,
|
||||||
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
class Neonime7n : Hxfile() {
|
class Neonime7n : Hxfile() {
|
||||||
override val name = "Neonime7n"
|
override val name = "Neonime7n"
|
||||||
override val mainUrl = "https://neonime.fun"
|
override val mainUrl = "https://7njctn.neonime.watch"
|
||||||
override val redirect = false
|
override val redirect = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ class Neonime8n : Hxfile() {
|
||||||
|
|
||||||
class KotakAnimeid : Hxfile() {
|
class KotakAnimeid : Hxfile() {
|
||||||
override val name = "KotakAnimeid"
|
override val name = "KotakAnimeid"
|
||||||
override val mainUrl = "https://nontonanimeid.bio"
|
override val mainUrl = "https://kotakanimeid.com"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() {
|
||||||
@JsonProperty("label") val label: String?
|
@JsonProperty("label") val label: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
open 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?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2)
|
||||||
|
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
link.url,
|
||||||
|
url,
|
||||||
|
getQualityFromName(link.resolution)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RList(
|
||||||
|
@JsonProperty("url") val url: String,
|
||||||
|
@JsonProperty("resolution") val resolution: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
@JsonProperty("rList") val rList: List<RList>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Responses(
|
||||||
|
@JsonProperty("data") val data: Data?,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class MoviehabNet : Moviehab() {
|
class MoviehabNet : Moviehab() {
|
||||||
override var mainUrl = "https://play.moviehab.asia"
|
override var mainUrl = "https://play.moviehab.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
open class Moviehab : ExtractorApi() {
|
open class Moviehab : ExtractorApi() {
|
||||||
|
|
@ -41,4 +41,4 @@ open class Moviehab : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
open 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<ExtractorLink>? {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import java.net.URI
|
||||||
|
|
||||||
open class MultiQuality : ExtractorApi() {
|
open class MultiQuality : ExtractorApi() {
|
||||||
override var name = "MultiQuality"
|
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 sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
||||||
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
||||||
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
||||||
|
|
@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() {
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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<Videos> = 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<ExtractorLink>? {
|
||||||
|
val doc = app.get(url).document
|
||||||
|
val sources = ArrayList<ExtractorLink>()
|
||||||
|
val datajson = doc.select("div[data-options]").attr("data-options")
|
||||||
|
if (datajson.isNotBlank()) {
|
||||||
|
val main = parseJson<DataOptionsJson>(datajson)
|
||||||
|
val metadatajson = parseJson<MetadataOkru>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
|
@ -67,7 +66,7 @@ open class Pelisplus(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
type = INFER_TYPE
|
element.attr("href").contains(".m3u8")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.api.Log
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
@ -7,12 +7,14 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
open class Minoplres : ExtractorApi() {
|
class SpeedoStream1 : SpeedoStream() {
|
||||||
|
override val mainUrl = "https://speedostream.nl"
|
||||||
|
}
|
||||||
|
|
||||||
override val name = "Minoplres" // formerly SpeedoStream
|
open class SpeedoStream : ExtractorApi() {
|
||||||
|
override val name = "SpeedoStream"
|
||||||
|
override val mainUrl = "https://speedostream.com"
|
||||||
override val requiresReferer = true
|
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<ExtractorLink> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
|
@ -24,7 +26,7 @@ open class Minoplres : ExtractorApi() {
|
||||||
M3u8Helper.generateM3u8(
|
M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
it.file,
|
it.file,
|
||||||
"$hostUrl/",
|
"$mainUrl/",
|
||||||
).forEach { m3uData -> sources.add(m3uData) }
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,4 +37,6 @@ open class Minoplres : ExtractorApi() {
|
||||||
private data class File(
|
private data class File(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -6,51 +6,6 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class Sblona : StreamSB() {
|
|
||||||
override var name = "Sblona"
|
|
||||||
override var mainUrl = "https://sblona.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Lvturbo : StreamSB() {
|
|
||||||
override var name = "Lvturbo"
|
|
||||||
override var mainUrl = "https://lvturbo.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sbrapid : StreamSB() {
|
|
||||||
override var name = "Sbrapid"
|
|
||||||
override var mainUrl = "https://sbrapid.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sbface : StreamSB() {
|
|
||||||
override var name = "Sbface"
|
|
||||||
override var mainUrl = "https://sbface.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sbsonic : StreamSB() {
|
|
||||||
override var name = "Sbsonic"
|
|
||||||
override var mainUrl = "https://sbsonic.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Vidgomunimesb : StreamSB() {
|
|
||||||
override var mainUrl = "https://vidgomunimesb.xyz"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sbasian : StreamSB() {
|
|
||||||
override var mainUrl = "https://sbasian.pro"
|
|
||||||
override var name = "Sbasian"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sbnet : StreamSB() {
|
|
||||||
override var name = "Sbnet"
|
|
||||||
override var mainUrl = "https://sbnet.one"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Keephealth : StreamSB() {
|
|
||||||
override var name = "Keephealth"
|
|
||||||
override var mainUrl = "https://keephealth.info"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Sbspeed : StreamSB() {
|
class Sbspeed : StreamSB() {
|
||||||
override var name = "Sbspeed"
|
override var name = "Sbspeed"
|
||||||
|
|
@ -122,70 +77,24 @@ class StreamSB10 : StreamSB() {
|
||||||
override var mainUrl = "https://sbplay2.xyz"
|
override var mainUrl = "https://sbplay2.xyz"
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamSB11 : StreamSB() {
|
// 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
|
||||||
override var mainUrl = "https://sbbrisk.com"
|
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
|
||||||
}
|
|
||||||
|
|
||||||
class Sblongvu : StreamSB() {
|
|
||||||
override var mainUrl = "https://sblongvu.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class StreamSB : ExtractorApi() {
|
open class StreamSB : ExtractorApi() {
|
||||||
override var name = "StreamSB"
|
override var name = "StreamSB"
|
||||||
override var mainUrl = "https://watchsb.com"
|
override var mainUrl = "https://watchsb.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
||||||
|
|
||||||
override suspend fun getUrl(
|
private val hexArray = "0123456789ABCDEF".toCharArray()
|
||||||
url: String,
|
|
||||||
referer: String?,
|
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
|
||||||
callback: (ExtractorLink) -> Unit
|
|
||||||
) {
|
|
||||||
val regexID =
|
|
||||||
Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)")
|
|
||||||
val id = regexID.findAll(url).map {
|
|
||||||
it.value.replace(Regex("(embed-|/e/)"), "")
|
|
||||||
}.first()
|
|
||||||
val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}"
|
|
||||||
val headers = mapOf(
|
|
||||||
"watchsb" to "sbstream",
|
|
||||||
)
|
|
||||||
val mapped = app.get(
|
|
||||||
master.lowercase(),
|
|
||||||
headers = headers,
|
|
||||||
referer = url,
|
|
||||||
).parsedSafe<Main>()
|
|
||||||
M3u8Helper.generateM3u8(
|
|
||||||
name,
|
|
||||||
mapped?.streamData?.file ?: return,
|
|
||||||
url,
|
|
||||||
headers = headers
|
|
||||||
).forEach(callback)
|
|
||||||
|
|
||||||
mapped.streamData.subs?.map {sub ->
|
private fun bytesToHex(bytes: ByteArray): String {
|
||||||
subtitleCallback.invoke(
|
val hexChars = CharArray(bytes.size * 2)
|
||||||
SubtitleFile(
|
for (j in bytes.indices) {
|
||||||
sub.label.toString(),
|
val v = bytes[j].toInt() and 0xFF
|
||||||
sub.file ?: return@map null,
|
|
||||||
)
|
hexChars[j * 2] = hexArray[v ushr 4]
|
||||||
)
|
hexChars[j * 2 + 1] = hexArray[v and 0x0F]
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun encodeId(id: String): String {
|
|
||||||
val code = "${createHashTable()}||$id||${createHashTable()}||streamsb"
|
|
||||||
return code.toCharArray().joinToString("") { char ->
|
|
||||||
char.code.toString(16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createHashTable(): String {
|
|
||||||
return buildString {
|
|
||||||
repeat(12) {
|
|
||||||
append(alphabet[Random.nextInt(alphabet.length)])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return String(hexChars)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Subs (
|
data class Subs (
|
||||||
|
|
@ -209,4 +118,42 @@ open class StreamSB : ExtractorApi() {
|
||||||
@JsonProperty("status_code") val statusCode: Int,
|
@JsonProperty("status_code") val statusCode: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
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 master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
||||||
|
val master = "$mainUrl/sources50/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||||
|
val headers = mapOf(
|
||||||
|
"watchsb" to "sbstream",
|
||||||
|
)
|
||||||
|
val mapped = app.get(
|
||||||
|
master.lowercase(),
|
||||||
|
headers = headers,
|
||||||
|
referer = url,
|
||||||
|
).parsedSafe<Main>()
|
||||||
|
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
mapped?.streamData?.file ?: return,
|
||||||
|
url,
|
||||||
|
headers = headers
|
||||||
|
).forEach(callback)
|
||||||
|
|
||||||
|
mapped.streamData.subs?.map {sub ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
sub.label.toString(),
|
||||||
|
sub.file ?: return@map null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,10 +9,6 @@ class StreamTapeNet : StreamTape() {
|
||||||
override var mainUrl = "https://streamtape.net"
|
override var mainUrl = "https://streamtape.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamTapeXyz : StreamTape() {
|
|
||||||
override var mainUrl = "https://streamtape.xyz"
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShaveTape : StreamTape(){
|
class ShaveTape : StreamTape(){
|
||||||
override var mainUrl = "https://shavetape.cash"
|
override var mainUrl = "https://shavetape.cash"
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ data class Files(
|
||||||
|
|
||||||
open class Supervideo : ExtractorApi() {
|
open class Supervideo : ExtractorApi() {
|
||||||
override var name = "Supervideo"
|
override var name = "Supervideo"
|
||||||
override var mainUrl = "https://supervideo.cc"
|
override var mainUrl = "https://supervideo.tv"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
|
@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
|
||||||
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
val jsonvideodata = parseJson<TantifilmJsonData>(response)
|
||||||
return jsonvideodata.data.map {
|
return jsonvideodata.data.map {
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
it.file+".${it.type}",
|
||||||
this.name,
|
this.name,
|
||||||
it.file+".${it.type}",
|
it.file+".${it.type}",
|
||||||
mainUrl,
|
mainUrl,
|
||||||
|
|
@ -7,10 +7,6 @@ class Uqload1 : Uqload() {
|
||||||
override var mainUrl = "https://uqload.com"
|
override var mainUrl = "https://uqload.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
class Uqload2 : Uqload() {
|
|
||||||
override var mainUrl = "https://uqload.co"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Uqload : ExtractorApi() {
|
open class Uqload : ExtractorApi() {
|
||||||
override val name: String = "Uqload"
|
override val name: String = "Uqload"
|
||||||
override val mainUrl: String = "https://www.uqload.com"
|
override val mainUrl: String = "https://www.uqload.com"
|
||||||
|
|
@ -19,14 +15,30 @@ open class Uqload : ExtractorApi() {
|
||||||
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
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)
|
||||||
|
}
|
||||||
|
with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
|
||||||
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
name,
|
name + flag,
|
||||||
link,
|
link,
|
||||||
url,
|
cleaned_url,
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.amap
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
class VidSrcExtractor2 : VidSrcExtractor() {
|
||||||
|
override val mainUrl = "https://vidsrc.me/embed"
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val newUrl = url.lowercase().replace(mainUrl, super.mainUrl)
|
||||||
|
super.getUrl(newUrl, referer, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class VidSrcExtractor : ExtractorApi() {
|
||||||
|
override val name = "VidSrc"
|
||||||
|
private val absoluteUrl = "https://v2.vidsrc.me"
|
||||||
|
override val mainUrl = "$absoluteUrl/embed"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Infinite function to validate the vidSrc pass */
|
||||||
|
suspend fun validatePass(url: String) {
|
||||||
|
val uri = URI(url)
|
||||||
|
val host = uri.host
|
||||||
|
|
||||||
|
// Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/
|
||||||
|
val referer = host.split(".").let {
|
||||||
|
val size = it.size
|
||||||
|
"https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
app.get(url, referer = referer)
|
||||||
|
delay(60_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val iframedoc = app.get(url).document
|
||||||
|
|
||||||
|
val serverslist =
|
||||||
|
iframedoc.select("div#sources.button_content div#content div#list div").map {
|
||||||
|
val datahash = it.attr("data-hash")
|
||||||
|
if (datahash.isNotBlank()) {
|
||||||
|
val links = try {
|
||||||
|
app.get(
|
||||||
|
"$absoluteUrl/srcrcp/$datahash",
|
||||||
|
referer = "https://rcp.vidsrc.me/"
|
||||||
|
).url
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
links
|
||||||
|
} else ""
|
||||||
|
}
|
||||||
|
|
||||||
|
serverslist.amap { server ->
|
||||||
|
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
|
||||||
|
if (linkfixed.contains("/prorcp")) {
|
||||||
|
val srcresponse = app.get(server, referer = absoluteUrl).text
|
||||||
|
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
||||||
|
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
|
||||||
|
val passRegex = Regex("""['"](.*set_pass[^"']*)""")
|
||||||
|
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
|
||||||
|
Regex("""^//"""), "https://"
|
||||||
|
)
|
||||||
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
srcm3u8,
|
||||||
|
"https://vidsrc.stream/",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
extractorData = pass,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
loadExtractor(linkfixed, url, subtitleCallback, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -25,13 +25,9 @@ open class Vidmoly : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val headers = mapOf(
|
|
||||||
"User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
|
|
||||||
"Sec-Fetch-Dest" to "iframe"
|
|
||||||
)
|
|
||||||
val script = app.get(
|
val script = app.get(
|
||||||
url,
|
url,
|
||||||
headers = headers,
|
|
||||||
referer = referer,
|
referer = referer,
|
||||||
).document.select("script")
|
).document.select("script")
|
||||||
.find { it.data().contains("sources:") }?.data()
|
.find { it.data().contains("sources:") }?.data()
|
||||||
|
|
@ -70,4 +66,4 @@ open class Vidmoly : ExtractorApi() {
|
||||||
@JsonProperty("kind") val kind: String? = null,
|
@JsonProperty("kind") val kind: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.argamap
|
import com.lagradost.cloudstream3.argamap
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
|
@ -71,7 +70,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
type = INFER_TYPE
|
element.attr("href").contains(".m3u8")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
|
open class Voe : ExtractorApi() {
|
||||||
|
override val name = "Voe"
|
||||||
|
override val mainUrl = "https://voe.sx"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url, referer = referer).document
|
||||||
|
val link = res.select("script").find { it.data().contains("const sources") }?.data()
|
||||||
|
?.substringAfter("\"hls\": \"")?.substringBefore("\",")
|
||||||
|
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
link ?: return,
|
||||||
|
"$mainUrl/",
|
||||||
|
headers = mapOf("Origin" to "$mainUrl/")
|
||||||
|
).forEach(callback)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
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.getQualityFromName
|
||||||
|
|
||||||
|
open class VoeExtractor : ExtractorApi() {
|
||||||
|
override val name: String = "Voe"
|
||||||
|
override val mainUrl: String = "https://voe.sx"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
private data class ResponseLinks(
|
||||||
|
@JsonProperty("hls") val hls: String?,
|
||||||
|
@JsonProperty("mp4") val mp4: String?,
|
||||||
|
@JsonProperty("video_height") val label: Int?
|
||||||
|
//val type: String // Mp4
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val html = app.get(url).text
|
||||||
|
if (html.isNotBlank()) {
|
||||||
|
val src = html.substringAfter("const sources =").substringBefore(";")
|
||||||
|
// Remove last comma, it is not proper json otherwise
|
||||||
|
.replace("0,", "0")
|
||||||
|
// Make json use the proper quotes
|
||||||
|
.replace("'", "\"")
|
||||||
|
|
||||||
|
//Log.i(this.name, "Result => (src) ${src}")
|
||||||
|
parseJson<ResponseLinks?>(src)?.let { voeLink ->
|
||||||
|
//Log.i(this.name, "Result => (voeLink) ${voeLink}")
|
||||||
|
|
||||||
|
// Always defaults to the hls link, but returns the mp4 if null
|
||||||
|
val linkUrl = voeLink.hls ?: voeLink.mp4
|
||||||
|
val linkLabel = voeLink.label?.toString() ?: ""
|
||||||
|
if (!linkUrl.isNullOrEmpty()) {
|
||||||
|
return listOf(
|
||||||
|
ExtractorLink(
|
||||||
|
name = this.name,
|
||||||
|
source = this.name,
|
||||||
|
url = linkUrl,
|
||||||
|
quality = getQualityFromName(linkLabel),
|
||||||
|
referer = url,
|
||||||
|
isM3u8 = voeLink.hls != null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
class Vidstreamz : WcoStream() {
|
class Vidstreamz : WcoStream() {
|
||||||
|
|
@ -127,7 +126,8 @@ open class WcoStream : ExtractorApi() {
|
||||||
|
|
||||||
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
|
||||||
return response.parsed<Response>().data.media.sources.map {
|
return response.parsed<Response>().data.media.sources.map {
|
||||||
ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE)
|
ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,16 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
class StreamM4u : XStreamCdn() {
|
|
||||||
override val name: String = "StreamM4u"
|
|
||||||
override val mainUrl: String = "https://streamm4u.club"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Fembed9hd : XStreamCdn() {
|
|
||||||
override var mainUrl = "https://fembed9hd.com"
|
|
||||||
override var name = "Fembed9hd"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Cdnplayer: XStreamCdn() {
|
class Cdnplayer: XStreamCdn() {
|
||||||
override val name: String = "Cdnplayer"
|
override val name: String = "Cdnplayer"
|
||||||
override val mainUrl: String = "https://cdnplayer.online"
|
override val mainUrl: String = "https://cdnplayer.online"
|
||||||
|
|
@ -70,18 +70,19 @@ open class YoutubeExtractor : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ytVideos[url]?.mapNotNull {
|
ytVideos[url]?.mapNotNull {
|
||||||
if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null
|
if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
|
||||||
|
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
it.content ?: return@mapNotNull null,
|
it.url ?: return@mapNotNull null,
|
||||||
"",
|
"",
|
||||||
it.height
|
it.height
|
||||||
)
|
)
|
||||||
}?.forEach(callback)
|
}?.forEach(callback)
|
||||||
ytVideosSubtitles[url]?.mapNotNull {
|
ytVideosSubtitles[url]?.mapNotNull {
|
||||||
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.content ?: return@mapNotNull null)
|
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.url ?: return@mapNotNull null)
|
||||||
}?.forEach(subtitleCallback)
|
}?.forEach(subtitleCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors.helper
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
import com.lagradost.api.Log
|
import android.util.Log
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.amap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.lagradost.cloudstream3.extractors.helper
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
|
||||||
class WcoHelper {
|
class WcoHelper {
|
||||||
|
|
@ -28,7 +30,9 @@ class WcoHelper {
|
||||||
private suspend fun getKeys() {
|
private suspend fun getKeys() {
|
||||||
keys = keys
|
keys = keys
|
||||||
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
|
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
|
||||||
.parsedSafe<ExternalKeys>()
|
.parsedSafe<ExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
|
||||||
|
BACKUP_KEY_DATA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getWcoKey(): ExternalKeys? {
|
suspend fun getWcoKey(): ExternalKeys? {
|
||||||
|
|
@ -39,7 +43,9 @@ class WcoHelper {
|
||||||
private suspend fun getNewKeys() {
|
private suspend fun getNewKeys() {
|
||||||
newKeys = newKeys
|
newKeys = newKeys
|
||||||
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
|
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
|
||||||
.parsedSafe<NewExternalKeys>()
|
.parsedSafe<NewExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
|
||||||
|
BACKUP_KEY_DATA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNewWcoKey(): NewExternalKeys? {
|
suspend fun getNewWcoKey(): NewExternalKeys? {
|
||||||
|
|
@ -21,11 +21,10 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val validApis
|
private val validApis by lazy {
|
||||||
get() =
|
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
|
||||||
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
//.distinctBy { it.uniqueId }
|
||||||
//.distinctBy { it.uniqueId }
|
}
|
||||||
|
|
||||||
|
|
||||||
data class CrossMetaData(
|
data class CrossMetaData(
|
||||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||||
|
|
@ -61,8 +60,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
val base = super.load(url)?.apply {
|
val base = super.load(url)?.apply {
|
||||||
this.recommendations =
|
this.recommendations = this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||||
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
|
||||||
val matchName = filterName(this.name)
|
val matchName = filterName(this.name)
|
||||||
when (this) {
|
when (this) {
|
||||||
is MovieLoadResponse -> {
|
is MovieLoadResponse -> {
|
||||||
|
|
@ -100,7 +98,6 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
this.dataUrl =
|
this.dataUrl =
|
||||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
|
||||||
|
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
|
||||||
|
import com.lagradost.cloudstream3.utils.SyncUtil
|
||||||
|
|
||||||
|
// wont be implemented
|
||||||
|
class MultiAnimeProvider : MainAPI() {
|
||||||
|
override var name = "MultiAnime"
|
||||||
|
override var lang = "en"
|
||||||
|
override val usesWebView = true
|
||||||
|
override val supportedTypes = setOf(TvType.Anime)
|
||||||
|
private val syncApi: SyncAPI = aniListApi
|
||||||
|
|
||||||
|
private val syncUtilType by lazy {
|
||||||
|
when (syncApi) {
|
||||||
|
is AniListApi -> "anilist"
|
||||||
|
is MALApi -> "myanimelist"
|
||||||
|
else -> throw ErrorLoadingException("Invalid Api")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val validApis by lazy {
|
||||||
|
APIHolder.apis.filter {
|
||||||
|
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
||||||
|
TvType.Anime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterName(name: String): String {
|
||||||
|
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: String): List<SearchResponse>? {
|
||||||
|
return syncApi.search(query)?.map {
|
||||||
|
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
|
return syncApi.getResult(url)?.let { res ->
|
||||||
|
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
|
||||||
|
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
|
||||||
|
}.filterNotNull()
|
||||||
|
|
||||||
|
val type =
|
||||||
|
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
|
||||||
|
|
||||||
|
newAnimeLoadResponse(
|
||||||
|
res.title ?: throw ErrorLoadingException("No Title found"),
|
||||||
|
url,
|
||||||
|
type
|
||||||
|
) {
|
||||||
|
posterUrl = res.posterUrl
|
||||||
|
plot = res.synopsis
|
||||||
|
tags = res.genres
|
||||||
|
rating = res.publicScore
|
||||||
|
addTrailer(res.trailers)
|
||||||
|
addAniListId(res.id.toIntOrNull())
|
||||||
|
recommendations = res.recommendations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,15 @@ package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.MainAPI
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
|
||||||
object SyncRedirector {
|
object SyncRedirector {
|
||||||
|
val syncApis = SyncApis
|
||||||
private val syncIds =
|
private val syncIds =
|
||||||
listOf(
|
listOf(
|
||||||
SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
|
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
|
||||||
SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
|
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun redirect(
|
suspend fun redirect(
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,6 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episode.episode_number,
|
episode.episode_number,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
this.name ?: this.original_name,
|
|
||||||
).toJson(),
|
).toJson(),
|
||||||
episode.name,
|
episode.name,
|
||||||
episode.season_number,
|
episode.season_number,
|
||||||
|
|
@ -123,7 +122,6 @@ open class TmdbProvider : MainAPI() {
|
||||||
this.id,
|
this.id,
|
||||||
episodeNum,
|
episodeNum,
|
||||||
season.season_number,
|
season.season_number,
|
||||||
this.name ?: this.original_name,
|
|
||||||
).toJson(),
|
).toJson(),
|
||||||
season = season.season_number
|
season = season.season_number
|
||||||
)
|
)
|
||||||
|
|
@ -153,8 +151,6 @@ open class TmdbProvider : MainAPI() {
|
||||||
recommendations = (this@toLoadResponse.recommendations
|
recommendations = (this@toLoadResponse.recommendations
|
||||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||||
addActors(credits?.cast?.toList().toActors())
|
addActors(credits?.cast?.toList().toActors())
|
||||||
|
|
||||||
contentRating = fetchContentRating(id, "US")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,8 +193,6 @@ open class TmdbProvider : MainAPI() {
|
||||||
recommendations = (this@toLoadResponse.recommendations
|
recommendations = (this@toLoadResponse.recommendations
|
||||||
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
|
||||||
addActors(credits?.cast?.toList().toActors())
|
addActors(credits?.cast?.toList().toActors())
|
||||||
|
|
||||||
contentRating = fetchContentRating(id, "US")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -270,26 +264,6 @@ open class TmdbProvider : MainAPI() {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun fetchContentRating(id: Int?, country: String): String? {
|
|
||||||
id ?: return null
|
|
||||||
|
|
||||||
val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results
|
|
||||||
return if (!contentRatings.isNullOrEmpty()) {
|
|
||||||
contentRatings.firstOrNull { it: ContentRating ->
|
|
||||||
it.iso_3166_1 == country
|
|
||||||
}?.rating
|
|
||||||
} else {
|
|
||||||
val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results
|
|
||||||
val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult ->
|
|
||||||
it.iso_3166_1 == country
|
|
||||||
}?.release_dates?.firstOrNull { it: ReleaseDate ->
|
|
||||||
!it.certification.isNullOrBlank()
|
|
||||||
}?.certification
|
|
||||||
|
|
||||||
certification
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Possible to add recommendations and such here.
|
// Possible to add recommendations and such here.
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
// https://www.themoviedb.org/movie/7445-brothers
|
// https://www.themoviedb.org/movie/7445-brothers
|
||||||
|
|
|
||||||
|
|
@ -1,471 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.metaproviders
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAlias
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
|
||||||
import com.lagradost.cloudstream3.APIHolder
|
|
||||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
|
||||||
import com.lagradost.cloudstream3.Actor
|
|
||||||
import com.lagradost.cloudstream3.ActorData
|
|
||||||
import com.lagradost.cloudstream3.Episode
|
|
||||||
import com.lagradost.cloudstream3.HomePageResponse
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
|
||||||
import com.lagradost.cloudstream3.MainAPI
|
|
||||||
import com.lagradost.cloudstream3.MainPageRequest
|
|
||||||
import com.lagradost.cloudstream3.NextAiring
|
|
||||||
import com.lagradost.cloudstream3.ProviderType
|
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
|
||||||
import com.lagradost.cloudstream3.ShowStatus
|
|
||||||
import com.lagradost.cloudstream3.TvType
|
|
||||||
import com.lagradost.cloudstream3.addDate
|
|
||||||
import com.lagradost.cloudstream3.app
|
|
||||||
import com.lagradost.cloudstream3.base64Decode
|
|
||||||
import com.lagradost.cloudstream3.mainPageOf
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import com.lagradost.cloudstream3.newHomePageResponse
|
|
||||||
import com.lagradost.cloudstream3.newMovieLoadResponse
|
|
||||||
import com.lagradost.cloudstream3.newMovieSearchResponse
|
|
||||||
import com.lagradost.cloudstream3.newTvSeriesLoadResponse
|
|
||||||
import com.lagradost.cloudstream3.newTvSeriesSearchResponse
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
open class TraktProvider : MainAPI() {
|
|
||||||
override var name = "Trakt"
|
|
||||||
override val hasMainPage = true
|
|
||||||
override val providerType = ProviderType.MetaProvider
|
|
||||||
override val supportedTypes = setOf(
|
|
||||||
TvType.Movie,
|
|
||||||
TvType.TvSeries,
|
|
||||||
TvType.Anime,
|
|
||||||
)
|
|
||||||
|
|
||||||
private val traktClientId =
|
|
||||||
base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
|
|
||||||
private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
|
|
||||||
|
|
||||||
override val mainPage = mainPageOf(
|
|
||||||
"$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
|
|
||||||
"$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
|
|
||||||
"$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
|
|
||||||
"$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
|
|
||||||
)
|
|
||||||
|
|
||||||
override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
|
|
||||||
|
|
||||||
val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
|
|
||||||
|
|
||||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
|
||||||
element.toSearchResponse()
|
|
||||||
}
|
|
||||||
return newHomePageResponse(request.name, results)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun MediaDetails.toSearchResponse(): SearchResponse {
|
|
||||||
|
|
||||||
val media = this.media ?: this
|
|
||||||
val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
|
|
||||||
val poster = media.images?.poster?.firstOrNull()
|
|
||||||
|
|
||||||
if (mediaType == TvType.Movie) {
|
|
||||||
return newMovieSearchResponse(
|
|
||||||
name = media.title!!,
|
|
||||||
url = Data(
|
|
||||||
type = mediaType,
|
|
||||||
mediaDetails = media,
|
|
||||||
).toJson(),
|
|
||||||
type = TvType.Movie,
|
|
||||||
) {
|
|
||||||
posterUrl = fixPath(poster)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return newTvSeriesSearchResponse(
|
|
||||||
name = media.title!!,
|
|
||||||
url = Data(
|
|
||||||
type = mediaType,
|
|
||||||
mediaDetails = media,
|
|
||||||
).toJson(),
|
|
||||||
type = TvType.TvSeries,
|
|
||||||
) {
|
|
||||||
this.posterUrl = fixPath(poster)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun search(query: String): List<SearchResponse>? {
|
|
||||||
val apiResponse =
|
|
||||||
getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
|
|
||||||
|
|
||||||
val results = parseJson<List<MediaDetails>>(apiResponse).map { element ->
|
|
||||||
element.toSearchResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse {
|
|
||||||
|
|
||||||
val data = parseJson<Data>(url)
|
|
||||||
val mediaDetails = data.mediaDetails
|
|
||||||
val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
|
|
||||||
|
|
||||||
val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
|
|
||||||
val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
|
|
||||||
|
|
||||||
val resActor =
|
|
||||||
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
|
|
||||||
|
|
||||||
val actors = parseJson<People>(resActor).cast?.map {
|
|
||||||
ActorData(
|
|
||||||
Actor(
|
|
||||||
name = it.person?.name!!,
|
|
||||||
image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
|
|
||||||
),
|
|
||||||
roleString = it.character
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val resRelated =
|
|
||||||
getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
|
|
||||||
|
|
||||||
val relatedMedia = parseJson<List<MediaDetails>>(resRelated).map { it.toSearchResponse() }
|
|
||||||
|
|
||||||
val isCartoon =
|
|
||||||
mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
|
|
||||||
val isAnime =
|
|
||||||
isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
|
|
||||||
val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
|
|
||||||
val isBollywood = mediaDetails?.country == "in"
|
|
||||||
|
|
||||||
if (data.type == TvType.Movie) {
|
|
||||||
|
|
||||||
val linkData = LinkData(
|
|
||||||
id = mediaDetails?.ids?.tmdb,
|
|
||||||
traktId = mediaDetails?.ids?.trakt,
|
|
||||||
traktSlug = mediaDetails?.ids?.slug,
|
|
||||||
tmdbId = mediaDetails?.ids?.tmdb,
|
|
||||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
|
||||||
tvdbId = mediaDetails?.ids?.tvdb,
|
|
||||||
tvrageId = mediaDetails?.ids?.tvrage,
|
|
||||||
type = data.type.toString(),
|
|
||||||
title = mediaDetails?.title,
|
|
||||||
year = mediaDetails?.year,
|
|
||||||
orgTitle = mediaDetails?.title,
|
|
||||||
isAnime = isAnime,
|
|
||||||
//jpTitle = later if needed as it requires another network request,
|
|
||||||
airedDate = mediaDetails?.released
|
|
||||||
?: mediaDetails?.firstAired,
|
|
||||||
isAsian = isAsian,
|
|
||||||
isBollywood = isBollywood,
|
|
||||||
).toJson()
|
|
||||||
|
|
||||||
return newMovieLoadResponse(
|
|
||||||
name = mediaDetails?.title!!,
|
|
||||||
url = data.toJson(),
|
|
||||||
dataUrl = linkData.toJson(),
|
|
||||||
type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
|
|
||||||
) {
|
|
||||||
this.name = mediaDetails.title
|
|
||||||
this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
|
|
||||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
|
||||||
this.year = mediaDetails.year
|
|
||||||
this.plot = mediaDetails.overview
|
|
||||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
|
||||||
this.tags = mediaDetails.genres
|
|
||||||
this.duration = mediaDetails.runtime
|
|
||||||
this.recommendations = relatedMedia
|
|
||||||
this.actors = actors
|
|
||||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
|
||||||
//posterHeaders
|
|
||||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
|
||||||
this.contentRating = mediaDetails.certification
|
|
||||||
addTrailer(mediaDetails.trailer)
|
|
||||||
addImdbId(mediaDetails.ids?.imdb)
|
|
||||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val resSeasons =
|
|
||||||
getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
|
|
||||||
val episodes = mutableListOf<Episode>()
|
|
||||||
val seasons = parseJson<List<Seasons>>(resSeasons)
|
|
||||||
var nextAir: NextAiring? = null
|
|
||||||
|
|
||||||
seasons.forEach { season ->
|
|
||||||
|
|
||||||
season.episodes?.map { episode ->
|
|
||||||
|
|
||||||
val linkData = LinkData(
|
|
||||||
id = mediaDetails?.ids?.tmdb,
|
|
||||||
traktId = mediaDetails?.ids?.trakt,
|
|
||||||
traktSlug = mediaDetails?.ids?.slug,
|
|
||||||
tmdbId = mediaDetails?.ids?.tmdb,
|
|
||||||
imdbId = mediaDetails?.ids?.imdb.toString(),
|
|
||||||
tvdbId = mediaDetails?.ids?.tvdb,
|
|
||||||
tvrageId = mediaDetails?.ids?.tvrage,
|
|
||||||
type = data.type.toString(),
|
|
||||||
season = episode.season,
|
|
||||||
episode = episode.number,
|
|
||||||
title = mediaDetails?.title,
|
|
||||||
year = mediaDetails?.year,
|
|
||||||
orgTitle = mediaDetails?.title,
|
|
||||||
isAnime = isAnime,
|
|
||||||
airedYear = mediaDetails?.year,
|
|
||||||
lastSeason = seasons.size,
|
|
||||||
epsTitle = episode.title,
|
|
||||||
//jpTitle = later if needed as it requires another network request,
|
|
||||||
date = episode.firstAired,
|
|
||||||
airedDate = episode.firstAired,
|
|
||||||
isAsian = isAsian,
|
|
||||||
isBollywood = isBollywood,
|
|
||||||
isCartoon = isCartoon
|
|
||||||
).toJson()
|
|
||||||
|
|
||||||
episodes.add(
|
|
||||||
Episode(
|
|
||||||
data = linkData.toJson(),
|
|
||||||
name = episode.title,
|
|
||||||
season = episode.season,
|
|
||||||
episode = episode.number,
|
|
||||||
posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
|
|
||||||
rating = episode.rating?.times(10)?.roundToInt(),
|
|
||||||
description = episode.overview,
|
|
||||||
runTime = episode.runtime
|
|
||||||
).apply {
|
|
||||||
this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
|
|
||||||
if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) {
|
|
||||||
nextAir = NextAiring(
|
|
||||||
episode = this.episode!!,
|
|
||||||
unixTime = this.date!!.div(1000L),
|
|
||||||
season = if (this.season == 1) null else this.season,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTvSeriesLoadResponse(
|
|
||||||
name = mediaDetails?.title!!,
|
|
||||||
url = data.toJson(),
|
|
||||||
type = if (isAnime) TvType.Anime else TvType.TvSeries,
|
|
||||||
episodes = episodes
|
|
||||||
) {
|
|
||||||
this.name = mediaDetails.title
|
|
||||||
this.type = if (isAnime) TvType.Anime else TvType.TvSeries
|
|
||||||
this.episodes = episodes
|
|
||||||
this.posterUrl = getOriginalWidthImageUrl(posterUrl)
|
|
||||||
this.year = mediaDetails.year
|
|
||||||
this.plot = mediaDetails.overview
|
|
||||||
this.showStatus = getStatus(mediaDetails.status)
|
|
||||||
this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
|
|
||||||
this.tags = mediaDetails.genres
|
|
||||||
this.duration = mediaDetails.runtime
|
|
||||||
this.recommendations = relatedMedia
|
|
||||||
this.actors = actors
|
|
||||||
this.comingSoon = isUpcoming(mediaDetails.released)
|
|
||||||
//posterHeaders
|
|
||||||
this.nextAiring = nextAir
|
|
||||||
this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
|
|
||||||
this.contentRating = mediaDetails.certification
|
|
||||||
addTrailer(mediaDetails.trailer)
|
|
||||||
addImdbId(mediaDetails.ids?.imdb)
|
|
||||||
addTMDbId(mediaDetails.ids?.tmdb.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getApi(url: String): String {
|
|
||||||
return app.get(
|
|
||||||
url = url,
|
|
||||||
headers = mapOf(
|
|
||||||
"Content-Type" to "application/json",
|
|
||||||
"trakt-api-version" to "2",
|
|
||||||
"trakt-api-key" to traktClientId,
|
|
||||||
)
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isUpcoming(dateString: String?): Boolean {
|
|
||||||
return try {
|
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
|
||||||
val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
|
|
||||||
unixTimeMS < dateTime
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
logError(t)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getStatus(t: String?): ShowStatus {
|
|
||||||
return when (t) {
|
|
||||||
"returning series" -> ShowStatus.Ongoing
|
|
||||||
"continuing" -> ShowStatus.Ongoing
|
|
||||||
else -> ShowStatus.Completed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fixPath(url: String?): String? {
|
|
||||||
url ?: return null
|
|
||||||
return "https://$url"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getWidthImageUrl(path: String?, width: String): String? {
|
|
||||||
if (path == null) return null
|
|
||||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
|
||||||
val fileName = Uri.parse(path).lastPathSegment ?: return null
|
|
||||||
return "https://image.tmdb.org/t/p/${width}/${fileName}"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getOriginalWidthImageUrl(path: String?): String? {
|
|
||||||
if (path == null) return null
|
|
||||||
if (!path.contains("image.tmdb.org")) return fixPath(path)
|
|
||||||
return getWidthImageUrl(path, "original")
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Data(
|
|
||||||
val type: TvType? = null,
|
|
||||||
val mediaDetails: MediaDetails? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class MediaDetails(
|
|
||||||
@JsonProperty("title") val title: String? = null,
|
|
||||||
@JsonProperty("year") val year: Int? = null,
|
|
||||||
@JsonProperty("ids") val ids: Ids? = null,
|
|
||||||
@JsonProperty("tagline") val tagline: String? = null,
|
|
||||||
@JsonProperty("overview") val overview: String? = null,
|
|
||||||
@JsonProperty("released") val released: String? = null,
|
|
||||||
@JsonProperty("runtime") val runtime: Int? = null,
|
|
||||||
@JsonProperty("country") val country: String? = null,
|
|
||||||
@JsonProperty("updatedAt") val updatedAt: String? = null,
|
|
||||||
@JsonProperty("trailer") val trailer: String? = null,
|
|
||||||
@JsonProperty("homepage") val homepage: String? = null,
|
|
||||||
@JsonProperty("status") val status: String? = null,
|
|
||||||
@JsonProperty("rating") val rating: Double? = null,
|
|
||||||
@JsonProperty("votes") val votes: Long? = null,
|
|
||||||
@JsonProperty("comment_count") val commentCount: Long? = null,
|
|
||||||
@JsonProperty("language") val language: String? = null,
|
|
||||||
@JsonProperty("languages") val languages: List<String>? = null,
|
|
||||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
|
||||||
@JsonProperty("genres") val genres: List<String>? = null,
|
|
||||||
@JsonProperty("certification") val certification: String? = null,
|
|
||||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
|
||||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
|
||||||
@JsonProperty("airs") val airs: Airs? = null,
|
|
||||||
@JsonProperty("network") val network: String? = null,
|
|
||||||
@JsonProperty("images") val images: Images? = null,
|
|
||||||
@JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Airs(
|
|
||||||
@JsonProperty("day") val day: String? = null,
|
|
||||||
@JsonProperty("time") val time: String? = null,
|
|
||||||
@JsonProperty("timezone") val timezone: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Ids(
|
|
||||||
@JsonProperty("trakt") val trakt: Int? = null,
|
|
||||||
@JsonProperty("slug") val slug: String? = null,
|
|
||||||
@JsonProperty("tvdb") val tvdb: Int? = null,
|
|
||||||
@JsonProperty("imdb") val imdb: String? = null,
|
|
||||||
@JsonProperty("tmdb") val tmdb: Int? = null,
|
|
||||||
@JsonProperty("tvrage") val tvrage: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Images(
|
|
||||||
@JsonProperty("fanart") val fanart: List<String>? = null,
|
|
||||||
@JsonProperty("poster") val poster: List<String>? = null,
|
|
||||||
@JsonProperty("logo") val logo: List<String>? = null,
|
|
||||||
@JsonProperty("clearart") val clearart: List<String>? = null,
|
|
||||||
@JsonProperty("banner") val banner: List<String>? = null,
|
|
||||||
@JsonProperty("thumb") val thumb: List<String>? = null,
|
|
||||||
@JsonProperty("screenshot") val screenshot: List<String>? = null,
|
|
||||||
@JsonProperty("headshot") val headshot: List<String>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class People(
|
|
||||||
@JsonProperty("cast") val cast: List<Cast>? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Cast(
|
|
||||||
@JsonProperty("character") val character: String? = null,
|
|
||||||
@JsonProperty("characters") val characters: List<String>? = null,
|
|
||||||
@JsonProperty("episode_count") val episodeCount: Long? = null,
|
|
||||||
@JsonProperty("person") val person: Person? = null,
|
|
||||||
@JsonProperty("images") val images: Images? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Person(
|
|
||||||
@JsonProperty("name") val name: String? = null,
|
|
||||||
@JsonProperty("ids") val ids: Ids? = null,
|
|
||||||
@JsonProperty("images") val images: Images? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class Seasons(
|
|
||||||
@JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
|
|
||||||
@JsonProperty("episode_count") val episodeCount: Int? = null,
|
|
||||||
@JsonProperty("episodes") val episodes: List<TraktEpisode>? = null,
|
|
||||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
|
||||||
@JsonProperty("ids") val ids: Ids? = null,
|
|
||||||
@JsonProperty("images") val images: Images? = null,
|
|
||||||
@JsonProperty("network") val network: String? = null,
|
|
||||||
@JsonProperty("number") val number: Int? = null,
|
|
||||||
@JsonProperty("overview") val overview: String? = null,
|
|
||||||
@JsonProperty("rating") val rating: Double? = null,
|
|
||||||
@JsonProperty("title") val title: String? = null,
|
|
||||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
|
||||||
@JsonProperty("votes") val votes: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class TraktEpisode(
|
|
||||||
@JsonProperty("available_translations") val availableTranslations: List<String>? = null,
|
|
||||||
@JsonProperty("comment_count") val commentCount: Int? = null,
|
|
||||||
@JsonProperty("episode_type") val episodeType: String? = null,
|
|
||||||
@JsonProperty("first_aired") val firstAired: String? = null,
|
|
||||||
@JsonProperty("ids") val ids: Ids? = null,
|
|
||||||
@JsonProperty("images") val images: Images? = null,
|
|
||||||
@JsonProperty("number") val number: Int? = null,
|
|
||||||
@JsonProperty("number_abs") val numberAbs: Int? = null,
|
|
||||||
@JsonProperty("overview") val overview: String? = null,
|
|
||||||
@JsonProperty("rating") val rating: Double? = null,
|
|
||||||
@JsonProperty("runtime") val runtime: Int? = null,
|
|
||||||
@JsonProperty("season") val season: Int? = null,
|
|
||||||
@JsonProperty("title") val title: String? = null,
|
|
||||||
@JsonProperty("updated_at") val updatedAt: String? = null,
|
|
||||||
@JsonProperty("votes") val votes: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class LinkData(
|
|
||||||
val id: Int? = null,
|
|
||||||
val traktId: Int? = null,
|
|
||||||
val traktSlug: String? = null,
|
|
||||||
val tmdbId: Int? = null,
|
|
||||||
val imdbId: String? = null,
|
|
||||||
val tvdbId: Int? = null,
|
|
||||||
val tvrageId: String? = null,
|
|
||||||
val type: String? = null,
|
|
||||||
val season: Int? = null,
|
|
||||||
val episode: Int? = null,
|
|
||||||
val aniId: String? = null,
|
|
||||||
val animeId: String? = null,
|
|
||||||
val title: String? = null,
|
|
||||||
val year: Int? = null,
|
|
||||||
val orgTitle: String? = null,
|
|
||||||
val isAnime: Boolean = false,
|
|
||||||
val airedYear: Int? = null,
|
|
||||||
val lastSeason: Int? = null,
|
|
||||||
val epsTitle: String? = null,
|
|
||||||
val jpTitle: String? = null,
|
|
||||||
val date: String? = null,
|
|
||||||
val airedDate: String? = null,
|
|
||||||
val isAsian: Boolean = false,
|
|
||||||
val isBollywood: Boolean = false,
|
|
||||||
val isCartoon: Boolean = false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package com.lagradost.cloudstream3.mvvm
|
package com.lagradost.cloudstream3.mvvm
|
||||||
|
|
||||||
import com.lagradost.api.BuildConfig
|
import android.util.Log
|
||||||
import com.lagradost.api.Log
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import com.bumptech.glide.load.HttpException
|
||||||
|
import com.lagradost.cloudstream3.BuildConfig
|
||||||
import com.lagradost.cloudstream3.ErrorLoadingException
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.InterruptedIOException
|
import java.io.InterruptedIOException
|
||||||
|
|
@ -46,6 +49,40 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||||
|
liveData.observe(this) { it?.let { t -> action(t) } }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||||
|
liveData.observe(this) { action(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any> some(value: T?): Some<T> {
|
||||||
|
return if (value == null) {
|
||||||
|
Some.None
|
||||||
|
} else {
|
||||||
|
Some.Success(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Some<out T> {
|
||||||
|
data class Success<out T>(val value: T) : Some<T>()
|
||||||
|
object None : Some<Nothing>()
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
is None -> "None"
|
||||||
|
is Success -> "Some(${value.toString()})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class ResourceSome<out T> {
|
||||||
|
data class Success<out T>(val value: T) : ResourceSome<T>()
|
||||||
|
object None : ResourceSome<Nothing>()
|
||||||
|
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Resource<out T> {
|
sealed class Resource<out T> {
|
||||||
data class Success<out T>(val value: T) : Resource<T>()
|
data class Success<out T>(val value: T) : Resource<T>()
|
||||||
data class Failure(
|
data class Failure(
|
||||||
|
|
@ -84,21 +121,13 @@ suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Throwable.getAllMessages(): String {
|
|
||||||
return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
|
|
||||||
val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else ""
|
|
||||||
return prefix + this.stackTrace.joinToString(
|
|
||||||
separator = "\n"
|
|
||||||
) {
|
|
||||||
"${it.fileName} ${it.lineNumber}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> safeFail(throwable: Throwable): Resource<T> {
|
fun <T> safeFail(throwable: Throwable): Resource<T> {
|
||||||
val stackTraceMsg = throwable.getStackTracePretty()
|
val stackTraceMsg =
|
||||||
|
(throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
|
||||||
|
separator = "\n"
|
||||||
|
) {
|
||||||
|
"${it.fileName} ${it.lineNumber}"
|
||||||
|
}
|
||||||
return Resource.Failure(false, null, null, stackTraceMsg)
|
return Resource.Failure(false, null, null, stackTraceMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,70 +147,6 @@ fun CoroutineScope.launchSafe(
|
||||||
return this.launch(context, start, obj)
|
return this.launch(context, start, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun<T> throwAbleToResource(
|
|
||||||
throwable: Throwable
|
|
||||||
): Resource<T> {
|
|
||||||
return when (throwable) {
|
|
||||||
is NullPointerException -> {
|
|
||||||
for (line in throwable.stackTrace) {
|
|
||||||
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
|
|
||||||
return Resource.Failure(
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
safeFail(throwable)
|
|
||||||
}
|
|
||||||
is SocketTimeoutException, is InterruptedIOException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
"Connection Timeout\nPlease try again later."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// is HttpException -> {
|
|
||||||
// Resource.Failure(
|
|
||||||
// false,
|
|
||||||
// throwable.statusCode,
|
|
||||||
// null,
|
|
||||||
// throwable.message ?: "HttpException"
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
is UnknownHostException -> {
|
|
||||||
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
|
|
||||||
}
|
|
||||||
is ErrorLoadingException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
throwable.message ?: "Error loading, try again later."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is NotImplementedError -> {
|
|
||||||
Resource.Failure(false, null, null, "This operation is not implemented.")
|
|
||||||
}
|
|
||||||
is SSLHandshakeException -> {
|
|
||||||
Resource.Failure(
|
|
||||||
true,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is CancellationException -> {
|
|
||||||
throwable.cause?.let {
|
|
||||||
throwAbleToResource(it)
|
|
||||||
} ?: safeFail(throwable)
|
|
||||||
}
|
|
||||||
else -> safeFail(throwable)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun <T> safeApiCall(
|
suspend fun <T> safeApiCall(
|
||||||
apiCall: suspend () -> T,
|
apiCall: suspend () -> T,
|
||||||
): Resource<T> {
|
): Resource<T> {
|
||||||
|
|
@ -190,7 +155,60 @@ suspend fun <T> safeApiCall(
|
||||||
Resource.Success(apiCall.invoke())
|
Resource.Success(apiCall.invoke())
|
||||||
} catch (throwable: Throwable) {
|
} catch (throwable: Throwable) {
|
||||||
logError(throwable)
|
logError(throwable)
|
||||||
throwAbleToResource(throwable)
|
when (throwable) {
|
||||||
|
is NullPointerException -> {
|
||||||
|
for (line in throwable.stackTrace) {
|
||||||
|
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
|
||||||
|
return@withContext Resource.Failure(
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
safeFail(throwable)
|
||||||
|
}
|
||||||
|
is SocketTimeoutException, is InterruptedIOException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"Connection Timeout\nPlease try again later."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HttpException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
false,
|
||||||
|
throwable.statusCode,
|
||||||
|
null,
|
||||||
|
throwable.message ?: "HttpException"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is UnknownHostException -> {
|
||||||
|
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
|
||||||
|
}
|
||||||
|
is ErrorLoadingException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
throwable.message ?: "Error loading, try again later."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is NotImplementedError -> {
|
||||||
|
Resource.Failure(false, null, null, "This operation is not implemented.")
|
||||||
|
}
|
||||||
|
is SSLHandshakeException -> {
|
||||||
|
Resource.Failure(
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> safeFail(throwable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue