Merge pull request #325 from recloudstream/library
Bump anilist base to latest
4
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,8 +1,8 @@
|
||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Report provider bug
|
- name: Request a new provider or report bug with an existing provider
|
||||||
url: https://github.com/recloudstream
|
url: https://github.com/recloudstream
|
||||||
about: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
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.
|
||||||
|
|
BIN
.github/downloads.jpg
vendored
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
BIN
.github/home.jpg
vendored
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 136 KiB |
47
.github/locales.py
vendored
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import re
|
||||||
|
import glob
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
||||||
|
START_MARKER = "/* begin language list */"
|
||||||
|
END_MARKER = "/* end language list */"
|
||||||
|
XML_NAME = "app/src/main/res/values-"
|
||||||
|
ISO_MAP_URL = "https://gist.githubusercontent.com/Josantonius/b455e315bc7f790d14b136d61d9ae469/raw"
|
||||||
|
INDENT = " "*4
|
||||||
|
|
||||||
|
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
|
||||||
|
|
||||||
|
# Load settings file
|
||||||
|
src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
|
||||||
|
before_src, rest = src.split(START_MARKER)
|
||||||
|
rest, after_src = rest.split(END_MARKER)
|
||||||
|
|
||||||
|
# Load already added langs
|
||||||
|
languages = {}
|
||||||
|
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
|
||||||
|
flag, name, iso = lang.groups()
|
||||||
|
languages[iso] = (flag, name)
|
||||||
|
|
||||||
|
# Add not yet added langs
|
||||||
|
for folder in glob.glob(f"{XML_NAME}*"):
|
||||||
|
iso = folder[len(XML_NAME):]
|
||||||
|
if iso not in languages.keys():
|
||||||
|
languages[iso] = ("", iso_map.get(iso.lower(),iso))
|
||||||
|
|
||||||
|
# Create triples
|
||||||
|
triples = []
|
||||||
|
for iso in sorted(languages.keys()):
|
||||||
|
flag, name = languages[iso]
|
||||||
|
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
|
||||||
|
|
||||||
|
# Update settings file
|
||||||
|
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
|
||||||
|
before_src +
|
||||||
|
START_MARKER +
|
||||||
|
"\n" +
|
||||||
|
"\n".join(triples) +
|
||||||
|
"\n" +
|
||||||
|
END_MARKER +
|
||||||
|
after_src
|
||||||
|
)
|
BIN
.github/player.jpg
vendored
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 48 KiB |
BIN
.github/results.jpg
vendored
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 96 KiB |
BIN
.github/search.jpg
vendored
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 149 KiB |
76
.github/workflows/build_to_archive.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
name: Archive build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
- '*.json'
|
||||||
|
- '**/wcokey.txt'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "Archive-build"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate access token
|
||||||
|
id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
repository: "recloudstream/secrets"
|
||||||
|
- name: Generate access token (archive)
|
||||||
|
id: generate_archive_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
repository: "recloudstream/cloudstream-archive"
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '11'
|
||||||
|
distribution: 'adopt'
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Fetch keystore
|
||||||
|
id: fetch_keystore
|
||||||
|
run: |
|
||||||
|
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
|
||||||
|
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
|
||||||
|
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
|
||||||
|
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
|
||||||
|
KEY_PWD="$(cat keystore_password.txt)"
|
||||||
|
echo "::add-mask::${KEY_PWD}"
|
||||||
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
|
- name: Run Gradle
|
||||||
|
run: |
|
||||||
|
./gradlew assemblePrerelease
|
||||||
|
env:
|
||||||
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: "recloudstream/cloudstream-archive"
|
||||||
|
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||||
|
path: "archive"
|
||||||
|
|
||||||
|
- name: Move build
|
||||||
|
run: |
|
||||||
|
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
|
||||||
|
|
||||||
|
- name: Push archive
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/archive
|
||||||
|
git config --local user.email "actions@github.com"
|
||||||
|
git config --local user.name "GitHub Actions"
|
||||||
|
git add .
|
||||||
|
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
|
||||||
|
git push --force
|
3
.github/workflows/generate_dokka.yml
vendored
|
@ -39,9 +39,8 @@ jobs:
|
||||||
|
|
||||||
- name: Clean old builds
|
- name: Clean old builds
|
||||||
run: |
|
run: |
|
||||||
shopt -s extglob
|
|
||||||
cd $GITHUB_WORKSPACE/dokka/
|
cd $GITHUB_WORKSPACE/dokka/
|
||||||
rm -rf !(.git)
|
rm -rf "./-cloudstream"
|
||||||
|
|
||||||
- name: Setup JDK 11
|
- name: Setup JDK 11
|
||||||
uses: actions/setup-java@v1
|
uses: actions/setup-java@v1
|
||||||
|
|
|
@ -2,7 +2,7 @@ name: Issue automatic actions
|
||||||
|
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
types: [opened, edited]
|
types: [opened]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
issue-moderator:
|
issue-moderator:
|
||||||
|
@ -18,7 +18,7 @@ jobs:
|
||||||
uses: actions-cool/issues-similarity-analysis@v1
|
uses: actions-cool/issues-similarity-analysis@v1
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
filter-threshold: 0.5
|
filter-threshold: 0.60
|
||||||
title-excludes: ''
|
title-excludes: ''
|
||||||
comment-title: |
|
comment-title: |
|
||||||
### Your issue looks similar to these issues:
|
### Your issue looks similar to these issues:
|
||||||
|
@ -41,7 +41,7 @@ jobs:
|
||||||
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
|
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
|
||||||
pip3 install httpx
|
pip3 install httpx
|
||||||
RES="$(python3 ./check_issue.py)"
|
RES="$(python3 ./check_issue.py)"
|
||||||
echo "::set-output name=name::${RES}"
|
echo "name=${RES}" >> $GITHUB_OUTPUT
|
||||||
- name: Comment if issue mentions a provider
|
- name: Comment if issue mentions a provider
|
||||||
if: steps.provider_check.outputs.name != 'none'
|
if: steps.provider_check.outputs.name != 'none'
|
||||||
uses: actions-cool/issues-helper@v3
|
uses: actions-cool/issues-helper@v3
|
10
.github/workflows/prerelease.yml
vendored
|
@ -40,12 +40,10 @@ jobs:
|
||||||
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
|
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
|
||||||
KEY_PWD="$(cat keystore_password.txt)"
|
KEY_PWD="$(cat keystore_password.txt)"
|
||||||
echo "::add-mask::${KEY_PWD}"
|
echo "::add-mask::${KEY_PWD}"
|
||||||
echo "::set-output name=key_pwd::$KEY_PWD"
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: |
|
run: |
|
||||||
./gradlew assemblePrerelease
|
./gradlew assemblePrerelease makeJar androidSourcesJar
|
||||||
./gradlew androidSourcesJar
|
|
||||||
./gradlew makeJar
|
|
||||||
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 }}
|
||||||
|
@ -55,9 +53,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
automatic_release_tag: "pre-release"
|
automatic_release_tag: "pre-release"
|
||||||
prerelease: false
|
prerelease: true
|
||||||
title: "Pre-release Build"
|
title: "Pre-release Build"
|
||||||
files: |
|
files: |
|
||||||
app/build/outputs/apk/prerelease/*.apk
|
app/build/outputs/apk/prerelease/release/*.apk
|
||||||
app/build/libs/app-sources.jar
|
app/build/libs/app-sources.jar
|
||||||
app/build/classes.jar
|
app/build/classes.jar
|
||||||
|
|
4
.github/workflows/pull_request.yml
vendored
|
@ -15,9 +15,9 @@ jobs:
|
||||||
- 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 assembleDebug
|
run: ./gradlew assemblePrereleaseDebug
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: pull-request-build
|
name: pull-request-build
|
||||||
path: "app/build/outputs/apk/debug/*.apk"
|
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||||
|
|
39
.github/workflows/update_locales.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
name: Update locale lists
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.xml'
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "locale-list"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate access token
|
||||||
|
id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
repository: "recloudstream/cloudstream"
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
|
- name: Edit files
|
||||||
|
run: |
|
||||||
|
python3 .github/locales.py
|
||||||
|
- name: Commit to the repo
|
||||||
|
run: |
|
||||||
|
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
|
||||||
|
git config --local user.name "recloudstream[bot]"
|
||||||
|
git add .
|
||||||
|
# "echo" returns true so the build succeeds, even if no changed files
|
||||||
|
git commit -m 'update list of locales' || echo
|
||||||
|
git push
|
|
@ -31,5 +31,10 @@
|
||||||
<option name="name" value="maven2" />
|
<option name="name" value="maven2" />
|
||||||
<option name="url" value="https://jitpack.io" />
|
<option name="url" value="https://jitpack.io" />
|
||||||
</remote-repository>
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="MavenRepo" />
|
||||||
|
<option name="name" value="MavenRepo" />
|
||||||
|
<option name="url" value="https://repo.maven.apache.org/maven2/" />
|
||||||
|
</remote-repository>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
35
README.md
|
@ -1,44 +1,23 @@
|
||||||
# CloudStream
|
# CloudStream
|
||||||
|
|
||||||
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
|
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
|
||||||
You can find the list of community-maintained extension repositories [here
|
|
||||||
](https://recloudstream.github.io/repos/)
|
|
||||||
|
|
||||||
|
|
||||||
[![Discord](https://img.shields.io/discord/737724143126052974?style=for-the-badge)](https://discord.gg/5Hus6fM)
|
[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
|
||||||
|
|
||||||
***Features:***
|
### Features:
|
||||||
+ **AdFree**, No ads whatsoever
|
+ **AdFree**, No ads whatsoever
|
||||||
+ No tracking/analytics
|
+ No tracking/analytics
|
||||||
+ Bookmarks
|
+ Bookmarks
|
||||||
+ Download and stream movies, tv-shows and anime
|
+ Download and stream movies, tv-shows and anime
|
||||||
+ Chromecast
|
+ Chromecast
|
||||||
|
|
||||||
***Screenshots:***
|
### 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/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"/>
|
<img src="./.github/player.jpg" height="200"/>
|
||||||
|
|
||||||
***The list of supported languages:***
|
### Supported languages:
|
||||||
* 🇱🇧 Arabic
|
<a href="https://hosted.weblate.org/engage/cloudstream/">
|
||||||
* 🇨🇿 Czech
|
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
||||||
* 🇳🇱 Dutch
|
</a>
|
||||||
* 🇬🇧 English
|
|
||||||
* 🇫🇷 French
|
|
||||||
* 🇩🇪 German
|
|
||||||
* 🇬🇷 Greek
|
|
||||||
* 🇮🇳 Hindi
|
|
||||||
* 🇮🇩 Indonesian
|
|
||||||
* 🇮🇹 Italian
|
|
||||||
* 🇲🇰 Macedonian
|
|
||||||
* 🇮🇳 Malayalam
|
|
||||||
* 🇳🇴 Norsk
|
|
||||||
* 🇵🇱 Polish
|
|
||||||
* 🇧🇷 Portuguese (Brazil)
|
|
||||||
* 🇷🇴 Romanian
|
|
||||||
* 🇪🇸 Spanish
|
|
||||||
* 🇸🇪 Swedish
|
|
||||||
* 🇵🇭 Tagalog
|
|
||||||
* 🇹🇷 Turkish
|
|
||||||
* 🇻🇳 Vietnamese
|
|
||||||
|
|
215
app/build.gradle
|
@ -1,215 +0,0 @@
|
||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'kotlin-android'
|
|
||||||
id 'kotlin-kapt'
|
|
||||||
id 'kotlin-android-extensions'
|
|
||||||
id 'org.jetbrains.dokka'
|
|
||||||
}
|
|
||||||
|
|
||||||
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
|
||||||
def allFilesFromDir = new File(tmpFilePath).listFiles()
|
|
||||||
def prereleaseStoreFile = null
|
|
||||||
if (allFilesFromDir != null) {
|
|
||||||
prereleaseStoreFile = allFilesFromDir.first()
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
testOptions {
|
|
||||||
unitTests.returnDefaultValues = true
|
|
||||||
}
|
|
||||||
signingConfigs {
|
|
||||||
prerelease {
|
|
||||||
if (prereleaseStoreFile != null) {
|
|
||||||
storeFile = file(prereleaseStoreFile)
|
|
||||||
storePassword System.getenv("SIGNING_STORE_PASSWORD")
|
|
||||||
keyAlias System.getenv("SIGNING_KEY_ALIAS")
|
|
||||||
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileSdkVersion 31
|
|
||||||
buildToolsVersion "30.0.3"
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.lagradost.cloudstream3"
|
|
||||||
minSdkVersion 21
|
|
||||||
targetSdkVersion 30
|
|
||||||
|
|
||||||
versionCode 50
|
|
||||||
versionName "3.1.4"
|
|
||||||
|
|
||||||
resValue "string", "app_version",
|
|
||||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
|
||||||
|
|
||||||
resValue "string", "commit_hash",
|
|
||||||
("git rev-parse --short HEAD".execute().text.trim() ?: "")
|
|
||||||
|
|
||||||
resValue "bool", "is_prerelease", "false"
|
|
||||||
|
|
||||||
buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
|
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
|
|
||||||
kapt {
|
|
||||||
includeCompileClasspath = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
// release {
|
|
||||||
// debuggable false
|
|
||||||
// minifyEnabled false
|
|
||||||
// shrinkResources false
|
|
||||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
// resValue "bool", "is_prerelease", "false"
|
|
||||||
// }
|
|
||||||
prerelease {
|
|
||||||
applicationIdSuffix ".prerelease"
|
|
||||||
buildConfigField("boolean", "BETA", "true")
|
|
||||||
signingConfig signingConfigs.prerelease
|
|
||||||
versionNameSuffix '-PRE'
|
|
||||||
debuggable false
|
|
||||||
minifyEnabled false
|
|
||||||
shrinkResources false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
resValue "bool", "is_prerelease", "true"
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
debuggable true
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
|
||||||
resValue "bool", "is_prerelease", "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
coreLibraryDesugaringEnabled true
|
|
||||||
|
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = '1.8'
|
|
||||||
freeCompilerArgs = ['-Xjvm-default=compatibility']
|
|
||||||
}
|
|
||||||
lintOptions {
|
|
||||||
checkReleaseBuilds false
|
|
||||||
abortOnError false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven { url 'https://jitpack.io' }
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'com.google.android.mediahome:video:1.0.0'
|
|
||||||
implementation 'androidx.test.ext:junit-ktx:1.1.3'
|
|
||||||
testImplementation 'org.json:json:20180813'
|
|
||||||
|
|
||||||
implementation 'androidx.core:core-ktx:1.8.0'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.4.2' // need target 32 for 1.5.0
|
|
||||||
|
|
||||||
// dont change this to 1.6.0 it looks ugly af
|
|
||||||
implementation 'com.google.android.material:material:1.5.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
|
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
|
||||||
testImplementation 'junit:junit:4.13.2'
|
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
|
||||||
|
|
||||||
//implementation "io.karn:khttp-android:0.1.2" //okhttp instead
|
|
||||||
// implementation 'org.jsoup:jsoup:1.13.1'
|
|
||||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
|
|
||||||
|
|
||||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:glide:4.13.1'
|
|
||||||
kapt 'com.github.bumptech.glide:compiler:4.13.1'
|
|
||||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
|
|
||||||
|
|
||||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
|
||||||
|
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
|
||||||
|
|
||||||
// implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
|
|
||||||
|
|
||||||
// Exoplayer
|
|
||||||
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
|
|
||||||
implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
|
|
||||||
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
|
|
||||||
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
|
|
||||||
|
|
||||||
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
|
|
||||||
|
|
||||||
// Bug reports
|
|
||||||
implementation "ch.acra:acra-core:5.8.4"
|
|
||||||
implementation "ch.acra:acra-toast:5.8.4"
|
|
||||||
|
|
||||||
compileOnly "com.google.auto.service:auto-service-annotations:1.0"
|
|
||||||
//either for java sources:
|
|
||||||
annotationProcessor "com.google.auto.service:auto-service:1.0"
|
|
||||||
//or for kotlin sources (requires kapt gradle plugin):
|
|
||||||
kapt "com.google.auto.service:auto-service:1.0"
|
|
||||||
|
|
||||||
// subtitle color picker
|
|
||||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
|
||||||
|
|
||||||
//run JS
|
|
||||||
implementation 'org.mozilla:rhino:1.7.14'
|
|
||||||
|
|
||||||
// TorrentStream
|
|
||||||
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
|
|
||||||
|
|
||||||
// Downloading
|
|
||||||
implementation "androidx.work:work-runtime:2.7.1"
|
|
||||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
|
||||||
|
|
||||||
// Networking
|
|
||||||
// implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
|
||||||
// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
|
||||||
implementation 'com.github.Blatzar:NiceHttp:0.3.2'
|
|
||||||
|
|
||||||
// Util to skip the URI file fuckery 🙏
|
|
||||||
implementation "com.github.tachiyomiorg:unifile:17bec43"
|
|
||||||
|
|
||||||
// API because cba maintaining it myself
|
|
||||||
implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
|
|
||||||
|
|
||||||
implementation 'com.github.discord:OverlappingPanels:0.1.3'
|
|
||||||
// debugImplementation because LeakCanary should only run in debug builds.
|
|
||||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
|
||||||
|
|
||||||
// for shimmer when loading
|
|
||||||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
|
||||||
|
|
||||||
implementation "androidx.tvprovider:tvprovider:1.0.0"
|
|
||||||
|
|
||||||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
|
||||||
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
|
|
||||||
|
|
||||||
// slow af yt
|
|
||||||
//implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
|
|
||||||
|
|
||||||
// newpipe yt
|
|
||||||
implementation 'com.github.recloudstream:NewPipeExtractor:0.22.1'
|
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
|
||||||
|
|
||||||
// Library/extensions searching with Levenshtein distance
|
|
||||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
task androidSourcesJar(type: Jar) {
|
|
||||||
getArchiveClassifier().set('sources')
|
|
||||||
from android.sourceSets.main.java.srcDirs//full sources
|
|
||||||
}
|
|
||||||
|
|
||||||
task makeJar(type: Copy) {
|
|
||||||
// after modifying here, you can export. Jar
|
|
||||||
from('build/intermediates/compile_app_classes_jar/debug')
|
|
||||||
into('build') // output location
|
|
||||||
include('classes.jar') // the classes file of the imported rack package
|
|
||||||
dependsOn build
|
|
||||||
}
|
|
253
app/build.gradle.kts
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
import com.android.build.gradle.api.BaseVariantOutput
|
||||||
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
id("kotlin-android-extensions")
|
||||||
|
id("org.jetbrains.dokka")
|
||||||
|
}
|
||||||
|
|
||||||
|
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||||
|
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
||||||
|
|
||||||
|
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||||
|
if (project.exec {
|
||||||
|
workingDir = projectDir
|
||||||
|
commandLine = this@execute.split(Regex("\\s"))
|
||||||
|
standardOutput = baot
|
||||||
|
}.exitValue == 0)
|
||||||
|
String(baot.toByteArray()).trim()
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
testOptions {
|
||||||
|
unitTests.isReturnDefaultValues = true
|
||||||
|
}
|
||||||
|
signingConfigs {
|
||||||
|
create("prerelease") {
|
||||||
|
if (prereleaseStoreFile != null) {
|
||||||
|
storeFile = file(prereleaseStoreFile)
|
||||||
|
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||||
|
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||||
|
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileSdk = 33
|
||||||
|
buildToolsVersion = "30.0.3"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.lagradost.cloudstream3"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 33
|
||||||
|
|
||||||
|
versionCode = 55
|
||||||
|
versionName = "3.4.0"
|
||||||
|
|
||||||
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
|
|
||||||
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
|
||||||
|
resValue("bool", "is_prerelease", "false")
|
||||||
|
|
||||||
|
buildConfigField(
|
||||||
|
"String",
|
||||||
|
"BUILDDATE",
|
||||||
|
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||||
|
)
|
||||||
|
|
||||||
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
includeCompileClasspath = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
isDebuggable = false
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
isDebuggable = true
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
flavorDimensions.add("state")
|
||||||
|
productFlavors {
|
||||||
|
create("stable") {
|
||||||
|
dimension = "state"
|
||||||
|
resValue("bool", "is_prerelease", "false")
|
||||||
|
}
|
||||||
|
create("prerelease") {
|
||||||
|
dimension = "state"
|
||||||
|
resValue("bool", "is_prerelease", "true")
|
||||||
|
buildConfigField("boolean", "BETA", "true")
|
||||||
|
applicationIdSuffix = ".prerelease"
|
||||||
|
signingConfig = signingConfigs.getByName("prerelease")
|
||||||
|
versionNameSuffix = "-PRE"
|
||||||
|
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
||||||
|
}
|
||||||
|
lint {
|
||||||
|
abortOnError = false
|
||||||
|
checkReleaseBuilds = false
|
||||||
|
}
|
||||||
|
namespace = "com.lagradost.cloudstream3"
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven("https://jitpack.io")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("com.google.android.mediahome:video:1.0.0")
|
||||||
|
implementation("androidx.test.ext:junit-ktx:1.1.3")
|
||||||
|
testImplementation("org.json:json:20180813")
|
||||||
|
|
||||||
|
implementation("androidx.core:core-ktx:1.8.0")
|
||||||
|
implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
|
||||||
|
|
||||||
|
// dont change this to 1.6.0 it looks ugly af
|
||||||
|
implementation("com.google.android.material:material:1.5.0")
|
||||||
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||||
|
|
||||||
|
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
||||||
|
// implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||||
|
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
|
|
||||||
|
implementation("com.github.bumptech.glide:glide:4.13.1")
|
||||||
|
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
||||||
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
|
||||||
|
|
||||||
|
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||||
|
|
||||||
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
|
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
||||||
|
|
||||||
|
// Exoplayer
|
||||||
|
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
|
||||||
|
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
|
||||||
|
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
|
||||||
|
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
|
||||||
|
|
||||||
|
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
||||||
|
|
||||||
|
// Bug reports
|
||||||
|
implementation("ch.acra:acra-core:5.8.4")
|
||||||
|
implementation("ch.acra:acra-toast:5.8.4")
|
||||||
|
|
||||||
|
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
||||||
|
//either for java sources:
|
||||||
|
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
||||||
|
//or for kotlin sources (requires kapt gradle plugin):
|
||||||
|
kapt("com.google.auto.service:auto-service:1.0")
|
||||||
|
|
||||||
|
// subtitle color picker
|
||||||
|
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||||
|
|
||||||
|
//run JS
|
||||||
|
// 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.1")
|
||||||
|
// 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")
|
||||||
|
|
||||||
|
implementation("androidx.tvprovider:tvprovider:1.0.0")
|
||||||
|
|
||||||
|
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||||
|
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
|
||||||
|
|
||||||
|
// slow af yt
|
||||||
|
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
||||||
|
|
||||||
|
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
|
||||||
|
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||||
|
|
||||||
|
// Library/extensions searching with Levenshtein distance
|
||||||
|
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
|
archiveClassifier.set("sources")
|
||||||
|
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is used by the gradlew plugin
|
||||||
|
tasks.register("makeJar", Copy::class) {
|
||||||
|
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||||
|
into("build")
|
||||||
|
include("classes.jar")
|
||||||
|
dependsOn("build")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<DokkaTask>().configureEach {
|
||||||
|
moduleName.set("Cloudstream")
|
||||||
|
dokkaSourceSets {
|
||||||
|
named("main") {
|
||||||
|
sourceLink {
|
||||||
|
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
|
||||||
|
localDirectory.set(file("src/main/java"))
|
||||||
|
|
||||||
|
// URL showing where the source code can be accessed through the web browser
|
||||||
|
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
||||||
|
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||||
|
remoteLineSuffix.set("#L")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
app/proguard-rules.pro
vendored
|
@ -1,6 +1,6 @@
|
||||||
# Add project specific ProGuard rules here.
|
# Add project specific ProGuard rules here.
|
||||||
# You can control the set of applied configuration files using the
|
# You can control the set of applied configuration files using the
|
||||||
# proguardFiles setting in build.gradle.
|
# proguardFiles setting in build.gradle.kts.
|
||||||
#
|
#
|
||||||
# For more details, see
|
# For more details, see
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
|
@ -138,7 +138,7 @@ class ExampleInstrumentedTest {
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if(!validResults) {
|
if (!validResults) {
|
||||||
System.err.println("Api ${api.name} did not load on any")
|
System.err.println("Api ${api.name} did not load on any")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,10 +180,12 @@ class ExampleInstrumentedTest {
|
||||||
@Test
|
@Test
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getAllProviders().apmap { api ->
|
getAllProviders().amap { api ->
|
||||||
if (api.hasMainPage) {
|
if (api.hasMainPage) {
|
||||||
try {
|
try {
|
||||||
val homepage = api.getMainPage()
|
val f = api.mainPage.first()
|
||||||
|
val homepage =
|
||||||
|
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
|
||||||
when {
|
when {
|
||||||
homepage == null -> {
|
homepage == null -> {
|
||||||
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
|
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
|
||||||
|
@ -192,7 +194,7 @@ class ExampleInstrumentedTest {
|
||||||
System.err.println("Homepage provider ${api.name} does not contain any items!")
|
System.err.println("Homepage provider ${api.name} does not contain any items!")
|
||||||
}
|
}
|
||||||
homepage.items.any { it.list.isEmpty() } -> {
|
homepage.items.any { it.list.isEmpty() } -> {
|
||||||
System.err.println ("Homepage provider ${api.name} does not have any items on result!")
|
System.err.println("Homepage provider ${api.name} does not have any items on result!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -217,7 +219,7 @@ class ExampleInstrumentedTest {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
||||||
val providers = getAllProviders()
|
val providers = getAllProviders()
|
||||||
providers.apmap { api ->
|
providers.amap { api ->
|
||||||
try {
|
try {
|
||||||
println("Trying $api")
|
println("Trying $api")
|
||||||
if (testSingleProviderApi(api)) {
|
if (testSingleProviderApi(api)) {
|
||||||
|
@ -231,7 +233,7 @@ class ExampleInstrumentedTest {
|
||||||
invalidProvider.add(Pair(api, e))
|
invalidProvider.add(Pair(api, e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(invalidProvider.isEmpty()) {
|
if (invalidProvider.isEmpty()) {
|
||||||
println("No Invalid providers! :D")
|
println("No Invalid providers! :D")
|
||||||
} else {
|
} else {
|
||||||
println("Invalid providers are: ")
|
println("Invalid providers are: ")
|
||||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 9.1 KiB After Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 9 KiB After Width: | Height: | Size: 8.2 KiB |
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
package="com.lagradost.cloudstream3">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads -->
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads -->
|
||||||
<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 -->
|
||||||
|
@ -11,7 +10,11 @@
|
||||||
<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 -->
|
||||||
<!-- <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run -->
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ -->
|
||||||
|
<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.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||||
|
|
||||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
<!-- <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
|
||||||
|
@ -21,6 +24,13 @@
|
||||||
android:name="android.software.leanback"
|
android:name="android.software.leanback"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<package android:name="org.videolan.vlc" />
|
||||||
|
<package android:name="com.instantbits.cast.webvideo" />
|
||||||
|
<package android:name="is.xyz.mpv" />
|
||||||
|
</queries>
|
||||||
|
|
||||||
|
<!-- Without the large heap Exoplayer buffering gets reset due to OOM. -->
|
||||||
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
|
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
|
||||||
<application
|
<application
|
||||||
android:name=".AcraApplication"
|
android:name=".AcraApplication"
|
||||||
|
@ -30,6 +40,7 @@
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
android:largeHeap="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
|
@ -103,6 +114,30 @@
|
||||||
|
|
||||||
<data android:scheme="cloudstreamrepo" />
|
<data android:scheme="cloudstreamrepo" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Allow searching with intents: cloudstreamsearch://Your%20Name -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="cloudstreamsearch" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Allow opening from continue watching with intents: cloudstreamsearch://1234
|
||||||
|
Used on Android TV Watch Next
|
||||||
|
-->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="cloudstreamcontinuewatching" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
@ -138,6 +173,10 @@
|
||||||
android:name=".ui.ControllerActivity"
|
android:name=".ui.ControllerActivity"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".utils.PackageInstallerService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.4 KiB |
|
@ -7,10 +7,12 @@ import android.content.ContextWrapper
|
||||||
import android.content.Intent
|
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 com.google.auto.service.AutoService
|
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.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
import com.lagradost.cloudstream3.utils.AppUtils.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
|
||||||
|
@ -74,19 +76,28 @@ class CustomSenderFactory : ReportSenderFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.UncaughtExceptionHandler {
|
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
|
Thread.UncaughtExceptionHandler {
|
||||||
override fun uncaughtException(thread: Thread, error: Throwable) {
|
override fun uncaughtException(thread: Thread, error: Throwable) {
|
||||||
ACRA.errorReporter.handleException(error)
|
ACRA.errorReporter.handleException(error)
|
||||||
try {
|
try {
|
||||||
PrintStream(errorFile).use { ps ->
|
PrintStream(errorFile).use { ps ->
|
||||||
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
||||||
ps.println(String.format("Fatal exception on thread %s (%d)", 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) {
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
onError.invoke()
|
onError.invoke()
|
||||||
} catch (ignored: Exception) { }
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
|
||||||
class AcraApplication : Application() {
|
class AcraApplication : Application() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Thread.setDefaultUncaughtExceptionHandler(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))
|
||||||
})
|
})
|
||||||
|
@ -183,5 +194,15 @@ class AcraApplication : Application() {
|
||||||
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
|
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
|
||||||
context?.openBrowser(url, fallbackWebview, fragment)
|
context?.openBrowser(url, fallbackWebview, fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Will fallback to webview if in TV layout */
|
||||||
|
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||||
|
openBrowser(
|
||||||
|
url,
|
||||||
|
isTvSettings(),
|
||||||
|
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -10,16 +11,23 @@ import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
import androidx.annotation.StringRes
|
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.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.google.android.gms.cast.framework.CastSession
|
import com.google.android.gms.cast.framework.CastSession
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
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.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||||
|
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
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
||||||
|
@ -34,6 +42,7 @@ object CommonActivity {
|
||||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var canEnterPipMode: Boolean = false
|
var canEnterPipMode: Boolean = false
|
||||||
var canShowPipMode: Boolean = false
|
var canShowPipMode: Boolean = false
|
||||||
var isInPIPMode: Boolean = false
|
var isInPIPMode: Boolean = false
|
||||||
|
@ -54,7 +63,9 @@ object CommonActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
|
/** duration is Toast.LENGTH_SHORT if null*/
|
||||||
|
@MainThread
|
||||||
|
fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
showToast(act, act.getString(message), duration)
|
showToast(act, act.getString(message), duration)
|
||||||
}
|
}
|
||||||
|
@ -62,6 +73,7 @@ object CommonActivity {
|
||||||
const val TAG = "COMPACT"
|
const val TAG = "COMPACT"
|
||||||
|
|
||||||
/** duration is Toast.LENGTH_SHORT if null*/
|
/** duration is Toast.LENGTH_SHORT if null*/
|
||||||
|
@MainThread
|
||||||
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
|
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
|
||||||
if (act == null || message == null) {
|
if (act == null || message == null) {
|
||||||
Log.w(TAG, "invalid showToast act = $act message = $message")
|
Log.w(TAG, "invalid showToast act = $act message = $message")
|
||||||
|
@ -98,9 +110,18 @@ object CommonActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Not all languages can be fetched from locale with a code.
|
||||||
|
* This map allows sidestepping the default Locale(languageCode)
|
||||||
|
* when setting the app language.
|
||||||
|
**/
|
||||||
|
val appLanguageExceptions = hashMapOf(
|
||||||
|
"zh-rTW" to Locale.TRADITIONAL_CHINESE
|
||||||
|
)
|
||||||
|
|
||||||
fun setLocale(context: Context?, languageCode: String?) {
|
fun setLocale(context: Context?, languageCode: String?) {
|
||||||
if (context == null || languageCode == null) return
|
if (context == null || languageCode == null) return
|
||||||
val locale = Locale(languageCode)
|
val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
|
||||||
val resources: Resources = context.resources
|
val resources: Resources = context.resources
|
||||||
val config = resources.configuration
|
val config = resources.configuration
|
||||||
Locale.setDefault(locale)
|
Locale.setDefault(locale)
|
||||||
|
@ -117,7 +138,7 @@ object CommonActivity {
|
||||||
setLocale(this, localeCode)
|
setLocale(this, localeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(act: Activity?) {
|
fun init(act: ComponentActivity?) {
|
||||||
if (act == null) return
|
if (act == null) 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
|
||||||
|
@ -129,6 +150,39 @@ object CommonActivity {
|
||||||
act.updateLocale()
|
act.updateLocale()
|
||||||
act.updateTv()
|
act.updateTv()
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||||
|
|
||||||
|
for (resumeApp in resumeApps) {
|
||||||
|
resumeApp.launcher =
|
||||||
|
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
val resultCode = result.resultCode
|
||||||
|
val data = result.data
|
||||||
|
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||||
|
val pos = resumeApp.getPosition(data)
|
||||||
|
val dur = resumeApp.getDuration(data)
|
||||||
|
if (dur > 0L && pos > 0L)
|
||||||
|
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
|
||||||
|
removeKey(resumeApp.lastId)
|
||||||
|
ResultFragment.updateUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for notification permissions on Android 13
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
|
act,
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
val requestPermissionLauncher = act.registerForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { isGranted: Boolean ->
|
||||||
|
Log.d(TAG, "Notification permission: $isGranted")
|
||||||
|
}
|
||||||
|
requestPermissionLauncher.launch(
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Activity.enterPIPMode() {
|
private fun Activity.enterPIPMode() {
|
||||||
|
@ -166,6 +220,8 @@ object CommonActivity {
|
||||||
"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)
|
||||||
|
R.style.MonetMode else R.style.AppTheme
|
||||||
else -> R.style.AppTheme
|
else -> R.style.AppTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +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
|
||||||
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||||
|
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
|
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||||
else -> R.style.OverlayPrimaryColorNormal
|
else -> R.style.OverlayPrimaryColorNormal
|
||||||
}
|
}
|
||||||
act.theme.applyStyle(currentTheme, true)
|
act.theme.applyStyle(currentTheme, true)
|
||||||
|
@ -283,7 +343,7 @@ object CommonActivity {
|
||||||
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_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
|
||||||
PlayerEventType.Lock
|
PlayerEventType.Lock
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
|
||||||
|
@ -292,22 +352,25 @@ object CommonActivity {
|
||||||
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_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
|
||||||
PlayerEventType.ShowMirrors
|
PlayerEventType.ShowMirrors
|
||||||
}
|
}
|
||||||
// OpenSubtitles shortcut
|
// OpenSubtitles shortcut
|
||||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
|
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
|
||||||
PlayerEventType.SearchSubtitlesOnline
|
PlayerEventType.SearchSubtitlesOnline
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
|
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
|
||||||
PlayerEventType.ShowSpeed
|
PlayerEventType.ShowSpeed
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
|
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
|
||||||
PlayerEventType.Resize
|
PlayerEventType.Resize
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_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 -> {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.lagradost.cloudstream3.ui.HeaderViewDecoration
|
||||||
|
|
||||||
|
fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
|
||||||
|
val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
|
||||||
|
view.addItemDecoration(HeaderViewDecoration(headerView))
|
||||||
|
}
|
|
@ -18,11 +18,12 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.collections.MutableList
|
|
||||||
|
|
||||||
const val USER_AGENT =
|
const val USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||||
|
@ -31,6 +32,12 @@ const val USER_AGENT =
|
||||||
val mapper = JsonMapper.builder().addModule(KotlinModule())
|
val mapper = JsonMapper.builder().addModule(KotlinModule())
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the constant for the all languages preference, if this is set then it is
|
||||||
|
* the equivalent of all languages being set
|
||||||
|
**/
|
||||||
|
const val AllLanguagesName = "universal"
|
||||||
|
|
||||||
object APIHolder {
|
object APIHolder {
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
|
@ -39,7 +46,8 @@ object APIHolder {
|
||||||
|
|
||||||
private const val defProvider = 0
|
private const val defProvider = 0
|
||||||
|
|
||||||
val allProviders: MutableList<MainAPI> = arrayListOf()
|
// ConcurrentModificationException is possible!!!
|
||||||
|
val allProviders = threadSafeListOf<MainAPI>()
|
||||||
|
|
||||||
fun initAll() {
|
fun initAll() {
|
||||||
for (api in allProviders) {
|
for (api in allProviders) {
|
||||||
|
@ -52,7 +60,7 @@ object APIHolder {
|
||||||
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
|
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
|
||||||
}
|
}
|
||||||
|
|
||||||
var apis: List<MainAPI> = arrayListOf()
|
var apis: List<MainAPI> = threadSafeListOf()
|
||||||
var apiMap: Map<String, Int>? = null
|
var apiMap: Map<String, Int>? = null
|
||||||
|
|
||||||
fun addPluginMapping(plugin: MainAPI) {
|
fun addPluginMapping(plugin: MainAPI) {
|
||||||
|
@ -72,16 +80,20 @@ object APIHolder {
|
||||||
|
|
||||||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||||
if (apiName == null) return null
|
if (apiName == null) return null
|
||||||
initMap()
|
synchronized(allProviders) {
|
||||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
initMap()
|
||||||
?: allProviders.firstOrNull { it.name == apiName }
|
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||||
|
// Leave the ?. null check, it can crash regardless
|
||||||
|
?: allProviders.firstOrNull { it?.name == apiName }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApiFromUrlNull(url: String?): MainAPI? {
|
fun getApiFromUrlNull(url: String?): MainAPI? {
|
||||||
if (url == null) return null
|
if (url == null) return null
|
||||||
for (api in allProviders) {
|
synchronized(allProviders) {
|
||||||
if (url.startsWith(api.mainUrl))
|
allProviders.forEach { api ->
|
||||||
return api
|
if (url.startsWith(api.mainUrl)) return api
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -155,7 +167,9 @@ object APIHolder {
|
||||||
|
|
||||||
val hashSet = HashSet<String>()
|
val hashSet = HashSet<String>()
|
||||||
val activeLangs = getApiProviderLangSettings()
|
val activeLangs = getApiProviderLangSettings()
|
||||||
hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name })
|
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
||||||
|
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
|
||||||
|
.map { it.name })
|
||||||
|
|
||||||
/*val set = settingsManager.getStringSet(
|
/*val set = settingsManager.getStringSet(
|
||||||
this.getString(R.string.search_providers_list_key),
|
this.getString(R.string.search_providers_list_key),
|
||||||
|
@ -191,11 +205,11 @@ object APIHolder {
|
||||||
|
|
||||||
fun Context.getApiProviderLangSettings(): HashSet<String> {
|
fun Context.getApiProviderLangSettings(): HashSet<String> {
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val hashSet = HashSet<String>()
|
val hashSet = hashSetOf(AllLanguagesName) // def is all languages
|
||||||
hashSet.add("en") // def is only en
|
// hashSet.add("en") // def is only en
|
||||||
val list = settingsManager.getStringSet(
|
val list = settingsManager.getStringSet(
|
||||||
this.getString(R.string.provider_lang_key),
|
this.getString(R.string.provider_lang_key),
|
||||||
hashSet.toMutableSet()
|
hashSet
|
||||||
)
|
)
|
||||||
|
|
||||||
if (list.isNullOrEmpty()) return hashSet
|
if (list.isNullOrEmpty()) return hashSet
|
||||||
|
@ -225,13 +239,24 @@ object APIHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Context.getHasTrailers(): Boolean {
|
private fun Context.getHasTrailers(): Boolean {
|
||||||
if (isTvSettings()) return false
|
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
|
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
|
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
|
||||||
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
|
// We are getting the weirdest crash ever done:
|
||||||
|
// java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType
|
||||||
|
// Trying fixing using classloader fuckery
|
||||||
|
val oldLoader = Thread.currentThread().contextClassLoader
|
||||||
|
Thread.currentThread().contextClassLoader = TvType::class.java.classLoader
|
||||||
|
|
||||||
|
val default = TvType.values()
|
||||||
|
.sorted()
|
||||||
|
.filter { it != TvType.NSFW }
|
||||||
|
.map { it.ordinal }
|
||||||
|
|
||||||
|
Thread.currentThread().contextClassLoader = oldLoader
|
||||||
|
|
||||||
val defaultSet = default.map { it.toString() }.toSet()
|
val defaultSet = default.map { it.toString() }.toSet()
|
||||||
val currentPrefMedia = try {
|
val currentPrefMedia = try {
|
||||||
PreferenceManager.getDefaultSharedPreferences(this)
|
PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
@ -241,7 +266,8 @@ object APIHolder {
|
||||||
null
|
null
|
||||||
} ?: default
|
} ?: default
|
||||||
val langs = this.getApiProviderLangSettings()
|
val langs = this.getApiProviderLangSettings()
|
||||||
val allApis = apis.filter { langs.contains(it.lang) }
|
val hasUniversal = langs.contains(AllLanguagesName)
|
||||||
|
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
|
||||||
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
||||||
return if (currentPrefMedia.isEmpty()) {
|
return if (currentPrefMedia.isEmpty()) {
|
||||||
allApis
|
allApis
|
||||||
|
@ -322,13 +348,24 @@ data class SettingsJson(
|
||||||
data class MainPageData(
|
data class MainPageData(
|
||||||
val name: String,
|
val name: String,
|
||||||
val data: String,
|
val data: String,
|
||||||
|
val horizontalImages: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MainPageRequest(
|
data class MainPageRequest(
|
||||||
val name: String,
|
val name: String,
|
||||||
val data: String,
|
val data: String,
|
||||||
|
val horizontalImages: Boolean,
|
||||||
|
//TODO genre selection or smth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun mainPage(url: String, name: String, horizontalImages: Boolean = false): MainPageData {
|
||||||
|
return MainPageData(name = name, data = url, horizontalImages = horizontalImages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mainPageOf(vararg elements: MainPageData): List<MainPageData> {
|
||||||
|
return elements.toList()
|
||||||
|
}
|
||||||
|
|
||||||
/** return list of MainPageData with url to name, make for more readable code */
|
/** return list of MainPageData with url to name, make for more readable code */
|
||||||
fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> {
|
fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> {
|
||||||
return elements.map { (url, name) -> MainPageData(name = name, data = url) }
|
return elements.map { (url, name) -> MainPageData(name = name, data = url) }
|
||||||
|
@ -337,7 +374,7 @@ fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> {
|
||||||
fun newHomePageResponse(
|
fun newHomePageResponse(
|
||||||
name: String,
|
name: String,
|
||||||
list: List<SearchResponse>,
|
list: List<SearchResponse>,
|
||||||
hasNext: Boolean? = null
|
hasNext: Boolean? = null,
|
||||||
): HomePageResponse {
|
): HomePageResponse {
|
||||||
return HomePageResponse(
|
return HomePageResponse(
|
||||||
listOf(HomePageList(name, list)),
|
listOf(HomePageList(name, list)),
|
||||||
|
@ -345,6 +382,17 @@ fun newHomePageResponse(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun newHomePageResponse(
|
||||||
|
data: MainPageRequest,
|
||||||
|
list: List<SearchResponse>,
|
||||||
|
hasNext: Boolean? = null,
|
||||||
|
): HomePageResponse {
|
||||||
|
return HomePageResponse(
|
||||||
|
listOf(HomePageList(data.name, list, data.horizontalImages)),
|
||||||
|
hasNext = hasNext ?: list.isNotEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse {
|
fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse {
|
||||||
return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty())
|
return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty())
|
||||||
}
|
}
|
||||||
|
@ -379,7 +427,19 @@ abstract class MainAPI {
|
||||||
open var storedCredentials: String? = null
|
open var storedCredentials: String? = null
|
||||||
open var canBeOverridden: Boolean = true
|
open var canBeOverridden: Boolean = true
|
||||||
|
|
||||||
//open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id
|
/** if this is turned on then it will request the homepage one after the other,
|
||||||
|
used to delay if they block many request at the same time*/
|
||||||
|
open var sequentialMainPage: Boolean = false
|
||||||
|
|
||||||
|
/** in milliseconds, this can be used to add more delay between homepage requests
|
||||||
|
* on first load if sequentialMainPage is turned on */
|
||||||
|
open var sequentialMainPageDelay: Long = 0L
|
||||||
|
|
||||||
|
/** in milliseconds, this can be used to add more delay between homepage requests when scrolling */
|
||||||
|
open var sequentialMainPageScrollDelay: Long = 0L
|
||||||
|
|
||||||
|
/** used to keep track when last homepage request was in unixtime ms */
|
||||||
|
var lastHomepageRequest: Long = 0L
|
||||||
|
|
||||||
open var lang = "en" // ISO_639_1 check SubtitleHelper
|
open var lang = "en" // ISO_639_1 check SubtitleHelper
|
||||||
|
|
||||||
|
@ -425,7 +485,9 @@ abstract class MainAPI {
|
||||||
|
|
||||||
open val vpnStatus = VPNStatus.None
|
open val vpnStatus = VPNStatus.None
|
||||||
open val providerType = ProviderType.DirectProvider
|
open val providerType = ProviderType.DirectProvider
|
||||||
open val mainPage = listOf(MainPageData("", ""))
|
|
||||||
|
//emptyList<MainPageData>() //
|
||||||
|
open val mainPage = listOf(MainPageData("", "", false))
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
open suspend fun getMainPage(
|
open suspend fun getMainPage(
|
||||||
|
@ -1039,7 +1101,7 @@ interface LoadResponse {
|
||||||
) {
|
) {
|
||||||
if (!isTrailersEnabled || trailerUrls == null) return
|
if (!isTrailersEnabled || trailerUrls == null) return
|
||||||
trailers.addAll(trailerUrls.map { TrailerData(it, referer, addRaw) })
|
trailers.addAll(trailerUrls.map { TrailerData(it, referer, addRaw) })
|
||||||
/*val trailers = trailerUrls.filter { it.isNotBlank() }.apmap { trailerUrl ->
|
/*val trailers = trailerUrls.filter { it.isNotBlank() }.amap { trailerUrl ->
|
||||||
val links = arrayListOf<ExtractorLink>()
|
val links = arrayListOf<ExtractorLink>()
|
||||||
val subs = arrayListOf<SubtitleFile>()
|
val subs = arrayListOf<SubtitleFile>()
|
||||||
if (!loadExtractor(
|
if (!loadExtractor(
|
||||||
|
@ -1100,18 +1162,43 @@ interface LoadResponse {
|
||||||
|
|
||||||
fun getDurationFromString(input: String?): Int? {
|
fun getDurationFromString(input: String?): Int? {
|
||||||
val cleanInput = input?.trim()?.replace(" ", "") ?: return null
|
val cleanInput = input?.trim()?.replace(" ", "") ?: return null
|
||||||
|
//Use first as sometimes the text passes on the 2 other Regex, but failed to provide accurate return value
|
||||||
|
Regex("(\\d+\\shr)|(\\d+\\shour)|(\\d+\\smin)|(\\d+\\ssec)").findAll(input).let { values ->
|
||||||
|
var seconds = 0
|
||||||
|
values.forEach {
|
||||||
|
val time_text = it.value
|
||||||
|
if (time_text.isNotBlank()) {
|
||||||
|
val time = time_text.filter { s -> s.isDigit() }.trim().toInt()
|
||||||
|
val scale = time_text.filter { s -> !s.isDigit() }.trim()
|
||||||
|
//println("Scale: $scale")
|
||||||
|
val timeval = when (scale) {
|
||||||
|
"hr", "hour" -> time * 60 * 60
|
||||||
|
"min" -> time * 60
|
||||||
|
"sec" -> time
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
seconds += timeval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (seconds > 0) {
|
||||||
|
return seconds / 60
|
||||||
|
}
|
||||||
|
}
|
||||||
Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
|
Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
|
||||||
if (values.size == 3) {
|
if (values.size == 3) {
|
||||||
val hours = values[1].toIntOrNull()
|
val hours = values[1].toIntOrNull()
|
||||||
val minutes = values[2].toIntOrNull()
|
val minutes = values[2].toIntOrNull()
|
||||||
return if (minutes != null && hours != null) {
|
if (minutes != null && hours != null) {
|
||||||
hours * 60 + minutes
|
return hours * 60 + minutes
|
||||||
} else null
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
|
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
|
||||||
if (values.size == 2) {
|
if (values.size == 2) {
|
||||||
return values[1].toIntOrNull()
|
val return_value = values[1].toIntOrNull()
|
||||||
|
if (return_value != null) {
|
||||||
|
return return_value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
@ -1138,6 +1225,11 @@ data class NextAiring(
|
||||||
val unixTime: Long,
|
val unixTime: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined
|
||||||
|
* @param name To be shown next to the season like "Season $displaySeason $name" but if displaySeason is null then "$name"
|
||||||
|
* @param displaySeason What to be displayed next to the season name, if null then the name is the only thing shown.
|
||||||
|
* */
|
||||||
data class SeasonData(
|
data class SeasonData(
|
||||||
val season: Int,
|
val season: Int,
|
||||||
val name: String? = null,
|
val name: String? = null,
|
||||||
|
@ -1218,9 +1310,12 @@ data class AnimeLoadResponse(
|
||||||
override var backgroundPosterUrl: String? = null,
|
override var backgroundPosterUrl: String? = null,
|
||||||
) : LoadResponse, EpisodeResponse
|
) : LoadResponse, EpisodeResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If episodes already exist appends the list.
|
||||||
|
* */
|
||||||
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
|
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List<Episode>?) {
|
||||||
if (episodes.isNullOrEmpty()) return
|
if (episodes.isNullOrEmpty()) return
|
||||||
this.episodes[status] = episodes
|
this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun MainAPI.newAnimeLoadResponse(
|
suspend fun MainAPI.newAnimeLoadResponse(
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.AttributeSet
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.*
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.annotation.IdRes
|
import androidx.annotation.IdRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
|
@ -27,6 +30,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
import com.google.android.gms.cast.framework.*
|
import com.google.android.gms.cast.framework.*
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.navigationrail.NavigationRailView
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
|
||||||
import com.lagradost.cloudstream3.APIHolder.allProviders
|
import com.lagradost.cloudstream3.APIHolder.allProviders
|
||||||
|
@ -34,77 +38,167 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.initAll
|
import com.lagradost.cloudstream3.APIHolder.initAll
|
||||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
import com.lagradost.cloudstream3.CommonActivity.loadThemes
|
||||||
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
||||||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||||
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.*
|
||||||
import com.lagradost.cloudstream3.network.initClient
|
import com.lagradost.cloudstream3.network.initClient
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
|
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
|
||||||
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.home.HomeViewModel
|
||||||
|
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
||||||
|
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setImage
|
||||||
|
import com.lagradost.cloudstream3.ui.result.setText
|
||||||
|
import com.lagradost.cloudstream3.ui.search.SearchFragment
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
|
||||||
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
|
||||||
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
|
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
|
||||||
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
|
||||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||||
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
|
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
|
||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.ResponseParser
|
import com.lagradost.nicehttp.ResponseParser
|
||||||
import kotlinx.android.synthetic.main.activity_main.*
|
import kotlinx.android.synthetic.main.activity_main.*
|
||||||
|
import kotlinx.android.synthetic.main.bottom_resultview_preview.*
|
||||||
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
import kotlinx.android.synthetic.main.fragment_result_swipe.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.net.URLDecoder
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
|
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
|
||||||
|
//https://wiki.videolan.org/Android_Player_Intents/
|
||||||
|
|
||||||
|
//https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
|
||||||
|
//https://mpv-android.github.io/mpv-android/intent.html
|
||||||
|
|
||||||
|
// https://www.webvideocaster.com/integrations
|
||||||
|
|
||||||
|
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
|
||||||
|
|
||||||
const val VLC_PACKAGE = "org.videolan.vlc"
|
const val VLC_PACKAGE = "org.videolan.vlc"
|
||||||
const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
|
const val MPV_PACKAGE = "is.xyz.mpv"
|
||||||
val VLC_COMPONENT: ComponentName =
|
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
|
||||||
ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
|
|
||||||
const val VLC_REQUEST_CODE = 42
|
|
||||||
|
|
||||||
const val VLC_FROM_START = -1
|
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
|
||||||
const val VLC_FROM_PROGRESS = -2
|
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
|
||||||
const val VLC_EXTRA_POSITION_OUT = "extra_position"
|
|
||||||
const val VLC_EXTRA_DURATION_OUT = "extra_duration"
|
//TODO REFACTOR AF
|
||||||
const val VLC_LAST_ID_KEY = "vlc_last_open_id"
|
open class ResultResume(
|
||||||
|
val packageString: String,
|
||||||
|
val action: String = Intent.ACTION_VIEW,
|
||||||
|
val position: String? = null,
|
||||||
|
val duration: String? = null,
|
||||||
|
var launcher: ActivityResultLauncher<Intent>? = null,
|
||||||
|
) {
|
||||||
|
val defaultTime = -1L
|
||||||
|
|
||||||
|
val lastId get() = "${packageString}_last_open_id"
|
||||||
|
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
|
||||||
|
val intent = Intent(action)
|
||||||
|
|
||||||
|
if (id != null)
|
||||||
|
setKey(lastId, id)
|
||||||
|
else
|
||||||
|
removeKey(lastId)
|
||||||
|
|
||||||
|
intent.setPackage(packageString)
|
||||||
|
callback.invoke(intent)
|
||||||
|
launcher?.launch(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getPosition(intent: Intent?): Long {
|
||||||
|
return defaultTime
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun getDuration(intent: Intent?): Long {
|
||||||
|
return defaultTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val VLC = object : ResultResume(
|
||||||
|
VLC_PACKAGE,
|
||||||
|
"org.videolan.vlc.player.result",
|
||||||
|
"extra_position",
|
||||||
|
"extra_duration",
|
||||||
|
) {
|
||||||
|
override fun getPosition(intent: Intent?): Long {
|
||||||
|
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDuration(intent: Intent?): Long {
|
||||||
|
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MPV = object : ResultResume(
|
||||||
|
MPV_PACKAGE,
|
||||||
|
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
|
||||||
|
position = "position",
|
||||||
|
duration = "duration",
|
||||||
|
) {
|
||||||
|
override fun getPosition(intent: Intent?): Long {
|
||||||
|
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDuration(intent: Intent?): Long {
|
||||||
|
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
|
||||||
|
|
||||||
|
val resumeApps = arrayOf(
|
||||||
|
VLC, MPV, WEB_VIDEO
|
||||||
|
)
|
||||||
|
|
||||||
// Short name for requests client to make it nicer to use
|
// Short name for requests client to make it nicer to use
|
||||||
|
|
||||||
|
@ -137,13 +231,130 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "MAINACT"
|
const val TAG = "MAINACT"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setting this will automatically enter the query in the search
|
||||||
|
* next time the search fragment is opened.
|
||||||
|
* This variable will clear itself after one use. Null does nothing.
|
||||||
|
*
|
||||||
|
* This is a very bad solution but I was unable to find a better one.
|
||||||
|
**/
|
||||||
|
private var nextSearchQuery: String? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
|
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
|
||||||
|
* Boolean signifies if stuff should be force reloaded (true if force reload, false if reload when necessary).
|
||||||
|
*
|
||||||
|
* The force reloading are used for plugin development to instantly reload the page on deployWithAdb
|
||||||
* */
|
* */
|
||||||
val afterPluginsLoadedEvent = Event<Boolean>()
|
val afterPluginsLoadedEvent = Event<Boolean>()
|
||||||
val mainPluginsLoadedEvent =
|
val mainPluginsLoadedEvent =
|
||||||
Event<Boolean>() // homepage api, used to speed up time to load for homepage
|
Event<Boolean>() // homepage api, used to speed up time to load for homepage
|
||||||
val afterRepositoryLoadedEvent = Event<Boolean>()
|
val afterRepositoryLoadedEvent = Event<Boolean>()
|
||||||
|
|
||||||
|
// kinda shitty solution, but cant com main->home otherwise for popups
|
||||||
|
val bookmarksUpdatedEvent = Event<Boolean>()
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if the str has launched an app task (be it successful or not)
|
||||||
|
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
|
||||||
|
* */
|
||||||
|
fun handleAppIntentUrl(
|
||||||
|
activity: FragmentActivity?,
|
||||||
|
str: String?,
|
||||||
|
isWebview: Boolean
|
||||||
|
): Boolean =
|
||||||
|
with(activity) {
|
||||||
|
// Invalid URIs can crash
|
||||||
|
fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
|
||||||
|
|
||||||
|
if (str != null && this != null) {
|
||||||
|
if (str.startsWith("https://cs.repo")) {
|
||||||
|
val realUrl = "https://" + str.substringAfter("?")
|
||||||
|
println("Repository url: $realUrl")
|
||||||
|
loadRepository(realUrl)
|
||||||
|
return true
|
||||||
|
} else if (str.contains(appString)) {
|
||||||
|
for (api in OAuth2Apis) {
|
||||||
|
if (str.contains("/${api.redirectUrl}")) {
|
||||||
|
ioSafe {
|
||||||
|
Log.i(TAG, "handleAppIntent $str")
|
||||||
|
val isSuccessful = api.handleRedirect(str)
|
||||||
|
|
||||||
|
if (isSuccessful) {
|
||||||
|
Log.i(TAG, "authenticated ${api.name}")
|
||||||
|
} else {
|
||||||
|
Log.i(TAG, "failed to authenticate ${api.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
this@with.runOnUiThread {
|
||||||
|
try {
|
||||||
|
showToast(
|
||||||
|
this@with,
|
||||||
|
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||||
|
api.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e) // format might fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This specific intent is used for the gradle deployWithAdb
|
||||||
|
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
|
||||||
|
if (str == "$appString:") {
|
||||||
|
PluginManager.hotReloadAllLocalPlugins(activity)
|
||||||
|
}
|
||||||
|
} else if (safeURI(str)?.scheme == appStringRepo) {
|
||||||
|
val url = str.replaceFirst(appStringRepo, "https")
|
||||||
|
loadRepository(url)
|
||||||
|
return true
|
||||||
|
} else if (safeURI(str)?.scheme == appStringSearch) {
|
||||||
|
nextSearchQuery =
|
||||||
|
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
|
||||||
|
nav_view.selectedItemId = R.id.navigation_search
|
||||||
|
} else if (safeURI(str)?.scheme == appStringResumeWatching) {
|
||||||
|
val id =
|
||||||
|
str.substringAfter("$appStringResumeWatching://").toIntOrNull()
|
||||||
|
?: return false
|
||||||
|
ioSafe {
|
||||||
|
val resumeWatchingCard =
|
||||||
|
HomeViewModel.getResumeWatching()?.firstOrNull { it.id == id }
|
||||||
|
?: return@ioSafe
|
||||||
|
activity.loadSearchResult(
|
||||||
|
resumeWatchingCard,
|
||||||
|
START_ACTION_RESUME_LATEST
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!isWebview) {
|
||||||
|
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
||||||
|
this.navigate(R.id.navigation_downloads)
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
for (api in apis) {
|
||||||
|
if (str.startsWith(api.mainUrl)) {
|
||||||
|
loadResult(str, api.name)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastPopup : SearchResponse? = null
|
||||||
|
fun loadPopup(result: SearchResponse) {
|
||||||
|
lastPopup = result
|
||||||
|
viewModel.load(
|
||||||
|
this, result.url, result.apiName, false, if (getApiDubstatusSettings()
|
||||||
|
.contains(DubStatus.Dubbed)
|
||||||
|
) DubStatus.Dubbed else DubStatus.Subbed, null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onColorSelected(dialogId: Int, color: Int) {
|
override fun onColorSelected(dialogId: Int, color: Int) {
|
||||||
|
@ -193,6 +404,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
R.id.navigation_settings_plugins,
|
R.id.navigation_settings_plugins,
|
||||||
).contains(destination.id)
|
).contains(destination.id)
|
||||||
|
|
||||||
|
|
||||||
|
val dontPush = listOf(
|
||||||
|
R.id.navigation_home,
|
||||||
|
R.id.navigation_search,
|
||||||
|
R.id.navigation_results_phone,
|
||||||
|
R.id.navigation_results_tv,
|
||||||
|
R.id.navigation_player,
|
||||||
|
).contains(destination.id)
|
||||||
|
|
||||||
|
nav_host_fragment?.apply {
|
||||||
|
val params = layoutParams as ConstraintLayout.LayoutParams
|
||||||
|
|
||||||
|
params.setMargins(
|
||||||
|
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
|
||||||
|
params.topMargin,
|
||||||
|
params.rightMargin,
|
||||||
|
params.bottomMargin
|
||||||
|
)
|
||||||
|
layoutParams = params
|
||||||
|
}
|
||||||
|
|
||||||
val landscape = when (resources.configuration.orientation) {
|
val landscape = when (resources.configuration.orientation) {
|
||||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||||
true
|
true
|
||||||
|
@ -259,6 +491,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
|
// Start any delayed updates
|
||||||
|
if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
|
||||||
|
Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (isCastApiAvailable()) {
|
if (isCastApiAvailable()) {
|
||||||
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
|
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
|
||||||
|
@ -289,12 +526,34 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
onUserLeaveHint(this)
|
onUserLeaveHint(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showConfirmExitDialog() {
|
||||||
|
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
|
||||||
|
builder.setTitle(R.string.confirm_exit_dialog)
|
||||||
|
builder.apply {
|
||||||
|
// Forceful exit since back button can actually go back to setup
|
||||||
|
setPositiveButton(R.string.yes) { _, _ -> exitProcess(0) }
|
||||||
|
setNegativeButton(R.string.no) { _, _ -> }
|
||||||
|
}
|
||||||
|
builder.show().setDefaultFocus()
|
||||||
|
}
|
||||||
|
|
||||||
private fun backPressed() {
|
private fun backPressed() {
|
||||||
this.window?.navigationBarColor =
|
this.window?.navigationBarColor =
|
||||||
this.colorFromAttribute(R.attr.primaryGrayBackground)
|
this.colorFromAttribute(R.attr.primaryGrayBackground)
|
||||||
this.updateLocale()
|
this.updateLocale()
|
||||||
super.onBackPressed()
|
|
||||||
this.updateLocale()
|
this.updateLocale()
|
||||||
|
|
||||||
|
val navHostFragment =
|
||||||
|
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
|
||||||
|
val navController = navHostFragment?.navController
|
||||||
|
val isAtHome =
|
||||||
|
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
|
||||||
|
|
||||||
|
if (isAtHome && isTrueTvSettings()) {
|
||||||
|
showConfirmExitDialog()
|
||||||
|
} else {
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
@ -306,31 +565,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
if (requestCode == VLC_REQUEST_CODE) {
|
|
||||||
if (resultCode == RESULT_OK && data != null) {
|
|
||||||
val pos: Long =
|
|
||||||
data.getLongExtra(
|
|
||||||
VLC_EXTRA_POSITION_OUT,
|
|
||||||
-1
|
|
||||||
) //Last position in media when player exited
|
|
||||||
val dur: Long =
|
|
||||||
data.getLongExtra(
|
|
||||||
VLC_EXTRA_DURATION_OUT,
|
|
||||||
-1
|
|
||||||
) //Last position in media when player exited
|
|
||||||
val id = getKey<Int>(VLC_LAST_ID_KEY)
|
|
||||||
println("SET KEY $id at $pos / $dur")
|
|
||||||
if (dur > 0 && pos > 0) {
|
|
||||||
setViewPos(id, pos, dur)
|
|
||||||
}
|
|
||||||
removeKey(VLC_LAST_ID_KEY)
|
|
||||||
ResultFragment.updateUI()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
val broadcastIntent = Intent()
|
val broadcastIntent = Intent()
|
||||||
broadcastIntent.action = "restart_service"
|
broadcastIntent.action = "restart_service"
|
||||||
|
@ -349,56 +583,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
val str = intent.dataString
|
val str = intent.dataString
|
||||||
loadCache()
|
loadCache()
|
||||||
if (str != null) {
|
handleAppIntentUrl(this, str, false)
|
||||||
if (str.startsWith("https://cs.repo")) {
|
|
||||||
val realUrl = "https://" + str.substringAfter("?")
|
|
||||||
println("Repository url: $realUrl")
|
|
||||||
loadRepository(realUrl)
|
|
||||||
} else if (str.contains(appString)) {
|
|
||||||
for (api in OAuth2Apis) {
|
|
||||||
if (str.contains("/${api.redirectUrl}")) {
|
|
||||||
val activity = this
|
|
||||||
ioSafe {
|
|
||||||
Log.i(TAG, "handleAppIntent $str")
|
|
||||||
val isSuccessful = api.handleRedirect(str)
|
|
||||||
|
|
||||||
if (isSuccessful) {
|
|
||||||
Log.i(TAG, "authenticated ${api.name}")
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "failed to authenticate ${api.name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.runOnUiThread {
|
|
||||||
try {
|
|
||||||
showToast(
|
|
||||||
activity,
|
|
||||||
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
|
||||||
api.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logError(e) // format might fail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (URI(str).scheme == appStringRepo) {
|
|
||||||
val url = str.replaceFirst(appStringRepo, "https")
|
|
||||||
loadRepository(url)
|
|
||||||
} else {
|
|
||||||
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
|
|
||||||
this.navigate(R.id.navigation_downloads)
|
|
||||||
} else {
|
|
||||||
for (api in apis) {
|
|
||||||
if (str.startsWith(api.mainUrl)) {
|
|
||||||
loadResult(str, api.name)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
|
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
|
||||||
|
@ -446,7 +631,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// it.hashCode() is not enough to make sure they are distinct
|
// it.hashCode() is not enough to make sure they are distinct
|
||||||
apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
apis =
|
||||||
|
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||||
APIHolder.apiMap = null
|
APIHolder.apiMap = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
|
@ -455,6 +641,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lateinit var viewModel: ResultViewModel2
|
||||||
|
|
||||||
|
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||||
|
viewModel =
|
||||||
|
ViewModelProvider(this)[ResultViewModel2::class.java]
|
||||||
|
|
||||||
|
return super.onCreateView(name, context, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hidePreviewPopupDialog() {
|
||||||
|
viewModel.clear()
|
||||||
|
bottomPreviewPopup.dismissSafe(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bottomPreviewPopup: BottomSheetDialog? = null
|
||||||
|
private fun showPreviewPopupDialog(): BottomSheetDialog {
|
||||||
|
val ret = (bottomPreviewPopup ?: run {
|
||||||
|
val builder =
|
||||||
|
BottomSheetDialog(this)
|
||||||
|
builder.setContentView(R.layout.bottom_resultview_preview)
|
||||||
|
builder.setOnDismissListener {
|
||||||
|
bottomPreviewPopup = null
|
||||||
|
viewModel.clear()
|
||||||
|
}
|
||||||
|
builder.setCanceledOnTouchOutside(true)
|
||||||
|
builder.show()
|
||||||
|
builder
|
||||||
|
})
|
||||||
|
bottomPreviewPopup = ret
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
app.initClient(this)
|
app.initClient(this)
|
||||||
|
@ -468,7 +685,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
val settingsForProvider = SettingsJson()
|
val settingsForProvider = SettingsJson()
|
||||||
settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
|
settingsForProvider.enableAdult =
|
||||||
|
settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
|
||||||
|
|
||||||
MainAPI.settingsForProvider = settingsForProvider
|
MainAPI.settingsForProvider = settingsForProvider
|
||||||
|
|
||||||
|
@ -484,7 +702,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||||
|
updateTv()
|
||||||
if (isTvSettings()) {
|
if (isTvSettings()) {
|
||||||
setContentView(R.layout.activity_main_tv)
|
setContentView(R.layout.activity_main_tv)
|
||||||
} else {
|
} else {
|
||||||
|
@ -502,15 +720,28 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
ioSafe {
|
ioSafe {
|
||||||
if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) {
|
if (settingsManager.getBoolean(
|
||||||
|
getString(R.string.auto_update_plugins_key),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
) {
|
||||||
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
|
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
|
||||||
} else {
|
} else {
|
||||||
PluginManager.loadAllOnlinePlugins(this@MainActivity)
|
loadAllOnlinePlugins(this@MainActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Automatically download not existing plugins
|
||||||
|
if (settingsManager.getBoolean(
|
||||||
|
getString(R.string.auto_download_plugins_key),
|
||||||
|
false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ioSafe {
|
ioSafe {
|
||||||
PluginManager.loadAllLocalPlugins(this@MainActivity)
|
PluginManager.loadAllLocalPlugins(this@MainActivity, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -527,9 +758,81 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
|
|
||||||
setNegativeButton("Ok") { _, _ -> }
|
setNegativeButton("Ok") { _, _ -> }
|
||||||
}
|
}
|
||||||
builder.show()
|
builder.show().setDefaultFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeNullable(viewModel.page) { resource ->
|
||||||
|
if (resource == null) {
|
||||||
|
bottomPreviewPopup.dismissSafe(this)
|
||||||
|
return@observeNullable
|
||||||
|
}
|
||||||
|
when (resource) {
|
||||||
|
is Resource.Failure -> {
|
||||||
|
showToast(this, R.string.error)
|
||||||
|
hidePreviewPopupDialog()
|
||||||
|
}
|
||||||
|
is Resource.Loading -> {
|
||||||
|
showPreviewPopupDialog().apply {
|
||||||
|
resultview_preview_loading?.isVisible = true
|
||||||
|
resultview_preview_result?.isVisible = false
|
||||||
|
resultview_preview_loading_shimmer?.startShimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Resource.Success -> {
|
||||||
|
val d = resource.value
|
||||||
|
showPreviewPopupDialog().apply {
|
||||||
|
resultview_preview_loading?.isVisible = false
|
||||||
|
resultview_preview_result?.isVisible = true
|
||||||
|
resultview_preview_loading_shimmer?.stopShimmer()
|
||||||
|
|
||||||
|
resultview_preview_title?.text = d.title
|
||||||
|
|
||||||
|
resultview_preview_meta_type.setText(d.typeText)
|
||||||
|
resultview_preview_meta_year.setText(d.yearText)
|
||||||
|
resultview_preview_meta_duration.setText(d.durationText)
|
||||||
|
resultview_preview_meta_rating.setText(d.ratingText)
|
||||||
|
|
||||||
|
resultview_preview_description?.setText(d.plotText)
|
||||||
|
resultview_preview_poster?.setImage(
|
||||||
|
d.posterImage ?: d.posterBackgroundImage
|
||||||
|
)
|
||||||
|
|
||||||
|
resultview_preview_poster?.setOnClickListener {
|
||||||
|
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
|
||||||
|
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
||||||
|
|
||||||
|
this@MainActivity.showBottomDialog(
|
||||||
|
WatchType.values().map { getString(it.stringRes) }.toList(),
|
||||||
|
value.ordinal,
|
||||||
|
this@MainActivity.getString(R.string.action_add_to_bookmarks),
|
||||||
|
showApply = false,
|
||||||
|
{}) {
|
||||||
|
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||||
|
bookmarksUpdatedEvent(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTvSettings()) // dont want this clickable on tv layout
|
||||||
|
resultview_preview_description?.setOnClickListener { view ->
|
||||||
|
view.context?.let { ctx ->
|
||||||
|
val builder: AlertDialog.Builder =
|
||||||
|
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||||
|
builder.setMessage(d.plotText.asString(ctx).html())
|
||||||
|
.setTitle(d.plotHeaderText.asString(ctx))
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resultview_preview_more_info?.setOnClickListener {
|
||||||
|
hidePreviewPopupDialog()
|
||||||
|
lastPopup?.let {
|
||||||
|
loadSearchResult(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ioSafe {
|
// ioSafe {
|
||||||
// val plugins =
|
// val plugins =
|
||||||
|
@ -546,10 +849,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
for (api in accountManagers) {
|
for (api in accountManagers) {
|
||||||
api.init()
|
api.init()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ioSafe {
|
inAppAuths.amap { api ->
|
||||||
inAppAuths.apmap { api ->
|
|
||||||
try {
|
try {
|
||||||
api.initialize()
|
api.initialize()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -573,6 +874,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
val navHostFragment =
|
val navHostFragment =
|
||||||
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
|
||||||
val navController = navHostFragment.navController
|
val navController = navHostFragment.navController
|
||||||
|
|
||||||
|
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
|
||||||
|
// Intercept search and add a query
|
||||||
|
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
|
||||||
|
bundle?.apply {
|
||||||
|
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
|
||||||
|
nextSearchQuery = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//val navController = findNavController(R.id.nav_host_fragment)
|
//val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
|
||||||
/*navOptions = NavOptions.Builder()
|
/*navOptions = NavOptions.Builder()
|
||||||
|
@ -586,7 +898,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
nav_view?.setupWithNavController(navController)
|
nav_view?.setupWithNavController(navController)
|
||||||
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
|
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
|
||||||
nav_rail?.setupWithNavController(navController)
|
nav_rail?.setupWithNavController(navController)
|
||||||
|
if (isTvSettings()) {
|
||||||
|
nav_rail?.background?.alpha = 200
|
||||||
|
} else {
|
||||||
|
nav_rail?.background?.alpha = 255
|
||||||
|
|
||||||
|
}
|
||||||
nav_rail?.setOnItemSelectedListener { item ->
|
nav_rail?.setOnItemSelectedListener { item ->
|
||||||
onNavDestinationSelected(
|
onNavDestinationSelected(
|
||||||
item,
|
item,
|
||||||
|
@ -755,8 +1072,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
||||||
// Used to check current focus for TV
|
// Used to check current focus for TV
|
||||||
// main {
|
// main {
|
||||||
// while (true) {
|
// while (true) {
|
||||||
// delay(1000)
|
// delay(5000)
|
||||||
// println("Current focus: $currentFocus")
|
// println("Current focus: $currentFocus")
|
||||||
|
// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
|
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
|
||||||
/*
|
/*
|
||||||
|
@ -26,10 +25,25 @@ fun <T, R> Iterable<T>.pmap(
|
||||||
return ArrayList<R>(destination)
|
return ArrayList<R>(destination)
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
suspend fun <K, V, R> Map<out K, V>.amap(f: suspend (Map.Entry<K, V>) -> R): List<R> =
|
||||||
|
with(CoroutineScope(GlobalScope.coroutineContext)) {
|
||||||
|
map { async { f(it) } }.map { it.await() }
|
||||||
|
}
|
||||||
|
|
||||||
fun <K, V, R> Map<out K, V>.apmap(f: suspend (Map.Entry<K, V>) -> R): List<R> = runBlocking {
|
fun <K, V, R> Map<out K, V>.apmap(f: suspend (Map.Entry<K, V>) -> R): List<R> = runBlocking {
|
||||||
map { async { f(it) } }.map { it.await() }
|
map { async { f(it) } }.map { it.await() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
suspend fun <A, B> List<A>.amap(f: suspend (A) -> B): List<B> =
|
||||||
|
with(CoroutineScope(GlobalScope.coroutineContext)) {
|
||||||
|
map { async { f(it) } }.map { it.await() }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking {
|
fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking {
|
||||||
map { async { f(it) } }.map { it.await() }
|
map { async { f(it) } }.map { it.await() }
|
||||||
}
|
}
|
||||||
|
@ -38,6 +52,12 @@ fun <A, B> List<A>.apmapIndexed(f: suspend (index: Int, A) -> B): List<B> = runB
|
||||||
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
|
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
suspend fun <A, B> List<A>.amapIndexed(f: suspend (index: Int, A) -> B): List<B> =
|
||||||
|
with(CoroutineScope(GlobalScope.coroutineContext)) {
|
||||||
|
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
|
||||||
|
}
|
||||||
|
|
||||||
// run code in parallel
|
// run code in parallel
|
||||||
/*fun <R> argpmap(
|
/*fun <R> argpmap(
|
||||||
vararg transforms: () -> R,
|
vararg transforms: () -> R,
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
open class AStreamHub : ExtractorApi() {
|
||||||
|
override val name = "AStreamHub"
|
||||||
|
override val mainUrl = "https://astreamhub.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
app.get(url).document.selectFirst("body > script").let { script ->
|
||||||
|
val text = script?.html() ?: ""
|
||||||
|
Log.i("Dev", "text => $text")
|
||||||
|
if (text.isNotBlank()) {
|
||||||
|
val m3link = "(?<=file:)(.*)(?=,)".toRegex().find(text)
|
||||||
|
?.groupValues?.get(0)?.trim()?.trim('"') ?: ""
|
||||||
|
Log.i("Dev", "m3link => $m3link")
|
||||||
|
if (m3link.isNotBlank()) {
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name = name,
|
||||||
|
source = name,
|
||||||
|
url = m3link,
|
||||||
|
isM3u8 = true,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
referer = referer ?: url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64Decode
|
import com.lagradost.cloudstream3.base64Decode
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
class Acefile : ExtractorApi() {
|
open class Acefile : ExtractorApi() {
|
||||||
override val name = "Acefile"
|
override val name = "Acefile"
|
||||||
override val mainUrl = "https://acefile.co"
|
override val mainUrl = "https://acefile.co"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
@ -27,7 +27,6 @@ class Acefile : ExtractorApi() {
|
||||||
res.substringAfter("\"file\":\"").substringBefore("\","),
|
res.substringAfter("\"file\":\"").substringBefore("\","),
|
||||||
"$mainUrl/",
|
"$mainUrl/",
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
headers = mapOf("range" to "bytes=0-")
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
class AsianLoad : ExtractorApi() {
|
open class AsianLoad : ExtractorApi() {
|
||||||
override var name = "AsianLoad"
|
override var name = "AsianLoad"
|
||||||
override var mainUrl = "https://asianembed.io"
|
override var mainUrl = "https://asianembed.io"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
|
@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
class Blogger : ExtractorApi() {
|
open class Blogger : ExtractorApi() {
|
||||||
override val name = "Blogger"
|
override val name = "Blogger"
|
||||||
override val mainUrl = "https://www.blogger.com"
|
override val mainUrl = "https://www.blogger.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -5,7 +5,7 @@ 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
|
||||||
|
|
||||||
class BullStream : ExtractorApi() {
|
open class BullStream : ExtractorApi() {
|
||||||
override val name = "BullStream"
|
override val name = "BullStream"
|
||||||
override val mainUrl = "https://bullstream.xyz"
|
override val mainUrl = "https://bullstream.xyz"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
@ -18,7 +18,7 @@ class BullStream : ExtractorApi() {
|
||||||
?: return null
|
?: return null
|
||||||
|
|
||||||
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
|
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
|
||||||
println("shiv : $m3u8")
|
//println("shiv : $m3u8")
|
||||||
return M3u8Helper.generateM3u8(
|
return M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
m3u8,
|
m3u8,
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
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
|
||||||
|
|
||||||
|
open class Cda: ExtractorApi() {
|
||||||
|
override var mainUrl = "https://ebd.cda.pl"
|
||||||
|
override var name = "Cda"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
|
val mediaId = url
|
||||||
|
.split("/").last()
|
||||||
|
.split("?").first()
|
||||||
|
val doc = app.get("https://ebd.cda.pl/647x500/$mediaId", headers=mapOf(
|
||||||
|
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
|
||||||
|
"User-Agent" to USER_AGENT,
|
||||||
|
"Cookie" to "cda.player=html5"
|
||||||
|
)).document
|
||||||
|
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
|
||||||
|
val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null
|
||||||
|
return listOf(ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
getFile(playerData.video.file),
|
||||||
|
referer = "https://ebd.cda.pl/647x500/$mediaId",
|
||||||
|
quality = Qualities.Unknown.value
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rot13(a: String): String {
|
||||||
|
return a.map {
|
||||||
|
when {
|
||||||
|
it in 'A'..'M' || it in 'a'..'m' -> it + 13
|
||||||
|
it in 'N'..'Z' || it in 'n'..'z' -> it - 13
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}.joinToString("")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cdaUggc(a: String): String {
|
||||||
|
val decoded = rot13(a)
|
||||||
|
return if (decoded.endsWith("adc.mp4")) decoded.replace("adc.mp4",".mp4")
|
||||||
|
else decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cdaDecrypt(b: String): String {
|
||||||
|
var a = b
|
||||||
|
.replace("_XDDD", "")
|
||||||
|
.replace("_CDA", "")
|
||||||
|
.replace("_ADC", "")
|
||||||
|
.replace("_CXD", "")
|
||||||
|
.replace("_QWE", "")
|
||||||
|
.replace("_Q5", "")
|
||||||
|
.replace("_IKSDE", "")
|
||||||
|
a = URLDecoder.decode(a, "UTF-8")
|
||||||
|
a = a.map { char ->
|
||||||
|
if (32 < char.toInt() && char.toInt() < 127) {
|
||||||
|
return@map String.format("%c", 33 + (char.toInt() + 14) % 94)
|
||||||
|
} else {
|
||||||
|
return@map char
|
||||||
|
}
|
||||||
|
}.joinToString("")
|
||||||
|
a = a
|
||||||
|
.replace(".cda.mp4", "")
|
||||||
|
.replace(".2cda.pl", ".cda.pl")
|
||||||
|
.replace(".3cda.pl", ".cda.pl")
|
||||||
|
return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream")
|
||||||
|
else "https://${a}.mp4"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFile(a: String) = when {
|
||||||
|
a.startsWith("uggc") -> cdaUggc(a)
|
||||||
|
!a.startsWith("http") -> cdaDecrypt(a)
|
||||||
|
else -> a
|
||||||
|
}
|
||||||
|
|
||||||
|
data class VideoPlayerData(
|
||||||
|
val file: String,
|
||||||
|
val qualities: Map<String, String> = mapOf(),
|
||||||
|
val quality: String?,
|
||||||
|
val ts: Int?,
|
||||||
|
val hash2: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PlayerData(
|
||||||
|
val video: VideoPlayerData
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
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.AppUtils.tryParseJson
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import java.net.URL
|
||||||
|
|
||||||
|
open class Dailymotion : ExtractorApi() {
|
||||||
|
override val mainUrl = "https://www.dailymotion.com"
|
||||||
|
override val name = "Dailymotion"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
@Suppress("RegExpSimplifiable")
|
||||||
|
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||||
|
|
||||||
|
// https://www.dailymotion.com/video/k3JAHfletwk94ayCVIu
|
||||||
|
// https://www.dailymotion.com/embed/video/k3JAHfletwk94ayCVIu
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val embedUrl = getEmbedUrl(url) ?: return
|
||||||
|
val doc = app.get(embedUrl).document
|
||||||
|
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||||
|
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||||
|
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
||||||
|
val id = getVideoId(embedUrl) ?: return
|
||||||
|
val dmV1st = config.dmInternalData.v1st
|
||||||
|
val dmTs = config.dmInternalData.ts
|
||||||
|
val metaDataUrl =
|
||||||
|
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||||
|
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
|
||||||
|
metaData.qualities.forEach { (key, video) ->
|
||||||
|
video.forEach {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
"$name $key",
|
||||||
|
it.url,
|
||||||
|
"",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEmbedUrl(url: String): String? {
|
||||||
|
if (url.contains("/embed/")) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
val vid = getVideoId(url) ?: return null
|
||||||
|
return "$mainUrl/embed/video/$vid"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getVideoId(url: String): String? {
|
||||||
|
val path = URL(url).path
|
||||||
|
val id = path.substringAfter("video/")
|
||||||
|
if (id.matches(videoIdRegex)) {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Config(
|
||||||
|
val context: Context,
|
||||||
|
val dmInternalData: InternalData
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InternalData(
|
||||||
|
val ts: Int,
|
||||||
|
val v1st: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Context(
|
||||||
|
@JsonProperty("access_token") val accessToken: String?,
|
||||||
|
val dmvk: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class MetaData(
|
||||||
|
val qualities: Map<String, List<VideoLink>>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VideoLink(
|
||||||
|
val type: String,
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,10 @@ 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 DoodWfExtractor : DoodLaExtractor() {
|
||||||
|
override var mainUrl = "https://dood.wf"
|
||||||
|
}
|
||||||
|
|
||||||
class DoodCxExtractor : DoodLaExtractor() {
|
class DoodCxExtractor : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dood.cx"
|
override var mainUrl = "https://dood.cx"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
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.getQualityFromName
|
||||||
|
import com.lagradost.cloudstream3.utils.httpsify
|
||||||
|
|
||||||
|
open class Embedgram : ExtractorApi() {
|
||||||
|
override val name = "Embedgram"
|
||||||
|
override val mainUrl = "https://embedgram.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val document = app.get(url, referer = referer).document
|
||||||
|
val link = document.select("video source:last-child").attr("src")
|
||||||
|
val quality = document.select("video source:last-child").attr("title")
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
httpsify(link),
|
||||||
|
"$mainUrl/",
|
||||||
|
getQualityFromName(quality),
|
||||||
|
headers = mapOf(
|
||||||
|
"Range" to "bytes=0-"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +1,54 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.apmap
|
import com.lagradost.cloudstream3.amap
|
||||||
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.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
|
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
|
||||||
class Fastream: ExtractorApi() {
|
open class Fastream: ExtractorApi() {
|
||||||
override var mainUrl = "https://fastream.to"
|
override var mainUrl = "https://fastream.to"
|
||||||
override var name = "Fastream"
|
override var name = "Fastream"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
suspend fun getstream(
|
||||||
|
response: Document,
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
sources: ArrayList<ExtractorLink>): Boolean{
|
||||||
val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
|
response.select("script").amap { script ->
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
|
||||||
val response = app.post("$mainUrl/dl",
|
val unpacked = getAndUnpack(script.data())
|
||||||
data = mapOf(
|
//val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
||||||
Pair("op","embed"),
|
val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"")
|
||||||
Pair("file_code",id),
|
//val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach
|
||||||
Pair("auto","1")
|
|
||||||
)).document
|
|
||||||
response.select("script").apmap { script ->
|
|
||||||
if (script.data().contains("sources")) {
|
|
||||||
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
|
||||||
val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
|
|
||||||
generateM3u8(
|
generateM3u8(
|
||||||
name,
|
name,
|
||||||
m3u8,
|
newm3u8link,
|
||||||
mainUrl
|
mainUrl
|
||||||
).forEach { link ->
|
).forEach { link ->
|
||||||
sources.add(link)
|
sources.add(link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val sources = ArrayList<ExtractorLink>()
|
||||||
|
val idregex = Regex("emb.html\\?(.*)=")
|
||||||
|
if (url.contains(Regex("(emb.html.*fastream)"))) {
|
||||||
|
val id = idregex.find(url)?.destructured?.component1() ?: ""
|
||||||
|
val response = app.post("https://fastream.to/dl", allowRedirects = false,
|
||||||
|
data = mapOf(
|
||||||
|
"op" to "embed",
|
||||||
|
"file_code" to id,
|
||||||
|
"auto" to "1"
|
||||||
|
)
|
||||||
|
).document
|
||||||
|
getstream(response, sources)
|
||||||
|
}
|
||||||
|
val response = app.get(url, referer = url).document
|
||||||
|
getstream(response, sources)
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
class Filesim : ExtractorApi() {
|
open class Filesim : ExtractorApi() {
|
||||||
override val name = "Filesim"
|
override val name = "Filesim"
|
||||||
override val mainUrl = "https://files.im"
|
override val mainUrl = "https://files.im"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -3,9 +3,9 @@ package com.lagradost.cloudstream3.extractors
|
||||||
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.M3u8Helper
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
class GMPlayer : ExtractorApi() {
|
open class GMPlayer : ExtractorApi() {
|
||||||
override val name = "GM Player"
|
override val name = "GM Player"
|
||||||
override val mainUrl = "https://gmplayer.xyz"
|
override val mainUrl = "https://gmplayer.xyz"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
@ -25,11 +25,16 @@ class GMPlayer : ExtractorApi() {
|
||||||
data = mapOf("hash" to id, "r" to ref)
|
data = mapOf("hash" to id, "r" to ref)
|
||||||
).parsed<GmResponse>().videoSource ?: return null
|
).parsed<GmResponse>().videoSource ?: return null
|
||||||
|
|
||||||
return M3u8Helper.generateM3u8(
|
return listOf(
|
||||||
name,
|
ExtractorLink(
|
||||||
m3u8,
|
this.name,
|
||||||
ref,
|
this.name,
|
||||||
headers = mapOf("accept" to "*/*")
|
m3u8,
|
||||||
|
ref,
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
headers = mapOf("accept" to "*/*"),
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
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() {
|
||||||
|
override var mainUrl = "https://databasegdriveplayer.co"
|
||||||
|
}
|
||||||
|
|
||||||
|
class DatabaseGdrive : Gdriveplayer() {
|
||||||
|
override var mainUrl = "https://series.databasegdriveplayer.co"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerapi : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayerapi.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerapp : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.app"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerfun : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.fun"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerio : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.io"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerme : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.me"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerbiz : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.biz"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerorg : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerus : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.us"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Gdriveplayerco : Gdriveplayer() {
|
||||||
|
override val mainUrl: String = "https://gdriveplayer.co"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Gdriveplayer : ExtractorApi() {
|
||||||
|
override val name = "Gdrive"
|
||||||
|
override val mainUrl = "https://gdriveplayer.to"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
private fun unpackJs(script: Element): String? {
|
||||||
|
return script.select("script").find { it.data().contains("eval(function(p,a,c,k,e,d)") }
|
||||||
|
?.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? {
|
||||||
|
return find(str)?.groupValues?.getOrNull(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.addMarks(str: String): String {
|
||||||
|
return this.replace(Regex("\"?$str\"?"), "\"$str\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val document = app.get(url).document
|
||||||
|
|
||||||
|
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
||||||
|
val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
|
||||||
|
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||||
|
?.split(Regex("\\D+"))
|
||||||
|
?.joinToString("") {
|
||||||
|
Char(it.toInt()).toString()
|
||||||
|
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||||
|
?: throw ErrorLoadingException("can't find password")
|
||||||
|
val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||||
|
|
||||||
|
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||||
|
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
|
||||||
|
|
||||||
|
Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(sourceData ?: return).map {
|
||||||
|
it.groupValues[1] to it.groupValues[2]
|
||||||
|
}.toList().distinctBy { it.second }.map { (link, quality) ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
source = this.name,
|
||||||
|
name = this.name,
|
||||||
|
url = "${httpsify(link)}&res=$quality",
|
||||||
|
referer = mainUrl,
|
||||||
|
quality = quality.toIntOrNull() ?: Qualities.Unknown.value,
|
||||||
|
headers = mapOf("Range" to "bytes=0-")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subData?.addMarks("file")?.addMarks("kind")?.addMarks("label").let { dataSub ->
|
||||||
|
tryParseJson<List<Tracks>>("[$dataSub]")?.map { sub ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
sub.label,
|
||||||
|
httpsify(sub.file)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AesData(
|
||||||
|
@JsonProperty("ct") val ct: String,
|
||||||
|
@JsonProperty("iv") val iv: String,
|
||||||
|
@JsonProperty("s") val s: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("kind") val kind: String,
|
||||||
|
@JsonProperty("label") val label: String
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -1,36 +1,83 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
||||||
|
class CineGrabber : GuardareStream() {
|
||||||
|
override var name = "CineGrabber"
|
||||||
|
override var mainUrl = "https://cinegrabber.com"
|
||||||
|
}
|
||||||
|
|
||||||
open class GuardareStream : ExtractorApi() {
|
open class GuardareStream : ExtractorApi() {
|
||||||
override var name = "Guardare"
|
override var name = "Guardare"
|
||||||
override var mainUrl = "https://guardare.stream"
|
override var mainUrl = "https://guardare.stream"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
data class GuardareJsonData (
|
data class GuardareJsonData(
|
||||||
@JsonProperty("data") val data : List<GuardareData>,
|
@JsonProperty("data") val data: List<GuardareData>,
|
||||||
|
@JsonProperty("captions") val captions: List<GuardareCaptions?>?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class GuardareData (
|
data class GuardareData(
|
||||||
@JsonProperty("file") val file : String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("label") val label : String,
|
@JsonProperty("label") val label: String,
|
||||||
@JsonProperty("type") val type : String
|
@JsonProperty("type") val type: String
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
|
||||||
val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text
|
// https://cinegrabber.com/asset/userdata/224879/caption/gqdmzh-71ez76z8/876438.srt
|
||||||
val jsonvideodata = AppUtils.parseJson<GuardareJsonData>(response)
|
data class GuardareCaptions(
|
||||||
return jsonvideodata.data.map {
|
@JsonProperty("id") val id: String,
|
||||||
ExtractorLink(
|
@JsonProperty("hash") val hash: String,
|
||||||
it.file+".${it.type}",
|
@JsonProperty("language") val language: String?,
|
||||||
this.name,
|
@JsonProperty("extension") val extension: String
|
||||||
it.file+".${it.type}",
|
) {
|
||||||
mainUrl,
|
fun getUrl(mainUrl: String, userId: String): String {
|
||||||
it.label.filter{ it.isDigit() }.toInt(),
|
return "$mainUrl/asset/userdata/$userId/caption/$hash/$id.$extension"
|
||||||
false
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val response =
|
||||||
|
app.post(url.replace("/v/", "/api/source/"), data = mapOf("d" to mainUrl)).text
|
||||||
|
|
||||||
|
val jsonVideoData = AppUtils.parseJson<GuardareJsonData>(response)
|
||||||
|
jsonVideoData.data.forEach {
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
it.file + ".${it.type}",
|
||||||
|
this.name,
|
||||||
|
it.file + ".${it.type}",
|
||||||
|
mainUrl,
|
||||||
|
it.label.filter { it.isDigit() }.toInt(),
|
||||||
|
false
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!jsonVideoData.captions.isNullOrEmpty()){
|
||||||
|
val iframe = app.get(url)
|
||||||
|
// var USER_ID = '224879';
|
||||||
|
val userIdRegex = Regex("""USER_ID.*?(\d+)""")
|
||||||
|
val userId = userIdRegex.find(iframe.text)?.groupValues?.getOrNull(1) ?: return
|
||||||
|
jsonVideoData.captions.forEach {
|
||||||
|
if (it == null) return@forEach
|
||||||
|
val subUrl = it.getUrl(mainUrl, userId)
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
it.language ?: "",
|
||||||
|
subUrl
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
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.tryParseJson
|
||||||
|
|
||||||
|
open class Jeniusplay : ExtractorApi() {
|
||||||
|
override val name = "Jeniusplay"
|
||||||
|
override val mainUrl = "https://jeniusplay.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val document = app.get(url, referer = "$mainUrl/").document
|
||||||
|
val hash = url.split("/").last().substringAfter("data=")
|
||||||
|
|
||||||
|
val m3uLink = app.post(
|
||||||
|
url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
|
||||||
|
data = mapOf("hash" to hash, "r" to "$referer"),
|
||||||
|
referer = url,
|
||||||
|
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||||
|
).parsed<ResponseSource>().videoSource
|
||||||
|
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
this.name,
|
||||||
|
m3uLink,
|
||||||
|
url,
|
||||||
|
).forEach(callback)
|
||||||
|
|
||||||
|
|
||||||
|
document.select("script").map { script ->
|
||||||
|
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||||
|
val subData =
|
||||||
|
getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],")
|
||||||
|
tryParseJson<List<Tracks>>("[$subData]")?.map { subtitle ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
getLanguage(subtitle.label ?: ""),
|
||||||
|
subtitle.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLanguage(str: String): String {
|
||||||
|
return when {
|
||||||
|
str.contains("indonesia", true) || str
|
||||||
|
.contains("bahasa", true) -> "Indonesian"
|
||||||
|
else -> str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ResponseSource(
|
||||||
|
@JsonProperty("hls") val hls: Boolean,
|
||||||
|
@JsonProperty("videoSource") val videoSource: String,
|
||||||
|
@JsonProperty("securedLink") val securedLink: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("kind") val kind: String?,
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,22 +1,26 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
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.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
class Linkbox : ExtractorApi() {
|
open class Linkbox : ExtractorApi() {
|
||||||
override val name = "Linkbox"
|
override val name = "Linkbox"
|
||||||
override val mainUrl = "https://www.linkbox.to"
|
override val mainUrl = "https://www.linkbox.to"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(
|
||||||
val id = url.substringAfter("id=")
|
url: String,
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
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 ->
|
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
|
||||||
sources.add(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
name,
|
name,
|
||||||
|
@ -26,8 +30,6 @@ class Linkbox : ExtractorApi() {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sources
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RList(
|
data class RList(
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
|
||||||
|
|
||||||
open class Mcloud : WcoStream() {
|
|
||||||
override var name = "Mcloud"
|
|
||||||
override var mainUrl = "https://mcloud.to"
|
|
||||||
override val requiresReferer = true
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
class MoviehabNet : Moviehab() {
|
||||||
|
override var mainUrl = "https://play.moviehab.net"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Moviehab : ExtractorApi() {
|
||||||
|
override var name = "Moviehab"
|
||||||
|
override var mainUrl = "https://play.moviehab.com"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val res = app.get(url)
|
||||||
|
res.document.select("video#player").let {
|
||||||
|
//should redirect first for making it works
|
||||||
|
val link = app.get("$mainUrl/${it.select("source").attr("src")}", referer = url).url
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
this.name,
|
||||||
|
link,
|
||||||
|
url
|
||||||
|
).forEach(callback)
|
||||||
|
|
||||||
|
Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
it.select("track").attr("label"),
|
||||||
|
"$mainUrl/$sub"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||||
|
|
||||||
class Mp4Upload : ExtractorApi() {
|
open class Mp4Upload : ExtractorApi() {
|
||||||
override var name = "Mp4Upload"
|
override var name = "Mp4Upload"
|
||||||
override var mainUrl = "https://www.mp4upload.com"
|
override var mainUrl = "https://www.mp4upload.com"
|
||||||
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
||||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
class MultiQuality : ExtractorApi() {
|
open class MultiQuality : ExtractorApi() {
|
||||||
override var name = "MultiQuality"
|
override var name = "MultiQuality"
|
||||||
override var mainUrl = "https://gogo-play.net"
|
override var mainUrl = "https://gogo-play.net"
|
||||||
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
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.Qualities
|
||||||
|
|
||||||
|
open class Mvidoo : ExtractorApi() {
|
||||||
|
override val name = "Mvidoo"
|
||||||
|
override val mainUrl = "https://mvidoo.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
private fun String.decodeHex(): String {
|
||||||
|
require(length % 2 == 0) { "Must have an even length" }
|
||||||
|
return String(
|
||||||
|
chunked(2)
|
||||||
|
.map { it.toInt(16).toByte() }
|
||||||
|
.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val document = app.get(url, referer = referer).text
|
||||||
|
val data = Regex("""\{var\s*[^\s]+\s*=\s*(\[[^]]+])""").find(document)?.groupValues?.get(1)
|
||||||
|
?.removeSurrounding("[", "]")?.replace("\"", "")?.replace("\\x", "")?.split(",")?.map { it.decodeHex() }?.reversed()?.joinToString("") ?: return
|
||||||
|
Regex("source\\s*src=\"([^\"]+)").find(data)?.groupValues?.get(1)?.let { link ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
link,
|
||||||
|
"$mainUrl/",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
headers = mapOf(
|
||||||
|
"Range" to "bytes=0-"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
|
data class Okrulinkdata (
|
||||||
|
@JsonProperty("status" ) var status : String? = null,
|
||||||
|
@JsonProperty("url" ) var url : String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
open class Okrulink: ExtractorApi() {
|
||||||
|
override var mainUrl = "https://okru.link"
|
||||||
|
override var name = "Okrulink"
|
||||||
|
override val requiresReferer = false
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
val key = url.substringAfter("html?t=")
|
||||||
|
val request = app.post("https://apizz.okru.link/decoding", allowRedirects = false,
|
||||||
|
data = mapOf("video" to key)
|
||||||
|
).parsedSafe<Okrulinkdata>()
|
||||||
|
if (request?.url != null) {
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
request.url!!,
|
||||||
|
"",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
isM3u8 = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return sources
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.apmap
|
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
|
||||||
|
@ -14,7 +14,7 @@ import org.jsoup.Jsoup
|
||||||
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
|
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
|
||||||
* If they diverge it'd be better to make them separate.
|
* If they diverge it'd be better to make them separate.
|
||||||
* */
|
* */
|
||||||
class Pelisplus(val mainUrl: String) {
|
open class Pelisplus(val mainUrl: String) {
|
||||||
val name: String = "Vidstream"
|
val name: String = "Vidstream"
|
||||||
|
|
||||||
private fun getExtractorUrl(id: String): String {
|
private fun getExtractorUrl(id: String): String {
|
||||||
|
@ -35,7 +35,7 @@ class Pelisplus(val mainUrl: String) {
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
): Boolean {
|
): Boolean {
|
||||||
try {
|
try {
|
||||||
normalApis.apmap { api ->
|
normalApis.amap { api ->
|
||||||
val url = api.getExtractorUrl(id)
|
val url = api.getExtractorUrl(id)
|
||||||
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
|
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,8 @@ class Pelisplus(val mainUrl: String) {
|
||||||
val qualityRegex = Regex("(\\d+)P")
|
val qualityRegex = Regex("(\\d+)P")
|
||||||
|
|
||||||
//a[download]
|
//a[download]
|
||||||
pageDoc.select(".dowload > a")?.apmap { element ->
|
pageDoc.select(".dowload > a")?.amap { element ->
|
||||||
val href = element.attr("href") ?: return@apmap
|
val href = element.attr("href") ?: return@amap
|
||||||
val qual = if (element.text()
|
val qual = if (element.text()
|
||||||
.contains("HDP")
|
.contains("HDP")
|
||||||
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
|
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
|
||||||
|
@ -84,7 +84,7 @@ class Pelisplus(val mainUrl: String) {
|
||||||
//val name = element.text()
|
//val name = element.text()
|
||||||
|
|
||||||
// Matches vidstream links with extractors
|
// Matches vidstream links with extractors
|
||||||
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
|
extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
|
||||||
if (link.startsWith(api.mainUrl)) {
|
if (link.startsWith(api.mainUrl)) {
|
||||||
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
|
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
|
open class PlayLtXyz: ExtractorApi() {
|
||||||
|
override val name: String = "PlayLt"
|
||||||
|
override val mainUrl: String = "https://play.playlt.xyz"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
private data class ResponseData(
|
||||||
|
@JsonProperty("data") val data: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
|
val extractedLinksList = mutableListOf<ExtractorLink>()
|
||||||
|
//Log.i(this.name, "Result => (url) $url")
|
||||||
|
var idUser = ""
|
||||||
|
var idFile = ""
|
||||||
|
var bodyText = ""
|
||||||
|
val doc = app.get(url, referer = referer).document
|
||||||
|
//Log.i(this.name, "Result => (url, script) $url / ${doc.select("script")}")
|
||||||
|
bodyText = doc.select("script").firstOrNull {
|
||||||
|
val text = it?.toString() ?: ""
|
||||||
|
text.contains("var idUser")
|
||||||
|
}?.toString() ?: ""
|
||||||
|
//Log.i(this.name, "Result => (bodyText) $bodyText")
|
||||||
|
if (bodyText.isNotBlank()) {
|
||||||
|
idUser = "(?<=var idUser = \")(.*)(?=\";)".toRegex().find(bodyText)
|
||||||
|
?.groupValues?.get(0) ?: ""
|
||||||
|
|
||||||
|
idFile = "(?<=var idfile = \")(.*)(?=\";)".toRegex().find(bodyText)
|
||||||
|
?.groupValues?.get(0) ?: ""
|
||||||
|
}
|
||||||
|
//Log.i(this.name, "Result => (idUser, idFile) $idUser / $idFile")
|
||||||
|
if (idUser.isNotBlank() && idFile.isNotBlank()) {
|
||||||
|
//val sess = HttpSession()
|
||||||
|
val ajaxHead = mapOf(
|
||||||
|
Pair("Origin", mainUrl),
|
||||||
|
Pair("Referer", mainUrl),
|
||||||
|
Pair("Sec-Fetch-Site", "same-site"),
|
||||||
|
Pair("Sec-Fetch-Mode", "cors"),
|
||||||
|
Pair("Sec-Fetch-Dest", "empty")
|
||||||
|
)
|
||||||
|
val ajaxData = mapOf(
|
||||||
|
Pair("referrer", referer ?: mainUrl),
|
||||||
|
Pair("typeend", "html")
|
||||||
|
)
|
||||||
|
|
||||||
|
//idUser = 608f7c85cf0743547f1f1b4e
|
||||||
|
val posturl = "https://api-plhq.playlt.xyz/apiv5/$idUser/$idFile"
|
||||||
|
val data = app.post(posturl, headers = ajaxHead, data = ajaxData)
|
||||||
|
//Log.i(this.name, "Result => (posturl) $posturl")
|
||||||
|
if (data.isSuccessful) {
|
||||||
|
val itemstr = data.text
|
||||||
|
Log.i(this.name, "Result => (data) $itemstr")
|
||||||
|
tryParseJson<ResponseData?>(itemstr)?.let { item ->
|
||||||
|
val linkUrl = item.data ?: ""
|
||||||
|
if (linkUrl.isNotBlank()) {
|
||||||
|
extractedLinksList.add(
|
||||||
|
ExtractorLink(
|
||||||
|
source = name,
|
||||||
|
name = name,
|
||||||
|
url = linkUrl,
|
||||||
|
referer = url,
|
||||||
|
quality = Qualities.Unknown.value,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extractedLinksList
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
|
||||||
class Solidfiles : ExtractorApi() {
|
open class Solidfiles : ExtractorApi() {
|
||||||
override val name = "Solidfiles"
|
override val name = "Solidfiles"
|
||||||
override val mainUrl = "https://www.solidfiles.com"
|
override val mainUrl = "https://www.solidfiles.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -7,7 +7,11 @@ 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
|
||||||
|
|
||||||
class SpeedoStream : ExtractorApi() {
|
class SpeedoStream1 : SpeedoStream() {
|
||||||
|
override val mainUrl = "https://speedostream.nl"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class SpeedoStream : ExtractorApi() {
|
||||||
override val name = "SpeedoStream"
|
override val name = "SpeedoStream"
|
||||||
override val mainUrl = "https://speedostream.com"
|
override val mainUrl = "https://speedostream.com"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
|
@ -1,12 +1,26 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
|
||||||
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
|
||||||
|
|
||||||
|
class Sbspeed : StreamSB() {
|
||||||
|
override var name = "Sbspeed"
|
||||||
|
override var mainUrl = "https://sbspeed.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Streamsss : StreamSB() {
|
||||||
|
override var mainUrl = "https://streamsss.net"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sbflix : StreamSB() {
|
||||||
|
override var mainUrl = "https://sbflix.xyz"
|
||||||
|
override var name = "Sbflix"
|
||||||
|
}
|
||||||
|
|
||||||
class Vidgomunime : StreamSB() {
|
class Vidgomunime : StreamSB() {
|
||||||
override var mainUrl = "https://vidgomunime.xyz"
|
override var mainUrl = "https://vidgomunime.xyz"
|
||||||
}
|
}
|
||||||
|
@ -84,15 +98,15 @@ open class StreamSB : ExtractorApi() {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Subs (
|
data class Subs (
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String? = null,
|
||||||
@JsonProperty("label") val label: String,
|
@JsonProperty("label") val label: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class StreamData (
|
data class StreamData (
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("cdn_img") val cdnImg: String,
|
@JsonProperty("cdn_img") val cdnImg: String,
|
||||||
@JsonProperty("hash") val hash: String,
|
@JsonProperty("hash") val hash: String,
|
||||||
@JsonProperty("subs") val subs: List<Subs>?,
|
@JsonProperty("subs") val subs: ArrayList<Subs>? = arrayListOf(),
|
||||||
@JsonProperty("length") val length: String,
|
@JsonProperty("length") val length: String,
|
||||||
@JsonProperty("id") val id: String,
|
@JsonProperty("id") val id: String,
|
||||||
@JsonProperty("title") val title: String,
|
@JsonProperty("title") val title: String,
|
||||||
|
@ -104,31 +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?): List<ExtractorLink>? {
|
override suspend fun getUrl(
|
||||||
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_-]+)")
|
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 {
|
val id = regexID.findAll(url).map {
|
||||||
it.value.replace(Regex("(embed-|\\/e\\/)"),"")
|
it.value.replace(Regex("(embed-|/e/)"), "")
|
||||||
}.first()
|
}.first()
|
||||||
val bytes = id.toByteArray()
|
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
||||||
val bytesToHex = bytesToHex(bytes)
|
val master = "$mainUrl/sources50/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
|
||||||
val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
|
|
||||||
val headers = mapOf(
|
val headers = mapOf(
|
||||||
"watchsb" to "streamsb",
|
"watchsb" to "sbstream",
|
||||||
)
|
)
|
||||||
val urltext = app.get(master,
|
val mapped = app.get(
|
||||||
|
master.lowercase(),
|
||||||
headers = headers,
|
headers = headers,
|
||||||
allowRedirects = false
|
referer = url,
|
||||||
).text
|
).parsedSafe<Main>()
|
||||||
val mapped = urltext.let { parseJson<Main>(it) }
|
|
||||||
val testurl = app.get(mapped.streamData.file, headers = headers).text
|
|
||||||
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
|
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
|
||||||
if (urltext.contains("m3u8") && testurl.contains("EXTM3U"))
|
M3u8Helper.generateM3u8(
|
||||||
return M3u8Helper.generateM3u8(
|
name,
|
||||||
name,
|
mapped?.streamData?.file ?: return,
|
||||||
mapped.streamData.file,
|
url,
|
||||||
url,
|
headers = headers
|
||||||
headers = headers
|
).forEach(callback)
|
||||||
|
|
||||||
|
mapped.streamData.subs?.map {sub ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
sub.label.toString(),
|
||||||
|
sub.file ?: return@map null,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return null
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -5,7 +5,15 @@ 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.Qualities
|
||||||
|
|
||||||
class StreamTape : ExtractorApi() {
|
class StreamTapeNet : StreamTape() {
|
||||||
|
override var mainUrl = "https://streamtape.net"
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShaveTape : StreamTape(){
|
||||||
|
override var mainUrl = "https://shavetape.cash"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class StreamTape : ExtractorApi() {
|
||||||
override var name = "StreamTape"
|
override var name = "StreamTape"
|
||||||
override var mainUrl = "https://streamtape.com"
|
override var mainUrl = "https://streamtape.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
@ -16,7 +24,8 @@ class StreamTape : 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)) {
|
with(app.get(url)) {
|
||||||
linkRegex.find(this.text)?.let {
|
linkRegex.find(this.text)?.let {
|
||||||
val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
|
val extractedUrl =
|
||||||
|
"https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3)}"
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.JsUnpacker
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
|
||||||
class Streamhub : ExtractorApi() {
|
open class Streamhub : ExtractorApi() {
|
||||||
override var mainUrl = "https://streamhub.to"
|
override var mainUrl = "https://streamhub.to"
|
||||||
override var name = "Streamhub"
|
override var name = "Streamhub"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
|
||||||
|
import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
|
open class Streamplay : ExtractorApi() {
|
||||||
|
override val name = "Streamplay"
|
||||||
|
override val mainUrl = "https://streamplay.to"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val request = app.get(url, referer = referer)
|
||||||
|
val redirectUrl = request.url
|
||||||
|
val mainServer = URI(redirectUrl).let {
|
||||||
|
"${it.scheme}://${it.host}"
|
||||||
|
}
|
||||||
|
val key = redirectUrl.substringAfter("embed-").substringBefore(".html")
|
||||||
|
val token =
|
||||||
|
request.document.select("script").find { it.data().contains("sitekey:") }?.data()
|
||||||
|
?.substringAfterLast("sitekey: '")?.substringBefore("',")?.let { captchaKey ->
|
||||||
|
getCaptchaToken(
|
||||||
|
redirectUrl,
|
||||||
|
captchaKey,
|
||||||
|
referer = "$mainServer/"
|
||||||
|
)
|
||||||
|
} ?: throw ErrorLoadingException("can't bypass captcha")
|
||||||
|
app.post(
|
||||||
|
"$mainServer/player-$key-488x286.html", data = mapOf(
|
||||||
|
"op" to "embed",
|
||||||
|
"token" to token
|
||||||
|
),
|
||||||
|
referer = redirectUrl,
|
||||||
|
headers = mapOf(
|
||||||
|
"Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||||
|
"Content-Type" to "application/x-www-form-urlencoded"
|
||||||
|
)
|
||||||
|
).document.select("script").find { script ->
|
||||||
|
script.data().contains("eval(function(p,a,c,k,e,d)")
|
||||||
|
}?.let {
|
||||||
|
val data = getAndUnpack(it.data()).substringAfter("sources=[").substringBefore(",desc")
|
||||||
|
.replace("file", "\"file\"")
|
||||||
|
.replace("label", "\"label\"")
|
||||||
|
tryParseJson<List<Source>>("[$data}]")?.map { res ->
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
res.file ?: return@map null,
|
||||||
|
"$mainServer/",
|
||||||
|
when (res.label) {
|
||||||
|
"HD" -> Qualities.P720.value
|
||||||
|
"SD" -> Qualities.P480.value
|
||||||
|
else -> Qualities.Unknown.value
|
||||||
|
},
|
||||||
|
headers = mapOf(
|
||||||
|
"Range" to "bytes=0-"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Source(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ data class Files(
|
||||||
@JsonProperty("label") val label: String? = null,
|
@JsonProperty("label") val label: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
open class Supervideo : ExtractorApi() {
|
open class Supervideo : ExtractorApi() {
|
||||||
override var name = "Supervideo"
|
override var name = "Supervideo"
|
||||||
override var mainUrl = "https://supervideo.tv"
|
override var mainUrl = "https://supervideo.tv"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
@ -20,10 +20,13 @@ data class Files(
|
||||||
val response = app.get(url).text
|
val response = app.get(url).text
|
||||||
val jstounpack = Regex("eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
|
val jstounpack = Regex("eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
|
||||||
val unpacjed = JsUnpacker(jstounpack).unpack()
|
val unpacjed = JsUnpacker(jstounpack).unpack()
|
||||||
val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",")
|
val extractedUrl =
|
||||||
|
unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString()
|
||||||
|
.replace("file", """"file"""").replace("label", """"label"""")
|
||||||
|
.substringBeforeLast(",")
|
||||||
val parsedlinks = parseJson<List<Files>>(extractedUrl)
|
val parsedlinks = parseJson<List<Files>>(extractedUrl)
|
||||||
parsedlinks.forEach { data ->
|
parsedlinks.forEach { data ->
|
||||||
if (data.label.isNullOrBlank()){ // mp4 links (with labels) are slow. Use only m3u8 link.
|
if (data.label.isNullOrBlank()) { // mp4 links (with labels) are slow. Use only m3u8 link.
|
||||||
M3u8Helper.generateM3u8(
|
M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
data.id,
|
data.id,
|
||||||
|
@ -34,8 +37,6 @@ data class Files(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return extractedLinksList
|
return extractedLinksList
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,41 +1,64 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
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.app
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
|
import com.lagradost.cloudstream3.mapper
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
|
||||||
|
|
||||||
class Cinestart: Tomatomatela() {
|
class Cinestart: Tomatomatela() {
|
||||||
override var name = "Cinestart"
|
override var name: String = "Cinestart"
|
||||||
override var mainUrl = "https://cinestart.net"
|
override val mainUrl: String = "https://cinestart.net"
|
||||||
override val details = "vr.php?v="
|
override val details = "vr.php?v="
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TomatomatelalClub: Tomatomatela() {
|
||||||
|
override var name: String = "Tomatomatela"
|
||||||
|
override val mainUrl: String = "https://tomatomatela.club"
|
||||||
|
}
|
||||||
|
|
||||||
open class Tomatomatela : ExtractorApi() {
|
open class Tomatomatela : ExtractorApi() {
|
||||||
override var name = "Tomatomatela"
|
override var name = "Tomatomatela"
|
||||||
override var mainUrl = "https://tomatomatela.com"
|
override val mainUrl = "https://tomatomatela.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
private data class Tomato (
|
private data class Tomato (
|
||||||
@JsonProperty("status") val status: Int,
|
@JsonProperty("status") val status: Int,
|
||||||
@JsonProperty("file") val file: String
|
@JsonProperty("file") val file: String?
|
||||||
)
|
)
|
||||||
open val details = "details.php?v="
|
open val details = "details.php?v="
|
||||||
|
open val embeddetails = "/embed.html#"
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val link = url.replace("$mainUrl/embed.html#","$mainUrl/$details")
|
val link = url.replace("$mainUrl$embeddetails","$mainUrl/$details")
|
||||||
val server = app.get(link, allowRedirects = false).text
|
val sources = ArrayList<ExtractorLink>()
|
||||||
val json = parseJson<Tomato>(server)
|
val server = app.get(link, allowRedirects = false,
|
||||||
if (json.status == 200) return listOf(
|
headers = mapOf(
|
||||||
ExtractorLink(
|
"User-Agent" to USER_AGENT,
|
||||||
name,
|
"Accept" to "application/json, text/javascript, */*; q=0.01",
|
||||||
name,
|
"Accept-Language" to "en-US,en;q=0.5",
|
||||||
json.file,
|
"X-Requested-With" to "XMLHttpRequest",
|
||||||
"",
|
"DNT" to "1",
|
||||||
Qualities.Unknown.value,
|
"Connection" to "keep-alive",
|
||||||
isM3u8 = false
|
"Sec-Fetch-Dest" to "empty",
|
||||||
|
"Sec-Fetch-Mode" to "cors",
|
||||||
|
"Sec-Fetch-Site" to "same-origin"
|
||||||
|
|
||||||
)
|
)
|
||||||
)
|
).parsedSafe<Tomato>()
|
||||||
return null
|
if (server?.file != null) {
|
||||||
|
sources.add(
|
||||||
|
ExtractorLink(
|
||||||
|
name,
|
||||||
|
name,
|
||||||
|
server.file,
|
||||||
|
"",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
isM3u8 = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return sources
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,19 +1,23 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
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.Qualities
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
class UpstreamExtractor: ExtractorApi() {
|
open class UpstreamExtractor : ExtractorApi() {
|
||||||
override val name: String = "Upstream.to"
|
override val name: String = "Upstream"
|
||||||
override val mainUrl: String = "https://upstream.to"
|
override val mainUrl: String = "https://upstream.to"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(
|
||||||
// WIP: m3u8 link fetched but sometimes not playing
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
//Log.i(this.name, "Result => (no extractor) ${url}")
|
//Log.i(this.name, "Result => (no extractor) ${url}")
|
||||||
val sources: MutableList<ExtractorLink> = mutableListOf()
|
|
||||||
val doc = app.get(url, referer = referer).text
|
val doc = app.get(url, referer = referer).text
|
||||||
if (doc.isNotBlank()) {
|
if (doc.isNotBlank()) {
|
||||||
var reg = Regex("(?<=master)(.*)(?=hls)")
|
var reg = Regex("(?<=master)(.*)(?=hls)")
|
||||||
|
@ -30,7 +34,9 @@ class UpstreamExtractor: ExtractorApi() {
|
||||||
domName = "${part}.${domName}"
|
domName = "${part}.${domName}"
|
||||||
}
|
}
|
||||||
domName.trimEnd('.')
|
domName.trimEnd('.')
|
||||||
} else { "" }
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
false -> ""
|
false -> ""
|
||||||
}
|
}
|
||||||
|
@ -42,18 +48,13 @@ class UpstreamExtractor: ExtractorApi() {
|
||||||
|
|
||||||
result?.forEach {
|
result?.forEach {
|
||||||
val linkUrl = "https://${domain}/hls/${it}/master.m3u8"
|
val linkUrl = "https://${domain}/hls/${it}/master.m3u8"
|
||||||
sources.add(
|
M3u8Helper.generateM3u8(
|
||||||
ExtractorLink(
|
this.name,
|
||||||
name = "Upstream m3u8",
|
linkUrl,
|
||||||
source = this.name,
|
"$mainUrl/",
|
||||||
url = linkUrl,
|
headers = mapOf("Origin" to mainUrl)
|
||||||
quality = Qualities.Unknown.value,
|
).forEach(callback)
|
||||||
referer = referer ?: linkUrl,
|
|
||||||
isM3u8 = true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sources
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.apmap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import kotlinx.coroutines.delay
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import java.net.URI
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
|
||||||
|
|
||||||
class VidSrcExtractor2 : VidSrcExtractor() {
|
class VidSrcExtractor2 : VidSrcExtractor() {
|
||||||
override val mainUrl = "https://vidsrc.me/embed"
|
override val mainUrl = "https://vidsrc.me/embed"
|
||||||
|
@ -27,6 +26,25 @@ open class VidSrcExtractor : ExtractorApi() {
|
||||||
override val mainUrl = "$absoluteUrl/embed"
|
override val mainUrl = "$absoluteUrl/embed"
|
||||||
override val requiresReferer = false
|
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(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
referer: String?,
|
referer: String?,
|
||||||
|
@ -40,7 +58,10 @@ open class VidSrcExtractor : ExtractorApi() {
|
||||||
val datahash = it.attr("data-hash")
|
val datahash = it.attr("data-hash")
|
||||||
if (datahash.isNotBlank()) {
|
if (datahash.isNotBlank()) {
|
||||||
val links = try {
|
val links = try {
|
||||||
app.get("$absoluteUrl/src/$datahash", referer = "https://source.vidsrc.me/").url
|
app.get(
|
||||||
|
"$absoluteUrl/src/$datahash",
|
||||||
|
referer = "https://source.vidsrc.me/"
|
||||||
|
).url
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
@ -48,17 +69,28 @@ open class VidSrcExtractor : ExtractorApi() {
|
||||||
} else ""
|
} else ""
|
||||||
}
|
}
|
||||||
|
|
||||||
serverslist.apmap { server ->
|
serverslist.amap { server ->
|
||||||
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
|
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
|
||||||
if (linkfixed.contains("/pro")) {
|
if (linkfixed.contains("/pro")) {
|
||||||
val srcresponse = app.get(server, referer = absoluteUrl).text
|
val srcresponse = app.get(server, referer = absoluteUrl).text
|
||||||
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
|
||||||
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap
|
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
|
||||||
M3u8Helper.generateM3u8(
|
val passRegex = Regex("""['"](.*set_pass[^"']*)""")
|
||||||
name,
|
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
|
||||||
srcm3u8,
|
Regex("""^//"""), "https://"
|
||||||
absoluteUrl
|
)
|
||||||
).forEach(callback)
|
|
||||||
|
callback.invoke(
|
||||||
|
ExtractorLink(
|
||||||
|
this.name,
|
||||||
|
this.name,
|
||||||
|
srcm3u8,
|
||||||
|
"https://vidsrc.stream/",
|
||||||
|
Qualities.Unknown.value,
|
||||||
|
extractorData = pass,
|
||||||
|
isM3u8 = true
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
loadExtractor(linkfixed, url, subtitleCallback, callback)
|
loadExtractor(linkfixed, url, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class VideovardSX : WcoStream() {
|
||||||
override var mainUrl = "https://videovard.sx"
|
override var mainUrl = "https://videovard.sx"
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoVard : ExtractorApi() {
|
open class VideoVard : ExtractorApi() {
|
||||||
override var name = "Videovard" // Cause works for animekisa and wco
|
override var name = "Videovard" // Cause works for animekisa and wco
|
||||||
override var mainUrl = "https://videovard.to"
|
override var mainUrl = "https://videovard.to"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
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.tryParseJson
|
||||||
|
|
||||||
|
class Vidmolyme : Vidmoly() {
|
||||||
|
override val mainUrl = "https://vidmoly.me"
|
||||||
|
}
|
||||||
|
|
||||||
|
open class Vidmoly : ExtractorApi() {
|
||||||
|
override val name = "Vidmoly"
|
||||||
|
override val mainUrl = "https://vidmoly.to"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
private fun String.addMarks(str: String): String {
|
||||||
|
return this.replace(Regex("\"?$str\"?"), "\"$str\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val script = app.get(
|
||||||
|
url,
|
||||||
|
referer = referer,
|
||||||
|
).document.select("script")
|
||||||
|
.find { it.data().contains("sources:") }?.data()
|
||||||
|
val videoData = script?.substringAfter("sources: [")
|
||||||
|
?.substringBefore("],")?.addMarks("file")
|
||||||
|
val subData = script?.substringAfter("tracks: [")?.substringBefore("]")?.addMarks("file")
|
||||||
|
?.addMarks("label")?.addMarks("kind")
|
||||||
|
|
||||||
|
tryParseJson<Source>(videoData)?.file?.let { m3uLink ->
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
name,
|
||||||
|
m3uLink,
|
||||||
|
"$mainUrl/"
|
||||||
|
).forEach(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
tryParseJson<List<SubSource>>("[${subData}]")
|
||||||
|
?.filter { it.kind == "captions" }?.map {
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
it.label.toString(),
|
||||||
|
fixUrl(it.file.toString())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Source(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class SubSource(
|
||||||
|
@JsonProperty("file") val file: String? = null,
|
||||||
|
@JsonProperty("label") val label: String? = null,
|
||||||
|
@JsonProperty("kind") val kind: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.apmap
|
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
|
||||||
|
@ -37,7 +37,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
val extractorUrl = getExtractorUrl(id)
|
val extractorUrl = getExtractorUrl(id)
|
||||||
argamap(
|
argamap(
|
||||||
{
|
{
|
||||||
normalApis.apmap { api ->
|
normalApis.amap { api ->
|
||||||
val url = api.getExtractorUrl(id)
|
val url = api.getExtractorUrl(id)
|
||||||
api.getSafeUrl(
|
api.getSafeUrl(
|
||||||
url,
|
url,
|
||||||
|
@ -55,8 +55,8 @@ class Vidstream(val mainUrl: String) {
|
||||||
val qualityRegex = Regex("(\\d+)P")
|
val qualityRegex = Regex("(\\d+)P")
|
||||||
|
|
||||||
//a[download]
|
//a[download]
|
||||||
pageDoc.select(".dowload > a")?.apmap { element ->
|
pageDoc.select(".dowload > a")?.amap { element ->
|
||||||
val href = element.attr("href") ?: return@apmap
|
val href = element.attr("href") ?: return@amap
|
||||||
val qual = if (element.text()
|
val qual = if (element.text()
|
||||||
.contains("HDP")
|
.contains("HDP")
|
||||||
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
|
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
|
||||||
|
@ -87,7 +87,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
//val name = element.text()
|
//val name = element.text()
|
||||||
|
|
||||||
// Matches vidstream links with extractors
|
// Matches vidstream links with extractors
|
||||||
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
|
extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
|
||||||
if (link.startsWith(api.mainUrl)) {
|
if (link.startsWith(api.mainUrl)) {
|
||||||
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
|
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,39 +13,42 @@ open class VoeExtractor : ExtractorApi() {
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
||||||
private data class ResponseLinks(
|
private data class ResponseLinks(
|
||||||
@JsonProperty("hls") val url: String?,
|
@JsonProperty("hls") val hls: String?,
|
||||||
|
@JsonProperty("mp4") val mp4: String?,
|
||||||
@JsonProperty("video_height") val label: Int?
|
@JsonProperty("video_height") val label: Int?
|
||||||
//val type: String // Mp4
|
//val type: String // Mp4
|
||||||
)
|
)
|
||||||
|
|
||||||
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 html = app.get(url).text
|
||||||
val doc = app.get(url).text
|
if (html.isNotBlank()) {
|
||||||
if (doc.isNotBlank()) {
|
val src = html.substringAfter("const sources =").substringBefore(";")
|
||||||
val start = "const sources ="
|
// Remove last comma, it is not proper json otherwise
|
||||||
var src = doc.substring(doc.indexOf(start))
|
|
||||||
src = src.substring(start.length, src.indexOf(";"))
|
|
||||||
.replace("0,", "0")
|
.replace("0,", "0")
|
||||||
.trim()
|
// Make json use the proper quotes
|
||||||
|
.replace("'", "\"")
|
||||||
|
|
||||||
//Log.i(this.name, "Result => (src) ${src}")
|
//Log.i(this.name, "Result => (src) ${src}")
|
||||||
parseJson<ResponseLinks?>(src)?.let { voelink ->
|
parseJson<ResponseLinks?>(src)?.let { voeLink ->
|
||||||
//Log.i(this.name, "Result => (voelink) ${voelink}")
|
//Log.i(this.name, "Result => (voeLink) ${voeLink}")
|
||||||
val linkUrl = voelink.url
|
|
||||||
val linkLabel = voelink.label?.toString() ?: ""
|
// 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()) {
|
if (!linkUrl.isNullOrEmpty()) {
|
||||||
extractedLinksList.add(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name = this.name,
|
name = this.name,
|
||||||
source = this.name,
|
source = this.name,
|
||||||
url = linkUrl,
|
url = linkUrl,
|
||||||
quality = getQualityFromName(linkLabel),
|
quality = getQualityFromName(linkLabel),
|
||||||
referer = url,
|
referer = url,
|
||||||
isM3u8 = true
|
isM3u8 = voeLink.hls != null
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return extractedLinksList
|
return emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -53,6 +53,12 @@ class VizcloudSite : WcoStream() {
|
||||||
override var mainUrl = "https://vizcloud.site"
|
override var mainUrl = "https://vizcloud.site"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Mcloud : WcoStream() {
|
||||||
|
override var name = "Mcloud"
|
||||||
|
override var mainUrl = "https://mcloud.to"
|
||||||
|
override val requiresReferer = true
|
||||||
|
}
|
||||||
|
|
||||||
open class WcoStream : ExtractorApi() {
|
open class WcoStream : ExtractorApi() {
|
||||||
override var name = "VidStream" // Cause works for animekisa and wco
|
override var name = "VidStream" // Cause works for animekisa and wco
|
||||||
override var mainUrl = "https://vidstream.pro"
|
override var mainUrl = "https://vidstream.pro"
|
||||||
|
|
|
@ -1,12 +1,28 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
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.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
|
|
||||||
|
class Cdnplayer: XStreamCdn() {
|
||||||
|
override val name: String = "Cdnplayer"
|
||||||
|
override val mainUrl: String = "https://cdnplayer.online"
|
||||||
|
}
|
||||||
|
|
||||||
|
class Kotakajair: XStreamCdn() {
|
||||||
|
override val name: String = "Kotakajair"
|
||||||
|
override val mainUrl: String = "https://kotakajair.xyz"
|
||||||
|
}
|
||||||
|
|
||||||
|
class FEnet: XStreamCdn() {
|
||||||
|
override val name: String = "FEnet"
|
||||||
|
override val mainUrl: String = "https://fembed.net"
|
||||||
|
}
|
||||||
|
|
||||||
class Rasacintaku: XStreamCdn() {
|
class Rasacintaku: XStreamCdn() {
|
||||||
override val mainUrl: String = "https://rasa-cintaku-semakin-berantai.xyz"
|
override val mainUrl: String = "https://rasa-cintaku-semakin-berantai.xyz"
|
||||||
}
|
}
|
||||||
|
@ -54,44 +70,67 @@ open class XStreamCdn : ExtractorApi() {
|
||||||
//val type: String // Mp4
|
//val type: String // Mp4
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class Player(
|
||||||
|
@JsonProperty("poster_file") val poster_file: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
private data class ResponseJson(
|
private data class ResponseJson(
|
||||||
@JsonProperty("success") val success: Boolean,
|
@JsonProperty("success") val success: Boolean,
|
||||||
@JsonProperty("data") val data: List<ResponseData>?
|
@JsonProperty("player") val player: Player? = null,
|
||||||
|
@JsonProperty("data") val data: List<ResponseData>?,
|
||||||
|
@JsonProperty("captions") val captions: List<Captions?>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class Captions(
|
||||||
|
@JsonProperty("id") val id: String,
|
||||||
|
@JsonProperty("hash") val hash: String,
|
||||||
|
@JsonProperty("language") val language: String,
|
||||||
|
@JsonProperty("extension") val extension: String
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun getExtractorUrl(id: String): String {
|
override fun getExtractorUrl(id: String): String {
|
||||||
return "$domainUrl/api/source/$id"
|
return "$domainUrl/api/source/$id"
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
val headers = mapOf(
|
val headers = mapOf(
|
||||||
"Referer" to url,
|
"Referer" to url,
|
||||||
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
|
||||||
)
|
)
|
||||||
val id = url.trimEnd('/').split("/").last()
|
val id = url.trimEnd('/').split("/").last()
|
||||||
val newUrl = "https://${domainUrl}/api/source/${id}"
|
val newUrl = "https://${domainUrl}/api/source/${id}"
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
app.post(newUrl, headers = headers).let { res ->
|
||||||
with(app.post(newUrl, headers = headers)) {
|
val sources = tryParseJson<ResponseJson?>(res.text)
|
||||||
if (this.code != 200) return listOf()
|
sources?.let {
|
||||||
val text = this.text
|
|
||||||
if (text.isEmpty()) return listOf()
|
|
||||||
if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf()
|
|
||||||
AppUtils.parseJson<ResponseJson?>(text)?.let {
|
|
||||||
if (it.success && it.data != null) {
|
if (it.success && it.data != null) {
|
||||||
it.data.forEach { data ->
|
it.data.map { source ->
|
||||||
extractedLinksList.add(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
name,
|
name,
|
||||||
name = name,
|
name = name,
|
||||||
data.file,
|
source.file,
|
||||||
url,
|
url,
|
||||||
getQualityFromName(data.label),
|
getQualityFromName(source.label),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val userData = sources?.player?.poster_file?.split("/")?.get(2)
|
||||||
|
sources?.captions?.map {
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
it?.language.toString(),
|
||||||
|
"$mainUrl/asset/userdata/$userData/caption/${it?.hash}/${it?.id}.${it?.extension}"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return extractedLinksList
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,7 +7,7 @@ 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 YourUpload: ExtractorApi() {
|
open class YourUpload: ExtractorApi() {
|
||||||
override val name = "Yourupload"
|
override val name = "Yourupload"
|
||||||
override val mainUrl = "https://www.yourupload.com"
|
override val mainUrl = "https://www.yourupload.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -10,7 +10,7 @@ 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
|
||||||
|
|
||||||
class Zorofile : ExtractorApi() {
|
open class Zorofile : ExtractorApi() {
|
||||||
override val name = "Zorofile"
|
override val name = "Zorofile"
|
||||||
override val mainUrl = "https://zorofile.com"
|
override val mainUrl = "https://zorofile.com"
|
||||||
override val requiresReferer = true
|
override val requiresReferer = true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.apmap
|
import com.lagradost.cloudstream3.amap
|
||||||
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
|
||||||
|
@ -36,7 +36,7 @@ open class ZplayerV2 : ExtractorApi() {
|
||||||
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
||||||
m3u8regex.findAll(testdata).map {
|
m3u8regex.findAll(testdata).map {
|
||||||
it.value
|
it.value
|
||||||
}.toList().apmap { urlm3u8 ->
|
}.toList().amap { urlm3u8 ->
|
||||||
if (urlm3u8.contains("m3u8")) {
|
if (urlm3u8.contains("m3u8")) {
|
||||||
val testurl = app.get(urlm3u8, headers = mapOf("Referer" to url)).text
|
val testurl = app.get(urlm3u8, headers = mapOf("Referer" to url)).text
|
||||||
if (testurl.contains("EXTM3U")) {
|
if (testurl.contains("EXTM3U")) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.apmap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
@ -18,7 +18,7 @@ class AsianEmbedHelper {
|
||||||
val doc = app.get(url).document
|
val doc = app.get(url).document
|
||||||
val links = doc.select("div#list-server-more > ul > li.linkserver")
|
val links = doc.select("div#list-server-more > ul > li.linkserver")
|
||||||
if (!links.isNullOrEmpty()) {
|
if (!links.isNullOrEmpty()) {
|
||||||
links.apmap {
|
links.amap {
|
||||||
val datavid = it.attr("data-video") ?: ""
|
val datavid = it.attr("data-video") ?: ""
|
||||||
//Log.i("AsianEmbed", "Result => (datavid) ${datavid}")
|
//Log.i("AsianEmbed", "Result => (datavid) ${datavid}")
|
||||||
if (datavid.isNotBlank()) {
|
if (datavid.isNotBlank()) {
|
||||||
|
|
|
@ -39,7 +39,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
): Boolean {
|
): Boolean {
|
||||||
tryParseJson<CrossMetaData>(data)?.let { metaData ->
|
tryParseJson<CrossMetaData>(data)?.let { metaData ->
|
||||||
if (!metaData.isSuccess) return false
|
if (!metaData.isSuccess) return false
|
||||||
metaData.movies?.apmap { (apiName, data) ->
|
metaData.movies?.amap { (apiName, data) ->
|
||||||
getApiFromNameNull(apiName)?.let {
|
getApiFromNameNull(apiName)?.let {
|
||||||
try {
|
try {
|
||||||
it.loadLinks(data, isCasting, subtitleCallback, callback)
|
it.loadLinks(data, isCasting, subtitleCallback, callback)
|
||||||
|
@ -64,10 +64,10 @@ class CrossTmdbProvider : TmdbProvider() {
|
||||||
val matchName = filterName(this.name)
|
val matchName = filterName(this.name)
|
||||||
when (this) {
|
when (this) {
|
||||||
is MovieLoadResponse -> {
|
is MovieLoadResponse -> {
|
||||||
val data = validApis.apmap { api ->
|
val data = validApis.amap { api ->
|
||||||
try {
|
try {
|
||||||
if (api.supportedTypes.contains(TvType.Movie)) { //|| api.supportedTypes.contains(TvType.AnimeMovie)
|
if (api.supportedTypes.contains(TvType.Movie)) { //|| api.supportedTypes.contains(TvType.AnimeMovie)
|
||||||
return@apmap api.search(this.name)?.first {
|
return@amap api.search(this.name)?.first {
|
||||||
if (filterName(it.name).equals(
|
if (filterName(it.name).equals(
|
||||||
matchName,
|
matchName,
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
|
|
|
@ -45,7 +45,7 @@ class MultiAnimeProvider : MainAPI() {
|
||||||
|
|
||||||
override suspend fun load(url: String): LoadResponse? {
|
override suspend fun load(url: String): LoadResponse? {
|
||||||
return syncApi.getResult(url)?.let { res ->
|
return syncApi.getResult(url)?.let { res ->
|
||||||
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url ->
|
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
|
||||||
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
|
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
|
||||||
}.filterNotNull()
|
}.filterNotNull()
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.bumptech.glide.load.HttpException
|
||||||
import com.lagradost.cloudstream3.BuildConfig
|
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.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import javax.net.ssl.SSLHandshakeException
|
import javax.net.ssl.SSLHandshakeException
|
||||||
|
@ -14,6 +15,7 @@ import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!"
|
const val DEBUG_EXCEPTION = "THIS IS A DEBUG EXCEPTION!"
|
||||||
|
const val DEBUG_PRINT = "DEBUG PRINT"
|
||||||
|
|
||||||
class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message")
|
class DebugException(message: String) : Exception("$DEBUG_EXCEPTION\n$message")
|
||||||
|
|
||||||
|
@ -23,6 +25,12 @@ inline fun debugException(message: () -> String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun debugPrint(tag: String = DEBUG_PRINT, message: () -> String) {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
Log.d(tag, message.invoke())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inline fun debugWarning(message: () -> String) {
|
inline fun debugWarning(message: () -> String) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
logError(DebugException(message.invoke()))
|
logError(DebugException(message.invoke()))
|
||||||
|
@ -41,10 +49,14 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
|
||||||
liveData.observe(this) { it?.let { t -> action(t) } }
|
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> {
|
inline fun <reified T : Any> some(value: T?): Some<T> {
|
||||||
return if (value == null) {
|
return if (value == null) {
|
||||||
Some.None
|
Some.None
|
||||||
|
@ -157,7 +169,7 @@ suspend fun <T> safeApiCall(
|
||||||
}
|
}
|
||||||
safeFail(throwable)
|
safeFail(throwable)
|
||||||
}
|
}
|
||||||
is SocketTimeoutException -> {
|
is SocketTimeoutException, is InterruptedIOException -> {
|
||||||
Resource.Failure(
|
Resource.Failure(
|
||||||
true,
|
true,
|
||||||
null,
|
null,
|
||||||
|
@ -192,7 +204,7 @@ suspend fun <T> safeApiCall(
|
||||||
true,
|
true,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
(throwable.message ?: "SSLHandshakeException") + "\nTry again later."
|
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> safeFail(throwable)
|
else -> safeFail(throwable)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.webkit.CookieManager
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.debugWarning
|
import com.lagradost.cloudstream3.mvvm.debugWarning
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.nicehttp.Requests.Companion.await
|
import com.lagradost.nicehttp.Requests.Companion.await
|
||||||
import com.lagradost.nicehttp.cookies
|
import com.lagradost.nicehttp.cookies
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -26,7 +27,10 @@ class CloudflareKiller : Interceptor {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Needs to clear cookies between sessions to generate new cookies.
|
// Needs to clear cookies between sessions to generate new cookies.
|
||||||
CookieManager.getInstance().removeAllCookies(null)
|
normalSafeApiCall {
|
||||||
|
// This can throw an exception on unsupported devices :(
|
||||||
|
CookieManager.getInstance().removeAllCookies(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf()
|
val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf()
|
||||||
|
@ -35,7 +39,7 @@ class CloudflareKiller : Interceptor {
|
||||||
* Gets the headers with cookies, webview user agent included!
|
* Gets the headers with cookies, webview user agent included!
|
||||||
* */
|
* */
|
||||||
fun getCookieHeaders(url: String): Headers {
|
fun getCookieHeaders(url: String): Headers {
|
||||||
val userAgentHeaders = WebViewResolver.webViewUserAgent?.let {
|
val userAgentHeaders = WebViewResolver.webViewUserAgent?.let {
|
||||||
mapOf("user-agent" to it)
|
mapOf("user-agent" to it)
|
||||||
} ?: emptyMap()
|
} ?: emptyMap()
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.network
|
||||||
|
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.nicehttp.Requests.Companion.await
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.cookies
|
import com.lagradost.nicehttp.cookies
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
|
@ -41,7 +41,8 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
|
||||||
savedCookiesMap[request.url.host]
|
savedCookiesMap[request.url.host]
|
||||||
// If no cookies are found fetch and save em.
|
// If no cookies are found fetch and save em.
|
||||||
?: (request.url.scheme + "://" + request.url.host + (ddosBypassPath ?: "")).let {
|
?: (request.url.scheme + "://" + request.url.host + (ddosBypassPath ?: "")).let {
|
||||||
app.get(it, cacheTime = 0).cookies.also { cookies ->
|
// Somehow app.get fails
|
||||||
|
Requests().get(it).cookies.also { cookies ->
|
||||||
savedCookiesMap[request.url.host] = cookies
|
savedCookiesMap[request.url.host] = cookies
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,6 +52,6 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
|
||||||
request.newBuilder()
|
request.newBuilder()
|
||||||
.headers(headers)
|
.headers(headers)
|
||||||
.build()
|
.build()
|
||||||
).await()
|
).execute()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -65,3 +65,23 @@ fun OkHttpClient.Builder.addAdGuardDns() = (
|
||||||
"94.140.14.141",
|
"94.140.14.141",
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.addDNSWatchDns() = (
|
||||||
|
addGenericDns(
|
||||||
|
"https://resolver2.dns.watch/dns-query",
|
||||||
|
// https://dns.watch/
|
||||||
|
listOf(
|
||||||
|
"84.200.69.80",
|
||||||
|
"84.200.70.40",
|
||||||
|
)
|
||||||
|
))
|
||||||
|
|
||||||
|
fun OkHttpClient.Builder.addQuad9Dns() = (
|
||||||
|
addGenericDns(
|
||||||
|
"https://dns.quad9.net/dns-query",
|
||||||
|
// https://www.quad9.net/service/service-addresses-and-features
|
||||||
|
listOf(
|
||||||
|
"9.9.9.9",
|
||||||
|
"149.112.112.112",
|
||||||
|
)
|
||||||
|
))
|
|
@ -4,19 +4,19 @@ import android.content.Context
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.nicehttp.Requests
|
import com.lagradost.nicehttp.Requests
|
||||||
import com.lagradost.nicehttp.getCookies
|
|
||||||
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
import com.lagradost.nicehttp.ignoreAllSSLErrors
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.Headers.Companion.toHeaders
|
import okhttp3.Headers.Companion.toHeaders
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import org.conscrypt.Conscrypt
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.TimeUnit
|
import java.security.Security
|
||||||
|
|
||||||
|
|
||||||
fun Requests.initClient(context: Context): OkHttpClient {
|
fun Requests.initClient(context: Context): OkHttpClient {
|
||||||
|
normalSafeApiCall { Security.insertProviderAt(Conscrypt.newProvider(), 1) }
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
|
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
|
||||||
baseClient = OkHttpClient.Builder()
|
baseClient = OkHttpClient.Builder()
|
||||||
|
@ -36,6 +36,8 @@ fun Requests.initClient(context: Context): OkHttpClient {
|
||||||
2 -> addCloudFlareDns()
|
2 -> addCloudFlareDns()
|
||||||
// 3 -> addOpenDns()
|
// 3 -> addOpenDns()
|
||||||
4 -> addAdGuardDns()
|
4 -> addAdGuardDns()
|
||||||
|
5 -> addDNSWatchDns()
|
||||||
|
6 -> addQuad9Dns()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Needs to be build as otherwise the other builders will change this object
|
// Needs to be build as otherwise the other builders will change this object
|
||||||
|
|
|
@ -7,9 +7,12 @@ import com.lagradost.cloudstream3.AcraApplication
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
import com.lagradost.cloudstream3.USER_AGENT
|
import com.lagradost.cloudstream3.USER_AGENT
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.mvvm.debugException
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
import com.lagradost.nicehttp.requestCreator
|
import com.lagradost.nicehttp.requestCreator
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
@ -64,9 +67,15 @@ class WebViewResolver(
|
||||||
method: String = "GET",
|
method: String = "GET",
|
||||||
requestCallBack: (Request) -> Boolean = { false },
|
requestCallBack: (Request) -> Boolean = { false },
|
||||||
): Pair<Request?, List<Request>> {
|
): Pair<Request?, List<Request>> {
|
||||||
return resolveUsingWebView(
|
return try {
|
||||||
requestCreator(method, url, referer = referer), requestCallBack
|
resolveUsingWebView(
|
||||||
)
|
requestCreator(method, url, referer = referer), requestCallBack
|
||||||
|
)
|
||||||
|
} catch (e: java.lang.IllegalArgumentException) {
|
||||||
|
logError(e)
|
||||||
|
debugException { "ILLEGAL URL IN resolveUsingWebView!" }
|
||||||
|
return null to emptyList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -96,7 +105,7 @@ class WebViewResolver(
|
||||||
}
|
}
|
||||||
|
|
||||||
var fixedRequest: Request? = null
|
var fixedRequest: Request? = null
|
||||||
val extraRequestList = mutableListOf<Request>()
|
val extraRequestList = threadSafeListOf<Request>()
|
||||||
|
|
||||||
main {
|
main {
|
||||||
// Useful for debugging
|
// Useful for debugging
|
||||||
|
@ -128,7 +137,7 @@ class WebViewResolver(
|
||||||
println("Loading WebView URL: $webViewUrl")
|
println("Loading WebView URL: $webViewUrl")
|
||||||
|
|
||||||
if (interceptUrl.containsMatchIn(webViewUrl)) {
|
if (interceptUrl.containsMatchIn(webViewUrl)) {
|
||||||
fixedRequest = request.toRequest().also {
|
fixedRequest = request.toRequest()?.also {
|
||||||
requestCallBack(it)
|
requestCallBack(it)
|
||||||
}
|
}
|
||||||
println("Web-view request finished: $webViewUrl")
|
println("Web-view request finished: $webViewUrl")
|
||||||
|
@ -137,9 +146,9 @@ class WebViewResolver(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
|
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
|
||||||
extraRequestList.add(request.toRequest().also {
|
request.toRequest()?.also {
|
||||||
if (requestCallBack(it)) destroyWebView()
|
if (requestCallBack(it)) destroyWebView()
|
||||||
})
|
}?.let(extraRequestList::add)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Suppress image requests as we don't display them anywhere
|
// Suppress image requests as we don't display them anywhere
|
||||||
|
@ -250,14 +259,19 @@ class WebViewResolver(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun WebResourceRequest.toRequest(): Request {
|
fun WebResourceRequest.toRequest(): Request? {
|
||||||
val webViewUrl = this.url.toString()
|
val webViewUrl = this.url.toString()
|
||||||
|
|
||||||
return requestCreator(
|
// If invalid url then it can crash with
|
||||||
this.method,
|
// java.lang.IllegalArgumentException: Expected URL scheme 'http' or 'https' but was 'data'
|
||||||
webViewUrl,
|
// At Request.Builder().url(addParamsToUrl(url, params))
|
||||||
this.requestHeaders,
|
return normalSafeApiCall {
|
||||||
)
|
requestCreator(
|
||||||
|
this.method,
|
||||||
|
webViewUrl,
|
||||||
|
this.requestHeaders,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Response.toWebResourceResponse(): WebResourceResponse {
|
fun Response.toWebResourceResponse(): WebResourceResponse {
|
||||||
|
|
|
@ -1,35 +1,46 @@
|
||||||
package com.lagradost.cloudstream3.plugins
|
package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
import dalvik.system.PathClassLoader
|
import android.app.*
|
||||||
import com.google.gson.Gson
|
import android.content.Context
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.widget.Toast
|
|
||||||
import android.app.Activity
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
||||||
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.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
|
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
|
|
||||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||||
|
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
|
||||||
|
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||||
|
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
|
||||||
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
||||||
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
|
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
|
||||||
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
||||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import org.acra.log.debug
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -38,6 +49,9 @@ import java.util.*
|
||||||
const val PLUGINS_KEY = "PLUGINS_KEY"
|
const val PLUGINS_KEY = "PLUGINS_KEY"
|
||||||
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
|
const val PLUGINS_KEY_LOCAL = "PLUGINS_KEY_LOCAL"
|
||||||
|
|
||||||
|
const val EXTENSIONS_CHANNEL_ID = "cloudstream3.extensions"
|
||||||
|
const val EXTENSIONS_CHANNEL_NAME = "Extensions"
|
||||||
|
const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel"
|
||||||
|
|
||||||
// Data class for internal storage
|
// Data class for internal storage
|
||||||
data class PluginData(
|
data class PluginData(
|
||||||
|
@ -78,6 +92,8 @@ object PluginManager {
|
||||||
|
|
||||||
const val TAG = "PluginManager"
|
const val TAG = "PluginManager"
|
||||||
|
|
||||||
|
private var hasCreatedNotChanel = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store data about the plugin for fetching later
|
* Store data about the plugin for fetching later
|
||||||
* */
|
* */
|
||||||
|
@ -112,6 +128,10 @@ object PluginManager {
|
||||||
val plugins = getPluginsOnline().filter {
|
val plugins = getPluginsOnline().filter {
|
||||||
!it.filePath.contains(repositoryPath)
|
!it.filePath.contains(repositoryPath)
|
||||||
}
|
}
|
||||||
|
val file = File(repositoryPath)
|
||||||
|
normalSafeApiCall {
|
||||||
|
if (file.exists()) file.deleteRecursively()
|
||||||
|
}
|
||||||
setKey(PLUGINS_KEY, plugins)
|
setKey(PLUGINS_KEY, plugins)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,8 +183,16 @@ object PluginManager {
|
||||||
val onlineData: Pair<String, SitePlugin>,
|
val onlineData: Pair<String, SitePlugin>,
|
||||||
) {
|
) {
|
||||||
val isOutdated =
|
val isOutdated =
|
||||||
onlineData.second.version != savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE
|
||||||
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
|
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN
|
||||||
|
|
||||||
|
fun validOnlineData(context: Context): Boolean {
|
||||||
|
return getPluginPath(
|
||||||
|
context,
|
||||||
|
savedData.internalName,
|
||||||
|
onlineData.first
|
||||||
|
).absolutePath == savedData.filePath
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
|
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
|
||||||
|
@ -196,10 +224,7 @@ object PluginManager {
|
||||||
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
|
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
|
||||||
// Load all plugins as fast as possible!
|
// Load all plugins as fast as possible!
|
||||||
loadAllOnlinePlugins(activity)
|
loadAllOnlinePlugins(activity)
|
||||||
|
afterPluginsLoadedEvent.invoke(false)
|
||||||
ioSafe {
|
|
||||||
afterPluginsLoadedEvent.invoke(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
||||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||||
|
@ -210,36 +235,137 @@ object PluginManager {
|
||||||
|
|
||||||
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
|
||||||
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
val outdatedPlugins = getPluginsOnline().map { savedData ->
|
||||||
onlinePlugins.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
onlinePlugins
|
||||||
|
.filter { onlineData -> savedData.internalName == onlineData.second.internalName }
|
||||||
.map { onlineData ->
|
.map { onlineData ->
|
||||||
OnlinePluginData(savedData, onlineData)
|
OnlinePluginData(savedData, onlineData)
|
||||||
|
}.filter {
|
||||||
|
it.validOnlineData(activity)
|
||||||
}
|
}
|
||||||
}.flatten().distinctBy { it.onlineData.second.url }
|
}.flatten().distinctBy { it.onlineData.second.url }
|
||||||
|
|
||||||
debug {
|
debugPrint {
|
||||||
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
|
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val updatedPlugins = mutableListOf<String>()
|
||||||
|
|
||||||
outdatedPlugins.apmap { pluginData ->
|
outdatedPlugins.apmap { pluginData ->
|
||||||
if (pluginData.isDisabled) {
|
if (pluginData.isDisabled) {
|
||||||
|
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
|
||||||
unloadPlugin(pluginData.savedData.filePath)
|
unloadPlugin(pluginData.savedData.filePath)
|
||||||
} else if (pluginData.isOutdated) {
|
} else if (pluginData.isOutdated) {
|
||||||
downloadAndLoadPlugin(
|
downloadPlugin(
|
||||||
activity,
|
activity,
|
||||||
pluginData.onlineData.second.url,
|
pluginData.onlineData.second.url,
|
||||||
pluginData.savedData.internalName,
|
pluginData.savedData.internalName,
|
||||||
pluginData.onlineData.first
|
File(pluginData.savedData.filePath),
|
||||||
)
|
true
|
||||||
|
).let { success ->
|
||||||
|
if (success)
|
||||||
|
updatedPlugins.add(pluginData.onlineData.second.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ioSafe {
|
main {
|
||||||
afterPluginsLoadedEvent.invoke(true)
|
val uitext = txt(R.string.plugins_updated, updatedPlugins.size)
|
||||||
|
createNotification(activity, uitext, updatedPlugins)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ioSafe {
|
||||||
|
afterPluginsLoadedEvent.invoke(false)
|
||||||
|
// }
|
||||||
|
|
||||||
Log.i(TAG, "Plugin update done!")
|
Log.i(TAG, "Plugin update done!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically download plugins not yet existing on local
|
||||||
|
* 1. Gets all online data from online plugins repo
|
||||||
|
* 2. Fetch all not downloaded plugins
|
||||||
|
* 3. Download them and reload plugins
|
||||||
|
**/
|
||||||
|
fun downloadNotExistingPluginsAndLoad(activity: Activity) {
|
||||||
|
val newDownloadPlugins = mutableListOf<String>()
|
||||||
|
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
||||||
|
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||||
|
val onlinePlugins = urls.toList().apmap {
|
||||||
|
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||||
|
}.flatten().distinctBy { it.second.url }
|
||||||
|
|
||||||
|
val providerLang = activity.getApiProviderLangSettings()
|
||||||
|
//Log.i(TAG, "providerLang => ${providerLang.toJson()}")
|
||||||
|
|
||||||
|
// Iterate online repos and returns not downloaded plugins
|
||||||
|
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
|
||||||
|
val sitePlugin = onlineData.second
|
||||||
|
//Don't include empty urls
|
||||||
|
if (sitePlugin.url.isBlank()) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
if (sitePlugin.repositoryUrl.isNullOrBlank()) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
|
||||||
|
//Omit already existing plugins
|
||||||
|
if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) {
|
||||||
|
Log.i(TAG, "Skip > ${sitePlugin.internalName}")
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
|
||||||
|
//Omit lang not selected on language setting
|
||||||
|
val lang = sitePlugin.language ?: return@mapNotNull null
|
||||||
|
//If set to 'universal', don't skip any language
|
||||||
|
if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
//Log.i(TAG, "sitePlugin lang => $lang")
|
||||||
|
|
||||||
|
//Omit NSFW, if disabled
|
||||||
|
sitePlugin.tvTypes?.let { tvtypes ->
|
||||||
|
if (!settingsForProvider.enableAdult) {
|
||||||
|
if (tvtypes.contains(TvType.NSFW.name)) {
|
||||||
|
return@mapNotNull null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val savedData = PluginData(
|
||||||
|
url = sitePlugin.url,
|
||||||
|
internalName = sitePlugin.internalName,
|
||||||
|
isOnline = true,
|
||||||
|
filePath = "",
|
||||||
|
version = sitePlugin.version
|
||||||
|
)
|
||||||
|
OnlinePluginData(savedData, onlineData)
|
||||||
|
}
|
||||||
|
//Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
|
||||||
|
|
||||||
|
notDownloadedPlugins.apmap { pluginData ->
|
||||||
|
downloadPlugin(
|
||||||
|
activity,
|
||||||
|
pluginData.onlineData.second.url,
|
||||||
|
pluginData.savedData.internalName,
|
||||||
|
pluginData.onlineData.first,
|
||||||
|
!pluginData.isDisabled
|
||||||
|
).let { success ->
|
||||||
|
if (success)
|
||||||
|
newDownloadPlugins.add(pluginData.onlineData.second.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
val uitext = txt(R.string.plugins_downloaded, newDownloadPlugins.size)
|
||||||
|
createNotification(activity, uitext, newDownloadPlugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ioSafe {
|
||||||
|
afterPluginsLoadedEvent.invoke(false)
|
||||||
|
// }
|
||||||
|
|
||||||
|
Log.i(TAG, "Plugin download done!")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use updateAllOnlinePluginsAndLoadThem
|
* Use updateAllOnlinePluginsAndLoadThem
|
||||||
* */
|
* */
|
||||||
|
@ -254,7 +380,23 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAllLocalPlugins(activity: Activity) {
|
/**
|
||||||
|
* Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb
|
||||||
|
**/
|
||||||
|
fun hotReloadAllLocalPlugins(activity: FragmentActivity?) {
|
||||||
|
Log.d(TAG, "Reloading all local plugins!")
|
||||||
|
if (activity == null) return
|
||||||
|
getPluginsLocal().forEach {
|
||||||
|
unloadPlugin(it.filePath)
|
||||||
|
}
|
||||||
|
loadAllLocalPlugins(activity, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins
|
||||||
|
* and reload all pages even if they are previously valid
|
||||||
|
**/
|
||||||
|
fun loadAllLocalPlugins(activity: Activity, forceReload: Boolean) {
|
||||||
val dir = File(LOCAL_PLUGINS_PATH)
|
val dir = File(LOCAL_PLUGINS_PATH)
|
||||||
removeKey(PLUGINS_KEY_LOCAL)
|
removeKey(PLUGINS_KEY_LOCAL)
|
||||||
|
|
||||||
|
@ -276,7 +418,7 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadedLocalPlugins = true
|
loadedLocalPlugins = true
|
||||||
afterPluginsLoadedEvent.invoke(true)
|
afterPluginsLoadedEvent.invoke(forceReload)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -339,9 +481,7 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
plugins[filePath] = pluginInstance
|
plugins[filePath] = pluginInstance
|
||||||
classLoaders[loader] = pluginInstance
|
classLoaders[loader] = pluginInstance
|
||||||
if (data.url != null) { // TODO: make this cleaner
|
urlPlugins[data.url ?: filePath] = pluginInstance
|
||||||
urlPlugins[data.url] = pluginInstance
|
|
||||||
}
|
|
||||||
pluginInstance.load(activity)
|
pluginInstance.load(activity)
|
||||||
Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
|
Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
|
||||||
currentlyLoading = null
|
currentlyLoading = null
|
||||||
|
@ -358,7 +498,7 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unloadPlugin(absolutePath: String) {
|
fun unloadPlugin(absolutePath: String) {
|
||||||
Log.i(TAG, "Unloading plugin: $absolutePath")
|
Log.i(TAG, "Unloading plugin: $absolutePath")
|
||||||
val plugin = plugins[absolutePath]
|
val plugin = plugins[absolutePath]
|
||||||
if (plugin == null) {
|
if (plugin == null) {
|
||||||
|
@ -382,6 +522,7 @@ object PluginManager {
|
||||||
classLoaders.values.removeIf { v -> v == plugin }
|
classLoaders.values.removeIf { v -> v == plugin }
|
||||||
|
|
||||||
plugins.remove(absolutePath)
|
plugins.remove(absolutePath)
|
||||||
|
urlPlugins.values.removeIf { v -> v == plugin }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -395,43 +536,75 @@ object PluginManager {
|
||||||
) + "." + name.hashCode()
|
) + "." + name.hashCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadAndLoadPlugin(
|
/**
|
||||||
|
* This should not be changed as it is used to also detect if a plugin is installed!
|
||||||
|
**/
|
||||||
|
fun getPluginPath(
|
||||||
|
context: Context,
|
||||||
|
internalName: String,
|
||||||
|
repositoryUrl: String
|
||||||
|
): File {
|
||||||
|
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
|
||||||
|
val fileName = getPluginSanitizedFileName(internalName)
|
||||||
|
return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadPlugin(
|
||||||
activity: Activity,
|
activity: Activity,
|
||||||
pluginUrl: String,
|
pluginUrl: String,
|
||||||
internalName: String,
|
internalName: String,
|
||||||
repositoryUrl: String
|
repositoryUrl: String,
|
||||||
|
loadPlugin: Boolean
|
||||||
|
): Boolean {
|
||||||
|
val file = getPluginPath(activity, internalName, repositoryUrl)
|
||||||
|
return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun downloadPlugin(
|
||||||
|
activity: Activity,
|
||||||
|
pluginUrl: String,
|
||||||
|
internalName: String,
|
||||||
|
file: File,
|
||||||
|
loadPlugin: Boolean
|
||||||
): Boolean {
|
): Boolean {
|
||||||
try {
|
try {
|
||||||
val folderName = getPluginSanitizedFileName(repositoryUrl) // Guaranteed unique
|
Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}")
|
||||||
val fileName = getPluginSanitizedFileName(internalName)
|
|
||||||
unloadPlugin("${activity.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
|
|
||||||
|
|
||||||
Log.d(TAG, "Downloading plugin: $pluginUrl to $folderName/$fileName")
|
|
||||||
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
|
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
|
||||||
val file = downloadPluginToFile(activity, pluginUrl, fileName, folderName)
|
val newFile = downloadPluginToFile(pluginUrl, file) ?: return false
|
||||||
return loadPlugin(
|
|
||||||
activity,
|
val data = PluginData(
|
||||||
file ?: return false,
|
internalName,
|
||||||
PluginData(internalName, pluginUrl, true, file.absolutePath, PLUGIN_VERSION_NOT_SET)
|
pluginUrl,
|
||||||
|
true,
|
||||||
|
newFile.absolutePath,
|
||||||
|
PLUGIN_VERSION_NOT_SET
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return if (loadPlugin) {
|
||||||
|
unloadPlugin(file.absolutePath)
|
||||||
|
loadPlugin(
|
||||||
|
activity,
|
||||||
|
newFile,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setPluginData(data)
|
||||||
|
true
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
suspend fun deletePlugin(file: File): Boolean {
|
||||||
* @param isFilePath will treat the pluginUrl as as the filepath instead of url
|
val list =
|
||||||
* */
|
(getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
|
||||||
suspend fun deletePlugin(pluginIdentifier: String, isFilePath: Boolean): Boolean {
|
|
||||||
val data =
|
|
||||||
(if (isFilePath) (getPluginsLocal() + getPluginsOnline()).firstOrNull { it.filePath == pluginIdentifier }
|
|
||||||
else getPluginsOnline().firstOrNull { it.url == pluginIdentifier }) ?: return false
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
if (File(data.filePath).delete()) {
|
if (File(file.absolutePath).delete()) {
|
||||||
unloadPlugin(data.filePath)
|
unloadPlugin(file.absolutePath)
|
||||||
deletePluginData(data)
|
list.forEach { deletePluginData(it) }
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
@ -439,4 +612,66 @@ object PluginManager {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Context.createNotificationChannel() {
|
||||||
|
hasCreatedNotChanel = true
|
||||||
|
// Create the NotificationChannel, but only on API 26+ because
|
||||||
|
// the NotificationChannel class is new and not in the support library
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val name = EXTENSIONS_CHANNEL_NAME //getString(R.string.channel_name)
|
||||||
|
val descriptionText =
|
||||||
|
EXTENSIONS_CHANNEL_DESCRIPT//getString(R.string.channel_description)
|
||||||
|
val importance = NotificationManager.IMPORTANCE_LOW
|
||||||
|
val channel = NotificationChannel(EXTENSIONS_CHANNEL_ID, name, importance).apply {
|
||||||
|
description = descriptionText
|
||||||
|
}
|
||||||
|
// Register the channel with the system
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotification(
|
||||||
|
context: Context,
|
||||||
|
uitext: UiText,
|
||||||
|
extensions: List<String>
|
||||||
|
): Notification? {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (extensions.isEmpty()) return null
|
||||||
|
|
||||||
|
val content = extensions.joinToString(", ")
|
||||||
|
// main { // DON'T WANT TO SLOW IT DOWN
|
||||||
|
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setColorized(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||||
|
.setContentTitle(uitext.asString(context))
|
||||||
|
//.setContentTitle(context.getString(title, extensionNames.size))
|
||||||
|
.setSmallIcon(R.drawable.ic_baseline_extension_24)
|
||||||
|
.setStyle(
|
||||||
|
NotificationCompat.BigTextStyle()
|
||||||
|
.bigText(content)
|
||||||
|
)
|
||||||
|
.setContentText(content)
|
||||||
|
|
||||||
|
if (!hasCreatedNotChanel) {
|
||||||
|
context.createNotificationChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
val notification = builder.build()
|
||||||
|
with(NotificationManagerCompat.from(context)) {
|
||||||
|
// notificationId is a unique int for each notification that you must define
|
||||||
|
notify((System.currentTimeMillis() / 1000).toInt(), notification)
|
||||||
|
}
|
||||||
|
return notification
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logError(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,11 +4,13 @@ import android.content.Context
|
||||||
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.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.apmap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
|
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
|
||||||
|
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
|
||||||
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
|
||||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
@ -70,6 +72,28 @@ object RepositoryManager {
|
||||||
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun parseRepoUrl(url: String): String? {
|
||||||
|
val fixedUrl = url.trim()
|
||||||
|
return if (fixedUrl.contains("^https?://".toRegex())) {
|
||||||
|
fixedUrl
|
||||||
|
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
|
||||||
|
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
|
||||||
|
return@let if (!it.contains("^https?://".toRegex()))
|
||||||
|
"https://${it}"
|
||||||
|
else fixedUrl
|
||||||
|
}
|
||||||
|
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
|
||||||
|
suspendSafeApiCall {
|
||||||
|
app.get("https://l.cloudstream.cf/${fixedUrl}").let {
|
||||||
|
return@let if (it.isSuccessful && !it.url.startsWith("https://cutt.ly/branded-domains")) it.url
|
||||||
|
else app.get("https://cutt.ly/${fixedUrl}").let let2@{ it2 ->
|
||||||
|
return@let2 if (it2.isSuccessful) it2.url else null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun parseRepository(url: String): Repository? {
|
suspend fun parseRepository(url: String): Repository? {
|
||||||
return suspendSafeApiCall {
|
return suspendSafeApiCall {
|
||||||
// Take manifestVersion and such into account later
|
// Take manifestVersion and such into account later
|
||||||
|
@ -84,7 +108,7 @@ object RepositoryManager {
|
||||||
// Normal parsed function not working?
|
// Normal parsed function not working?
|
||||||
// return response.parsedSafe()
|
// return response.parsedSafe()
|
||||||
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
|
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
|
||||||
} catch (t : Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
|
@ -95,7 +119,7 @@ object RepositoryManager {
|
||||||
* */
|
* */
|
||||||
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
|
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
|
||||||
val repo = parseRepository(repositoryUrl) ?: return null
|
val repo = parseRepository(repositoryUrl) ?: return null
|
||||||
return repo.pluginLists.apmap { url ->
|
return repo.pluginLists.amap { url ->
|
||||||
parsePlugins(url).map {
|
parsePlugins(url).map {
|
||||||
repositoryUrl to it
|
repositoryUrl to it
|
||||||
}
|
}
|
||||||
|
@ -103,29 +127,21 @@ object RepositoryManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun downloadPluginToFile(
|
suspend fun downloadPluginToFile(
|
||||||
context: Context,
|
|
||||||
pluginUrl: String,
|
pluginUrl: String,
|
||||||
fileName: String,
|
file: File
|
||||||
folder: String
|
|
||||||
): File? {
|
): File? {
|
||||||
return suspendSafeApiCall {
|
return suspendSafeApiCall {
|
||||||
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
|
file.mkdirs()
|
||||||
if (!extensionsDir.exists())
|
|
||||||
extensionsDir.mkdirs()
|
|
||||||
|
|
||||||
val newDir = File(extensionsDir, folder)
|
|
||||||
newDir.mkdirs()
|
|
||||||
|
|
||||||
val newFile = File(newDir, "${fileName}.cs3")
|
|
||||||
// Overwrite if exists
|
// Overwrite if exists
|
||||||
if (newFile.exists()) {
|
if (file.exists()) {
|
||||||
newFile.delete()
|
file.delete()
|
||||||
}
|
}
|
||||||
newFile.createNewFile()
|
file.createNewFile()
|
||||||
|
|
||||||
val body = app.get(pluginUrl).okhttpResponse.body
|
val body = app.get(pluginUrl).okhttpResponse.body
|
||||||
write(body.byteStream(), newFile.outputStream())
|
write(body.byteStream(), file.outputStream())
|
||||||
newFile
|
file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,9 +176,17 @@ object RepositoryManager {
|
||||||
extensionsDir,
|
extensionsDir,
|
||||||
getPluginSanitizedFileName(repository.url)
|
getPluginSanitizedFileName(repository.url)
|
||||||
)
|
)
|
||||||
PluginManager.deleteRepositoryData(file.absolutePath)
|
|
||||||
|
|
||||||
file.delete()
|
// Unload all plugins, not using deletePlugin since we
|
||||||
|
// delete all data and files in deleteRepositoryData
|
||||||
|
normalSafeApiCall {
|
||||||
|
file.listFiles { plugin: File ->
|
||||||
|
unloadPlugin(plugin.absolutePath)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginManager.deleteRepositoryData(file.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun write(stream: InputStream, output: OutputStream) {
|
private fun write(stream: InputStream, output: OutputStream) {
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
object VotingApi { // please do not cheat the votes lol
|
||||||
|
private const val LOGKEY = "VotingApi"
|
||||||
|
|
||||||
|
enum class VoteType(val value: Int) {
|
||||||
|
UPVOTE(1),
|
||||||
|
DOWNVOTE(-1),
|
||||||
|
NONE(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val apiDomain = "https://api.countapi.xyz"
|
||||||
|
|
||||||
|
private fun transformUrl(url: String): String = // dont touch or all votes get reset
|
||||||
|
MessageDigest
|
||||||
|
.getInstance("SHA-256")
|
||||||
|
.digest("${url}#funny-salt".toByteArray())
|
||||||
|
.fold("") { str, it -> str + "%02x".format(it) }
|
||||||
|
|
||||||
|
suspend fun SitePlugin.getVotes(): Int {
|
||||||
|
return getVotes(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun SitePlugin.vote(requestType: VoteType): Int {
|
||||||
|
return vote(url, requestType)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SitePlugin.getVoteType(): VoteType {
|
||||||
|
return getVoteType(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SitePlugin.canVote(): Boolean {
|
||||||
|
return canVote(this.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin url to Int
|
||||||
|
private val votesCache = mutableMapOf<String, Int>()
|
||||||
|
|
||||||
|
suspend fun getVotes(pluginUrl: String): Int {
|
||||||
|
val url = "${apiDomain}/get/cs3-votes/${transformUrl(pluginUrl)}"
|
||||||
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
|
return votesCache[pluginUrl] ?: app.get(url).parsedSafe<Result>()?.value?.also {
|
||||||
|
votesCache[pluginUrl] = it
|
||||||
|
} ?: (0.also {
|
||||||
|
ioSafe {
|
||||||
|
createBucket(pluginUrl)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getVoteType(pluginUrl: String): VoteType {
|
||||||
|
return getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun createBucket(pluginUrl: String) {
|
||||||
|
val url =
|
||||||
|
"${apiDomain}/create?namespace=cs3-votes&key=${transformUrl(pluginUrl)}&value=0&update_lowerbound=-2&update_upperbound=2&enable_reset=0"
|
||||||
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
|
app.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun canVote(pluginUrl: String): Boolean {
|
||||||
|
if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val voteLock = Mutex()
|
||||||
|
suspend fun vote(pluginUrl: String, requestType: VoteType): Int {
|
||||||
|
// Prevent multiple requests at the same time.
|
||||||
|
voteLock.withLock {
|
||||||
|
if (!canVote(pluginUrl)) {
|
||||||
|
main {
|
||||||
|
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
return getVotes(pluginUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
val savedType: VoteType =
|
||||||
|
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: VoteType.NONE
|
||||||
|
|
||||||
|
val newType = if (requestType == savedType) VoteType.NONE else requestType
|
||||||
|
val changeValue = if (requestType == savedType) {
|
||||||
|
-requestType.value
|
||||||
|
} else if (savedType == VoteType.NONE) {
|
||||||
|
requestType.value
|
||||||
|
} else if (savedType != requestType) {
|
||||||
|
-savedType.value + requestType.value
|
||||||
|
} else 0
|
||||||
|
|
||||||
|
// Pre-emptively set vote key
|
||||||
|
setKey("cs3-votes/${transformUrl(pluginUrl)}", newType)
|
||||||
|
|
||||||
|
val url =
|
||||||
|
"${apiDomain}/update/cs3-votes/${transformUrl(pluginUrl)}?amount=${changeValue}"
|
||||||
|
Log.d(LOGKEY, "Requesting: $url")
|
||||||
|
val res = app.get(url).parsedSafe<Result>()?.value
|
||||||
|
|
||||||
|
if (res == null) {
|
||||||
|
// "Refund" key if the response is invalid
|
||||||
|
setKey("cs3-votes/${transformUrl(pluginUrl)}", savedType)
|
||||||
|
} else {
|
||||||
|
votesCache[pluginUrl] = res
|
||||||
|
}
|
||||||
|
return res ?: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class Result(
|
||||||
|
val value: Int?
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,7 +13,8 @@ class AbstractSubtitleEntities {
|
||||||
var epNumber: Int? = null,
|
var epNumber: Int? = null,
|
||||||
var seasonNumber: Int? = null,
|
var seasonNumber: Int? = null,
|
||||||
var year: Int? = null,
|
var year: Int? = null,
|
||||||
var isHearingImpaired: Boolean = false
|
var isHearingImpaired: Boolean = false,
|
||||||
|
var headers: Map<String, String> = emptyMap()
|
||||||
)
|
)
|
||||||
|
|
||||||
data class SubtitleSearch(
|
data class SubtitleSearch(
|
||||||
|
|
|
@ -12,6 +12,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
val aniListApi = AniListApi(0)
|
val aniListApi = AniListApi(0)
|
||||||
val openSubtitlesApi = OpenSubtitlesApi(0)
|
val openSubtitlesApi = OpenSubtitlesApi(0)
|
||||||
val indexSubtitlesApi = IndexSubtitleApi()
|
val indexSubtitlesApi = IndexSubtitleApi()
|
||||||
|
val addic7ed = Addic7ed()
|
||||||
val localListApi = LocalList()
|
val localListApi = LocalList()
|
||||||
|
|
||||||
// used to login via app intent
|
// used to login via app intent
|
||||||
|
@ -38,12 +39,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
|
||||||
val subtitleProviders
|
val subtitleProviders
|
||||||
get() = listOf(
|
get() = listOf(
|
||||||
openSubtitlesApi,
|
openSubtitlesApi,
|
||||||
// indexSubtitlesApi // they got anti scraping measures in place :(
|
indexSubtitlesApi, // they got anti scraping measures in place :(
|
||||||
|
addic7ed
|
||||||
)
|
)
|
||||||
|
|
||||||
const val appString = "cloudstreamapp"
|
const val appString = "cloudstreamapp"
|
||||||
const val appStringRepo = "cloudstreamrepo"
|
const val appStringRepo = "cloudstreamrepo"
|
||||||
|
|
||||||
|
// Instantly start the search given a query
|
||||||
|
const val appStringSearch = "cloudstreamsearch"
|
||||||
|
|
||||||
|
// Instantly resume watching a show
|
||||||
|
const val appStringResumeWatching = "cloudstreamcontinuewatching"
|
||||||
|
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
val unixTimeMs: Long
|
val unixTimeMs: Long
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders
|
package com.lagradost.cloudstream3.syncproviders
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
|
||||||
interface OAuth2API : AuthAPI {
|
interface OAuth2API : AuthAPI {
|
||||||
val key: String
|
val key: String
|
||||||
val redirectUrl: String
|
val redirectUrl: String
|
||||||
|
|
||||||
suspend fun handleRedirect(url: String) : Boolean
|
suspend fun handleRedirect(url: String) : Boolean
|
||||||
fun authenticate()
|
fun authenticate(activity: FragmentActivity?)
|
||||||
}
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import com.lagradost.cloudstream3.TvType
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
|
||||||
|
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||||
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
|
||||||
|
class Addic7ed : AbstractSubApi {
|
||||||
|
override val name = "Addic7ed"
|
||||||
|
override val idPrefix = "addic7ed"
|
||||||
|
override val requiresLogin = false
|
||||||
|
override val icon: Nothing? = null
|
||||||
|
override val createAccountUrl: Nothing? = null
|
||||||
|
|
||||||
|
override fun loginInfo(): Nothing? = null
|
||||||
|
|
||||||
|
override fun logOut() {}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val host = "https://www.addic7ed.com"
|
||||||
|
const val TAG = "ADDIC7ED"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixUrl(url: String): String {
|
||||||
|
return if (url.startsWith("/")) host + url
|
||||||
|
else if (!url.startsWith("http")) "$host/$url"
|
||||||
|
else url
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
|
||||||
|
val lang = query.lang
|
||||||
|
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
|
||||||
|
val queryText = query.query.trim()
|
||||||
|
val epNum = query.epNumber ?: 0
|
||||||
|
val seasonNum = query.seasonNumber ?: 0
|
||||||
|
val yearNum = query.year ?: 0
|
||||||
|
|
||||||
|
fun cleanResources(
|
||||||
|
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
|
||||||
|
name: String,
|
||||||
|
link: String,
|
||||||
|
headers: Map<String, String>,
|
||||||
|
isHearingImpaired: Boolean
|
||||||
|
) {
|
||||||
|
results.add(
|
||||||
|
AbstractSubtitleEntities.SubtitleEntity(
|
||||||
|
idPrefix = idPrefix,
|
||||||
|
name = name,
|
||||||
|
lang = queryLang.toString(),
|
||||||
|
data = link,
|
||||||
|
source = this.name,
|
||||||
|
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
|
||||||
|
epNumber = epNum,
|
||||||
|
seasonNumber = seasonNum,
|
||||||
|
year = yearNum,
|
||||||
|
headers = headers,
|
||||||
|
isHearingImpaired = isHearingImpaired
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val title = queryText.substringBefore("(").trim()
|
||||||
|
val url = "$host/search.php?search=${title}&Submit=Search"
|
||||||
|
val hostDocument = app.get(url).document
|
||||||
|
var searchResult = ""
|
||||||
|
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
|
||||||
|
else if (!hostDocument.select("table.tabel")
|
||||||
|
.isNullOrEmpty()
|
||||||
|
) searchResult = hostDocument.select("a:contains($title)").attr("href").toString()
|
||||||
|
else {
|
||||||
|
val show =
|
||||||
|
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
|
||||||
|
?.substringBefore(",")
|
||||||
|
val doc = app.get(
|
||||||
|
"$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
|
||||||
|
referer = "$host/"
|
||||||
|
).document
|
||||||
|
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
|
||||||
|
if (node.selectFirst("td")?.text()
|
||||||
|
?.toIntOrNull() == seasonNum && node.select("td:eq(1)")
|
||||||
|
.text()
|
||||||
|
.toIntOrNull() == epNum
|
||||||
|
) searchResult = fixUrl(node.select("a").attr("href"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
|
||||||
|
val document = app.get(
|
||||||
|
url = fixUrl(searchResult),
|
||||||
|
).document
|
||||||
|
|
||||||
|
document.select(".tabel95 .tabel95 tr:contains($queryLang)").mapNotNull { node ->
|
||||||
|
val name = if (seasonNum > 0) "${document.select(".titulo").text().replace("Subtitle","").trim()}${
|
||||||
|
node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")
|
||||||
|
}" else "${document.select(".titulo").text().replace("Subtitle","").trim()}${node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")}"
|
||||||
|
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
|
||||||
|
val isHearingImpaired =
|
||||||
|
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
|
||||||
|
cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String {
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.syncproviders.providers
|
package com.lagradost.cloudstream3.syncproviders.providers
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.capitalize
|
import com.lagradost.cloudstream3.APIHolder.capitalize
|
||||||
|
@ -7,6 +8,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||||
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
import com.lagradost.cloudstream3.syncproviders.AuthAPI
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||||
|
@ -18,6 +20,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
|
@ -47,9 +50,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
removeAccountKeys()
|
removeAccountKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun authenticate() {
|
override fun authenticate(activity: FragmentActivity?) {
|
||||||
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
|
val request = "https://anilist.co/api/v2/oauth/authorize?client_id=$key&response_type=token"
|
||||||
openBrowser(request)
|
openBrowser(request, activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun handleRedirect(url: String): Boolean {
|
override suspend fun handleRedirect(url: String): Boolean {
|
||||||
|
@ -521,19 +524,26 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun postApi(q: String, cache: Boolean = false): String? {
|
private suspend fun postApi(q: String, cache: Boolean = false): String? {
|
||||||
return if (!checkToken()) {
|
return suspendSafeApiCall {
|
||||||
app.post(
|
if (!checkToken()) {
|
||||||
"https://graphql.anilist.co/",
|
app.post(
|
||||||
headers = mapOf(
|
"https://graphql.anilist.co/",
|
||||||
"Authorization" to "Bearer " + (getAuth() ?: return null),
|
headers = mapOf(
|
||||||
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
"Authorization" to "Bearer " + (getAuth() ?: return@suspendSafeApiCall null),
|
||||||
),
|
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
|
||||||
cacheTime = 0,
|
),
|
||||||
data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
|
cacheTime = 0,
|
||||||
timeout = 5 // REASONABLE TIMEOUT
|
data = mapOf(
|
||||||
).text.replace("\\/", "/")
|
"query" to URLEncoder.encode(
|
||||||
} else {
|
q,
|
||||||
null
|
"UTF-8"
|
||||||
|
)
|
||||||
|
), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
|
||||||
|
timeout = 5 // REASONABLE TIMEOUT
|
||||||
|
).text.replace("\\/", "/")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|