Compare commits

..

2 commits

Author SHA1 Message Date
ArjixWasTaken
9f09132a7c
Fix oversight when cookies are already stored in cloudflare (#49) 2022-08-21 12:14:45 -07:00
Arjix
921a1dab37 Feat: AdvancedWebView 2022-08-21 22:09:56 +03:00
406 changed files with 11952 additions and 30434 deletions

View file

@ -1,8 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Request a new provider or report bug with an existing provider - name: Report provider bug
url: https://github.com/recloudstream url: https://github.com/recloudstream
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. about: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
- name: Discord - 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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
.github/home.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

63
.github/locales.py vendored
View file

@ -1,63 +0,0 @@
import re
import glob
import requests
import lxml.etree as ET # builtin library doesn't preserve comments
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */"
XML_NAME = "app/src/main/res/values-"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
INDENT = " "*4
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
# Load settings file
src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
before_src, rest = src.split(START_MARKER)
rest, after_src = rest.split(END_MARKER)
# Load already added langs
languages = {}
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
flag, name, iso = lang.groups()
languages[iso] = (flag, name)
# Add not yet added langs
for folder in glob.glob(f"{XML_NAME}*"):
iso = folder[len(XML_NAME):]
if iso not in languages.keys():
entry = iso_map.get(iso.lower(),{'nativeName':iso})
languages[iso] = ("", entry['nativeName'].split(',')[0])
# Create triples
triples = []
for iso in sorted(languages.keys()):
flag, name = languages[iso]
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
# Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
before_src +
START_MARKER +
"\n" +
"\n".join(triples) +
"\n" +
END_MARKER +
after_src
)
# Go through each values.xml file and fix escaped \@string
for file in glob.glob(f"{XML_NAME}*/strings.xml"):
try:
tree = ET.parse(file)
for child in tree.getroot():
if child.text.startswith("\\@string/"):
print(f"[{file}] fixing {child.attrib['name']}")
child.text = child.text.replace("\\@string/", "@string/")
with open(file, 'wb') as fp:
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
except ET.ParseError as ex:
print(f"[{file}] {ex}")

BIN
.github/player.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

BIN
.github/results.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

BIN
.github/search.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View file

@ -1,76 +0,0 @@
name: Archive build
on:
push:
branches: [ master ]
paths-ignore:
- '*.md'
- '*.json'
- '**/wcokey.txt'
workflow_dispatch:
concurrency:
group: "Archive-build"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@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

View file

@ -39,8 +39,9 @@ jobs:
- name: Clean old builds - name: Clean old builds
run: | run: |
shopt -s extglob
cd $GITHUB_WORKSPACE/dokka/ cd $GITHUB_WORKSPACE/dokka/
rm -rf "./-cloudstream" rm -rf !(.git)
- name: Setup JDK 11 - name: Setup JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1

View file

@ -2,7 +2,7 @@ name: Issue automatic actions
on: on:
issues: issues:
types: [opened] types: [opened, edited]
jobs: jobs:
issue-moderator: issue-moderator:
@ -15,28 +15,15 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
- name: Similarity analysis - name: Similarity analysis
id: similarity
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.60 filter-threshold: 0.5
title-excludes: '' title-excludes: ''
comment-title: | comment-title: |
### Your issue looks similar to these issues: ### Your issue looks similar to these issues:
Please close if duplicate. Please close if duplicate.
comment-body: '${index}. ${similarity} #${number}' comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true'
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible duplicate"]
})
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Automatically close issues that dont follow the issue template - name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2 uses: lucasbento/auto-close-issues@v1.0.2
@ -54,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 "name=${RES}" >> $GITHUB_OUTPUT echo "::set-output name=name::${RES}"
- 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
@ -66,18 +53,6 @@ jobs:
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM). Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
Found provider name: `${{ steps.provider_check.outputs.name }}` Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible provider issue"]
})
- name: Add eyes reaction to all issues - name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0 uses: actions-cool/emoji-helper@v1.0.0
with: with:

View file

@ -40,10 +40,12 @@ 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 "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT echo "::set-output name=key_pwd::$KEY_PWD"
- name: Run Gradle - name: Run Gradle
run: | run: |
./gradlew assemblePrerelease makeJar androidSourcesJar ./gradlew assemblePrerelease
./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 }}
@ -53,9 +55,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: true prerelease: false
title: "Pre-release Build" title: "Pre-release Build"
files: | files: |
app/build/outputs/apk/prerelease/release/*.apk app/build/outputs/apk/prerelease/*.apk
app/build/libs/app-sources.jar app/build/libs/app-sources.jar
app/build/classes.jar app/build/classes.jar

View file

@ -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 assemblePrereleaseDebug run: ./gradlew assembleDebug
- 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/prerelease/debug/*.apk" path: "app/build/outputs/apk/debug/*.apk"

View file

@ -1,42 +0,0 @@
name: Fix locale issues
on:
workflow_dispatch:
push:
paths:
- '**.xml'
branches:
- master
concurrency:
group: "locale"
cancel-in-progress: true
jobs:
create:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@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: Install dependencies
run: |
pip3 install lxml
- name: Edit files
run: |
python3 .github/locales.py
- name: Commit to the repo
run: |
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
git config --local user.name "recloudstream[bot]"
git add .
# "echo" returns true so the build succeeds, even if no changed files
git commit -m 'chore(locales): fix locale issues' || echo
git push

View file

@ -31,10 +31,5 @@
<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>

View file

@ -1,18 +1,44 @@
# 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://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM) [![Discord](https://img.shields.io/discord/737724143126052974?style=for-the-badge)](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
### Supported languages: ***Screenshots:***
<a href="https://hosted.weblate.org/engage/cloudstream/">
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" /> <img src="./.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"/>
</a> <img src="./.github/player.jpg" height="200"/>
***The list of supported languages:***
* 🇱🇧 Arabic
* 🇨🇿 Czech
* 🇳🇱 Dutch
* 🇬🇧 English
* 🇫🇷 French
* 🇩🇪 German
* 🇬🇷 Greek
* 🇮🇳 Hindi
* 🇮🇩 Indonesian
* 🇮🇹 Italian
* 🇲🇰 Macedonian
* 🇮🇳 Malayalam
* 🇳🇴 Norsk
* 🇵🇱 Polish
* 🇧🇷 Portuguese (Brazil)
* 🇷🇴 Romanian
* 🇪🇸 Spanish
* 🇸🇪 Swedish
* 🇵🇭 Tagalog
* 🇹🇷 Turkish
* 🇻🇳 Vietnamese

215
app/build.gradle Normal file
View file

@ -0,0 +1,215 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-android-extensions'
id 'org.jetbrains.dokka'
}
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
def allFilesFromDir = new File(tmpFilePath).listFiles()
def prereleaseStoreFile = null
if (allFilesFromDir != null) {
prereleaseStoreFile = allFilesFromDir.first()
}
android {
testOptions {
unitTests.returnDefaultValues = true
}
signingConfigs {
prerelease {
if (prereleaseStoreFile != null) {
storeFile = file(prereleaseStoreFile)
storePassword System.getenv("SIGNING_STORE_PASSWORD")
keyAlias System.getenv("SIGNING_KEY_ALIAS")
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
}
}
}
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.lagradost.cloudstream3"
minSdkVersion 21
targetSdkVersion 30
versionCode 50
versionName "3.1.3"
resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
resValue "string", "commit_hash",
("git rev-parse --short HEAD".execute().text.trim() ?: "")
resValue "bool", "is_prerelease", "false"
buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
includeCompileClasspath = true
}
}
buildTypes {
// release {
// debuggable false
// minifyEnabled false
// shrinkResources false
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// resValue "bool", "is_prerelease", "false"
// }
prerelease {
applicationIdSuffix ".prerelease"
buildConfigField("boolean", "BETA", "true")
signingConfig signingConfigs.prerelease
versionNameSuffix '-PRE'
debuggable false
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "bool", "is_prerelease", "true"
}
debug {
debuggable true
applicationIdSuffix ".debug"
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "bool", "is_prerelease", "true"
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ['-Xjvm-default=compatibility']
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.google.android.mediahome:video:1.0.0'
implementation 'androidx.test.ext:junit-ktx:1.1.3'
testImplementation 'org.json:json:20180813'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
// dont change this to 1.6.0 it looks ugly af
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//implementation "io.karn:khttp-android:0.1.2" //okhttp instead
// implementation 'org.jsoup:jsoup:1.13.1'
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation 'com.github.bumptech.glide:glide:4.13.1'
kapt 'com.github.bumptech.glide:compiler:4.13.1'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
// Exoplayer
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
// Bug reports
implementation "ch.acra:acra-core:5.8.4"
implementation "ch.acra:acra-toast:5.8.4"
compileOnly "com.google.auto.service:auto-service-annotations:1.0"
//either for java sources:
annotationProcessor "com.google.auto.service:auto-service:1.0"
//or for kotlin sources (requires kapt gradle plugin):
kapt "com.google.auto.service:auto-service:1.0"
// subtitle color picker
implementation 'com.jaredrummler:colorpicker:1.1.0'
//run JS
implementation 'org.mozilla:rhino:1.7.14'
// TorrentStream
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
// Downloading
implementation "androidx.work:work-runtime:2.7.1"
implementation "androidx.work:work-runtime-ktx:2.7.1"
// Networking
// implementation "com.squareup.okhttp3:okhttp:4.9.2"
// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
implementation 'com.github.Blatzar:NiceHttp:0.3.2'
// Util to skip the URI file fuckery 🙏
implementation "com.github.tachiyomiorg:unifile:17bec43"
// API because cba maintaining it myself
implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
implementation 'com.github.discord:OverlappingPanels:0.1.3'
// debugImplementation because LeakCanary should only run in debug builds.
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
// for shimmer when loading
implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.tvprovider:tvprovider:1.0.0"
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
// slow af yt
//implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
// newpipe yt
implementation 'com.github.recloudstream:NewPipeExtractor:master-SNAPSHOT'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Library/extensions searching with Levenshtein distance
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
}
task androidSourcesJar(type: Jar) {
getArchiveClassifier().set('sources')
from android.sourceSets.main.java.srcDirs//full sources
}
task makeJar(type: Copy) {
// after modifying here, you can export. Jar
from('build/intermediates/compile_app_classes_jar/debug')
into('build') // output location
include('classes.jar') // the classes file of the imported rack package
dependsOn build
}

View file

@ -1,258 +0,0 @@
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 = 57
versionName = "4.0.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")
// Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3
// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+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
// 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.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.0")
// Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.4.2")
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏
implementation("com.github.tachiyomiorg:unifile:17bec43")
// API because cba maintaining it myself
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
implementation("com.github.discord:OverlappingPanels:0.1.3")
// debugImplementation because LeakCanary should only run in debug builds.
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
// for shimmer when loading
implementation("com.facebook.shimmer:shimmer:0.5.0")
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")
// color pallette for images -> colors
implementation("androidx.palette:palette-ktx:1.0.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")
}
}
}
}

View file

@ -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.kts. # proguardFiles setting in build.gradle.
# #
# 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

View file

@ -1,8 +1,9 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
@ -15,11 +16,142 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
//@Test
//fun useAppContext() {
// // Context of the app under test.
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
//}
private fun getAllProviders(): List<MainAPI> { private fun getAllProviders(): List<MainAPI> {
println("Providers: ${APIHolder.allProviders.size}")
return APIHolder.allProviders //.filter { !it.usesWebView } return APIHolder.allProviders //.filter { !it.usesWebView }
} }
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
if (url == null) return true
var linksLoaded = 0
try {
val success = api.loadLinks(url, false, {}) { link ->
Assert.assertTrue(
"Api ${api.name} returns link with invalid Quality",
Qualities.values().map { it.value }.contains(link.quality)
)
Assert.assertTrue(
"Api ${api.name} returns link with invalid url ${link.url}",
link.url.length > 4
)
linksLoaded++
}
if (success) {
return linksLoaded > 0
}
Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .loadLinks")
}
logError(e)
}
return true
}
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
val searchQueries = listOf("over", "iron", "guy")
var correctResponses = 0
var searchResult: List<SearchResponse>? = null
for (query in searchQueries) {
val response = try {
api.search(query)
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .search")
}
logError(e)
null
}
if (!response.isNullOrEmpty()) {
correctResponses++
if (searchResult == null) {
searchResult = response
}
}
}
if (correctResponses == 0 || searchResult == null) {
System.err.println("Api ${api.name} did not return any valid search responses")
return false
}
try {
var validResults = false
for (result in searchResult) {
Assert.assertEquals(
"Invalid apiName on response on ${api.name}",
result.apiName,
api.name
)
val load = api.load(result.url) ?: continue
Assert.assertEquals(
"Invalid apiName on load on ${api.name}",
load.apiName,
result.apiName
)
Assert.assertTrue(
"Api ${api.name} on load does not contain any of the supportedTypes",
api.supportedTypes.contains(load.type)
)
when (load) {
is AnimeLoadResponse -> {
val gotNoEpisodes =
load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
if (gotNoEpisodes) {
println("Api ${api.name} got no episodes on ${load.url}")
continue
}
val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
validResults = loadLinks(api, url)
if (!validResults) continue
}
is MovieLoadResponse -> {
val gotNoEpisodes = load.dataUrl.isBlank()
if (gotNoEpisodes) {
println("Api ${api.name} got no movie on ${load.url}")
continue
}
validResults = loadLinks(api, load.dataUrl)
if (!validResults) continue
}
is TvSeriesLoadResponse -> {
val gotNoEpisodes = load.episodes.isEmpty()
if (gotNoEpisodes) {
println("Api ${api.name} got no episodes on ${load.url}")
continue
}
validResults = loadLinks(api, load.episodes.first().data)
if (!validResults) continue
}
}
break
}
if(!validResults) {
System.err.println("Api ${api.name} did not load on any")
}
return validResults
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .load")
}
logError(e)
return false
}
}
@Test @Test
fun providersExist() { fun providersExist() {
Assert.assertTrue(getAllProviders().isNotEmpty()) Assert.assertTrue(getAllProviders().isNotEmpty())
@ -27,7 +159,6 @@ class ExampleInstrumentedTest {
} }
@Test @Test
@Throws(AssertionError::class)
fun providerCorrectData() { fun providerCorrectData() {
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 } val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty()) Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
@ -49,21 +180,66 @@ class ExampleInstrumentedTest {
@Test @Test
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().amap { api -> getAllProviders().apmap { api ->
TestingUtils.testHomepage(api, ::println) if (api.hasMainPage) {
try {
val homepage = api.getMainPage()
when {
homepage == null -> {
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
}
homepage.items.isEmpty() -> {
System.err.println("Homepage provider ${api.name} does not contain any items!")
}
homepage.items.any { it.list.isEmpty() } -> {
System.err.println ("Homepage provider ${api.name} does not have any items on result!")
}
}
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
}
logError(e)
}
}
} }
} }
println("Done providerCorrectHomepage") println("Done providerCorrectHomepage")
} }
// @Test
// fun testSingleProvider() {
// testSingleProviderApi(ThenosProvider())
// }
@Test @Test
fun testAllProvidersCorrect() { fun providerCorrect() {
runBlocking { runBlocking {
TestingUtils.getDeferredProviderTests( val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
this, val providers = getAllProviders()
getAllProviders(), providers.apmap { api ->
::println try {
) { _, _ -> } println("Trying $api")
if (testSingleProviderApi(api)) {
println("Success $api")
} else {
System.err.println("Error $api")
invalidProvider.add(Pair(api, null))
}
} catch (e: Exception) {
logError(e)
invalidProvider.add(Pair(api, e))
}
}
if(invalidProvider.isEmpty()) {
println("No Invalid providers! :D")
} else {
println("Invalid providers are: ")
for (provider in invalidProvider) {
println("${provider.first}")
}
}
} }
println("Done providerCorrect")
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 9 KiB

View file

@ -1,6 +1,7 @@
<?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 -->
@ -10,11 +11,7 @@
<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="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ --> <!-- <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="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" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; --> <!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; -->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
@ -24,13 +21,6 @@
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"
@ -40,7 +30,6 @@
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"
@ -98,16 +87,6 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
<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="cloudstreamplayer" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -124,30 +103,6 @@
<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" />
@ -183,10 +138,6 @@
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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

@ -4,15 +4,11 @@ import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
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.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
@ -21,7 +17,6 @@ import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.acra.ACRA
import org.acra.ReportField import org.acra.ReportField
import org.acra.config.CoreConfiguration import org.acra.config.CoreConfiguration
import org.acra.data.CrashReportData import org.acra.data.CrashReportData
@ -29,23 +24,17 @@ import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.acra.sender.ReportSender import org.acra.sender.ReportSender
import org.acra.sender.ReportSenderFactory import org.acra.sender.ReportSenderFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.PrintStream
import java.lang.Exception
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess
class CustomReportSender : ReportSender { class CustomReportSender : ReportSender {
// Sends all your crashes to google forms // Sends all your crashes to google forms
override fun send(context: Context, errorContent: CrashReportData) { override fun send(context: Context, errorContent: CrashReportData) {
println("Sending report") println("Sending report")
val url = val url =
"https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse" "https://docs.google.com/forms/u/0/d/e/1FAIpQLSeFmyBChi6HF3IkhTVWPiDXJtxt8W0Hf4Agljm_0-0_QuEYFg/formResponse"
val data = mapOf( val data = mapOf(
"entry.753293084" to errorContent.toJSON() "entry.134906550" to errorContent.toJSON()
) )
thread { // to not run it on main thread thread { // to not run it on main thread
@ -76,42 +65,7 @@ class CustomSenderFactory : ReportSenderFactory {
} }
} }
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, error: Throwable) {
ACRA.errorReporter.handleException(error)
try {
PrintStream(errorFile).use { ps ->
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
ps.println(
String.format(
"Fatal exception on thread %s (%d)",
thread.name,
thread.id
)
)
error.printStackTrace(ps)
}
} catch (ignored: FileNotFoundException) {
}
try {
onError.invoke()
} catch (ignored: Exception) {
}
exitProcess(1)
}
}
class AcraApplication : Application() { class AcraApplication : Application() {
override fun onCreate() {
super.onCreate()
Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
})
}
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
context = base context = base
@ -194,15 +148,5 @@ 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()
)
}
} }
} }

View file

@ -1,38 +1,31 @@
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
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.os.Looper
import android.util.Log 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.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
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.coroutines.currentCoroutineContext
import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.NewPipe
import java.util.* import java.util.*
@ -42,7 +35,6 @@ 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
@ -63,9 +55,7 @@ object CommonActivity {
} }
} }
/** duration is Toast.LENGTH_SHORT if null*/ fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
@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)
} }
@ -73,7 +63,6 @@ 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")
@ -110,18 +99,9 @@ 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 = appLanguageExceptions[languageCode] ?: Locale(languageCode) val locale = 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)
@ -138,7 +118,7 @@ object CommonActivity {
setLocale(this, localeCode) setLocale(this, localeCode)
} }
fun init(act: ComponentActivity?) { fun init(act: Activity?) {
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
@ -148,41 +128,8 @@ object CommonActivity {
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
act.updateLocale() act.updateLocale()
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() {
@ -220,8 +167,6 @@ 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
} }
@ -242,10 +187,6 @@ 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)
@ -343,7 +284,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_7 -> { KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7 -> {
PlayerEventType.Lock PlayerEventType.Lock
} }
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> { KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
@ -352,25 +293,22 @@ 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_9 -> { KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
PlayerEventType.ShowMirrors PlayerEventType.ShowMirrors
} }
// OpenSubtitles shortcut // OpenSubtitles shortcut
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> { KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
PlayerEventType.SearchSubtitlesOnline PlayerEventType.SearchSubtitlesOnline
} }
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> { KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
PlayerEventType.ShowSpeed PlayerEventType.ShowSpeed
} }
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> { KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
PlayerEventType.Resize PlayerEventType.Resize
} }
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> { KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_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
} }

View file

@ -1,11 +0,0 @@
package com.lagradost.cloudstream3
import android.view.LayoutInflater
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.ui.HeaderViewDecoration
fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
view.addItemDecoration(HeaderViewDecoration(headerView))
}

View file

@ -13,17 +13,17 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor import okhttp3.Interceptor
import org.mozilla.javascript.Scriptable
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"
@ -32,12 +32,6 @@ 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
@ -46,8 +40,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
// ConcurrentModificationException is possible!!! val allProviders: MutableList<MainAPI> = arrayListOf()
val allProviders = threadSafeListOf<MainAPI>()
fun initAll() { fun initAll() {
for (api in allProviders) { for (api in allProviders) {
@ -60,7 +53,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> = threadSafeListOf() var apis: List<MainAPI> = arrayListOf()
var apiMap: Map<String, Int>? = null var apiMap: Map<String, Int>? = null
fun addPluginMapping(plugin: MainAPI) { fun addPluginMapping(plugin: MainAPI) {
@ -80,20 +73,16 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? { fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null if (apiName == null) return null
synchronized(allProviders) { initMap()
initMap() return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
return apiMap?.get(apiName)?.let { apis.getOrNull(it) } ?: allProviders.firstOrNull { it.name == apiName }
// 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
synchronized(allProviders) { for (api in allProviders) {
allProviders.forEach { api -> if (url.startsWith(api.mainUrl))
if (url.startsWith(api.mainUrl)) return api return api
}
} }
return null return null
} }
@ -162,61 +151,12 @@ object APIHolder {
return null return null
} }
private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
/**
* Get anime tracker information based on title, year and type.
* Both titles are attempted to be matched with both Romaji and English title.
* Uses the consumet api.
*
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
* @param year Optional parameter to only get anime with a specific year
**/
suspend fun getTracker(
titles: List<String>,
types: Set<TrackerType>?,
year: Int?
): Tracker? {
return try {
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
val mainTitle = titles[0]
val search =
trackerCache[mainTitle]
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
.parsedSafe<AniSearch>()?.also {
trackerCache[mainTitle] = it
} ?: return null
val res = search.results?.find { media ->
val matchingYears = year == null || media.releaseDate == year
val matchingTitles = media.title?.let { title ->
titles.any { userTitle ->
title.isMatchingTitles(userTitle)
}
} ?: false
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
matchingTitles && matchingTypes && matchingYears
} ?: return null
Tracker(res.malId, res.aniId, res.image, res.cover)
} catch (t: Throwable) {
logError(t)
null
}
}
fun Context.getApiSettings(): HashSet<String> { fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>() val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings() val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName) hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name })
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),
@ -252,11 +192,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 = hashSetOf(AllLanguagesName) // def is all languages val hashSet = HashSet<String>()
// 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 hashSet.toMutableSet()
) )
if (list.isNullOrEmpty()) return hashSet if (list.isNullOrEmpty()) return hashSet
@ -286,35 +226,23 @@ object APIHolder {
} }
private fun Context.getHasTrailers(): Boolean { private fun Context.getHasTrailers(): Boolean {
if (this.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> {
// We are getting the weirdest crash ever done: val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
// 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)
.getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet) .getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet)
?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null } ?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null }
} catch (e: Throwable) { } catch (e: Throwable) {
null null
} ?: default } ?: default
val langs = this.getApiProviderLangSettings() val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName) val allApis = apis.filter { langs.contains(it.lang) }
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
@ -367,57 +295,6 @@ object APIHolder {
} }
} }
/*
// THIS IS WORK IN PROGRESS API
interface ITag {
val name: UiText
}
data class SimpleTag(override val name: UiText, val data: String) : ITag
enum class SelectType {
SingleSelect,
MultiSelect,
MultiSelectAndExclude,
}
enum class SelectValue {
Selected,
Excluded,
}
interface GenreSelector {
val title: UiText
val id : Int
}
data class TagSelector(
override val title: UiText,
override val id : Int,
val tags: Set<ITag>,
val defaultTags : Set<ITag> = setOf(),
val selectType: SelectType = SelectType.SingleSelect,
) : GenreSelector
data class BoolSelector(
override val title: UiText,
override val id : Int,
val defaultValue : Boolean = false,
) : GenreSelector
data class InputField(
override val title: UiText,
override val id : Int,
val hint : UiText? = null,
) : GenreSelector
// This response describes how a user might filter the homepage or search results
data class GenreResponse(
val searchSelectors : List<GenreSelector>,
val filterSelectors: List<GenreSelector> = searchSelectors
) */
/* /*
0 = Site not good 0 = Site not good
@ -438,32 +315,17 @@ data class ProvidersInfoJson(
@JsonProperty("status") var status: Int, @JsonProperty("status") var status: Int,
) )
data class SettingsJson(
@JsonProperty("enableAdult") var enableAdult: Boolean = false,
)
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) }
@ -472,7 +334,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)),
@ -480,17 +342,6 @@ 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())
} }
@ -503,7 +354,6 @@ fun newHomePageResponse(list: List<HomePageList>, hasNext: Boolean? = null): Hom
abstract class MainAPI { abstract class MainAPI {
companion object { companion object {
var overrideData: HashMap<String, ProvidersInfoJson>? = null var overrideData: HashMap<String, ProvidersInfoJson>? = null
var settingsForProvider: SettingsJson = SettingsJson()
} }
fun init() { fun init() {
@ -525,19 +375,7 @@ abstract class MainAPI {
open var storedCredentials: String? = null open var storedCredentials: String? = null
open var canBeOverridden: Boolean = true open var canBeOverridden: Boolean = true
/** if this is turned on then it will request the homepage one after the other, //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id
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
@ -559,20 +397,6 @@ abstract class MainAPI {
open val hasMainPage = false open val hasMainPage = false
open val hasQuickSearch = false open val hasQuickSearch = false
/**
* A set of which ids the provider can open with getLoadUrl()
* If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
* an Imdb class which inherits from SyncId.
*
* getLoadUrl() is then used to get page url based on that ID.
*
* Example:
* "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
*
* This is used to launch pages from personal lists or recommendations using IDs.
**/
open val supportedSyncNames = setOf<SyncIdName>()
open val supportedTypes = setOf( open val supportedTypes = setOf(
TvType.Movie, TvType.Movie,
TvType.TvSeries, TvType.TvSeries,
@ -584,8 +408,7 @@ abstract class MainAPI {
open val vpnStatus = VPNStatus.None open val vpnStatus = VPNStatus.None
open val providerType = ProviderType.DirectProvider open val providerType = ProviderType.DirectProvider
//emptyList<MainPageData>() // open val mainPage = listOf(MainPageData("", ""))
open val mainPage = listOf(MainPageData("", "", false))
@WorkerThread @WorkerThread
open suspend fun getMainPage( open suspend fun getMainPage(
@ -643,14 +466,6 @@ abstract class MainAPI {
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? { open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return null return null
} }
/**
* Get the load() url based on a sync ID like IMDb or MAL.
* Only contains SyncIds based on supportedSyncUrls.
**/
open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
return null
}
} }
/** Might need a different implementation for desktop*/ /** Might need a different implementation for desktop*/
@ -736,19 +551,6 @@ fun fixTitle(str: String): String {
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } .replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
} }
} }
/**
* Get rhino context in a safe way as it needs to be initialized on the main thread.
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
* Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
**/
suspend fun getRhinoContext(): org.mozilla.javascript.Context {
return Coroutines.mainWork {
val rhino = org.mozilla.javascript.Context.enter()
rhino.initSafeStandardObjects()
rhino.optimizationLevel = -1
rhino
}
}
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */ /** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
fun imdbUrlToId(url: String): String? { fun imdbUrlToId(url: String): String? {
@ -1089,11 +891,8 @@ data class TvSeriesSearchResponse(
) : SearchResponse ) : SearchResponse
data class TrailerData( data class TrailerData(
val extractorUrl: String, var mirros: List<ExtractorLink>,
val referer: String?, var subtitles: List<SubtitleFile> = emptyList(),
val raw: Boolean,
//var mirros: List<ExtractorLink>,
//var subtitles: List<SubtitleFile> = emptyList(),
) )
interface LoadResponse { interface LoadResponse {
@ -1172,8 +971,7 @@ interface LoadResponse {
addRaw: Boolean = false addRaw: Boolean = false
) { ) {
if (!isTrailersEnabled || trailerUrl.isNullOrBlank()) return if (!isTrailersEnabled || trailerUrl.isNullOrBlank()) return
this.trailers.add(TrailerData(trailerUrl, referer, addRaw)) val links = arrayListOf<ExtractorLink>()
/*val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>() val subs = arrayListOf<SubtitleFile>()
if (!loadExtractor( if (!loadExtractor(
trailerUrl, trailerUrl,
@ -1197,13 +995,12 @@ interface LoadResponse {
) )
} else { } else {
this.trailers.add(TrailerData(links, subs)) this.trailers.add(TrailerData(links, subs))
}*/ }
} }
/*
fun LoadResponse.addTrailer(newTrailers: List<ExtractorLink>) { fun LoadResponse.addTrailer(newTrailers: List<ExtractorLink>) {
trailers.addAll(newTrailers.map { TrailerData(listOf(it)) }) trailers.addAll(newTrailers.map { TrailerData(listOf(it)) })
}*/ }
suspend fun LoadResponse.addTrailer( suspend fun LoadResponse.addTrailer(
trailerUrls: List<String>?, trailerUrls: List<String>?,
@ -1211,8 +1008,7 @@ interface LoadResponse {
addRaw: Boolean = false addRaw: Boolean = false
) { ) {
if (!isTrailersEnabled || trailerUrls == null) return if (!isTrailersEnabled || trailerUrls == null) return
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(
@ -1235,7 +1031,7 @@ interface LoadResponse {
links to subs links to subs
} }
}.map { (links, subs) -> TrailerData(links, subs) } }.map { (links, subs) -> TrailerData(links, subs) }
this.trailers.addAll(trailers)*/ this.trailers.addAll(trailers)
} }
fun LoadResponse.addImdbId(id: String?) { fun LoadResponse.addImdbId(id: String?) {
@ -1273,43 +1069,18 @@ 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()
if (minutes != null && hours != null) { return if (minutes != null && hours != null) {
return hours * 60 + minutes 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) {
val return_value = values[1].toIntOrNull() return values[1].toIntOrNull()
if (return_value != null) {
return return_value
}
} }
} }
return null return null
@ -1327,7 +1098,7 @@ fun LoadResponse?.isAnimeBased(): Boolean {
fun TvType?.isEpisodeBased(): Boolean { fun TvType?.isEpisodeBased(): Boolean {
if (this == null) return false if (this == null) return false
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) return (this == TvType.TvSeries || this == TvType.Anime)
} }
@ -1336,11 +1107,6 @@ 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,
@ -1351,7 +1117,6 @@ interface EpisodeResponse {
var showStatus: ShowStatus? var showStatus: ShowStatus?
var nextAiring: NextAiring? var nextAiring: NextAiring?
var seasonNames: List<SeasonData>? var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?>
} }
@JvmName("addSeasonNamesString") @JvmName("addSeasonNamesString")
@ -1420,25 +1185,11 @@ data class AnimeLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse { ) : LoadResponse, EpisodeResponse
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
return episodes.map { (status, episodes) ->
val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
status to episodes
.filter { it.season == maxSeason }
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
}.toMap()
}
}
/**
* 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] = (this.episodes[status] ?: emptyList()) + episodes this.episodes[status] = episodes
} }
suspend fun MainAPI.newAnimeLoadResponse( suspend fun MainAPI.newAnimeLoadResponse(
@ -1629,17 +1380,7 @@ data class TvSeriesLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse { ) : LoadResponse, EpisodeResponse
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
val maxSeason =
episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
val max = episodes
.filter { it.season == maxSeason }
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max)
}
}
suspend fun MainAPI.newTvSeriesLoadResponse( suspend fun MainAPI.newTvSeriesLoadResponse(
name: String, name: String,
@ -1671,61 +1412,3 @@ fun fetchUrls(text: String?): List<String> {
fun String?.toRatingInt(): Int? = fun String?.toRatingInt(): Int? =
this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt() this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt()
data class Tracker(
val malId: Int? = null,
val aniId: String? = null,
val image: String? = null,
val cover: String? = null,
)
data class Title(
@JsonProperty("romaji") val romaji: String? = null,
@JsonProperty("english") val english: String? = null,
) {
fun isMatchingTitles(title: String?): Boolean {
if (title == null) return false
return english.equals(title, true) || romaji.equals(title, true)
}
}
data class Results(
@JsonProperty("id") val aniId: String? = null,
@JsonProperty("malId") val malId: Int? = null,
@JsonProperty("title") val title: Title? = null,
@JsonProperty("releaseDate") val releaseDate: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("image") val image: String? = null,
@JsonProperty("cover") val cover: String? = null,
)
data class AniSearch(
@JsonProperty("results") val results: ArrayList<Results>? = arrayListOf()
)
/**
* used for the getTracker() method
**/
enum class TrackerType {
MOVIE,
TV,
TV_SHORT,
ONA,
OVA,
SPECIAL,
MUSIC;
companion object {
fun getTypes(type: TvType): Set<TrackerType> {
return when (type) {
TvType.Movie -> setOf(MOVIE)
TvType.AnimeMovie -> setOf(MOVIE)
TvType.TvSeries -> setOf(TV, TV_SHORT)
TvType.Anime -> setOf(TV, TV_SHORT, ONA, OVA)
TvType.OVA -> setOf(OVA, SPECIAL, ONA)
TvType.Others -> setOf(MUSIC)
else -> emptySet()
}
}
}
}

View file

@ -1,25 +1,18 @@
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.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.* import android.view.KeyEvent
import android.widget.Toast import android.view.Menu
import androidx.activity.result.ActivityResultLauncher import android.view.MenuItem
import android.view.WindowManager
import androidx.annotation.IdRes import androidx.annotation.IdRes
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
@ -32,187 +25,81 @@ 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.google.android.material.snackbar.Snackbar
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis 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.* import com.lagradost.cloudstream3.mvvm.logError
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.appStringPlayer
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.home.HomeViewModel import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator
import com.lagradost.cloudstream3.ui.result.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.isNetworkAvailable
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.Coroutines.main
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.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 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 MPV_PACKAGE = "is.xyz.mpv" const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" val VLC_COMPONENT: ComponentName =
ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
const val VLC_REQUEST_CODE = 42
val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") const val VLC_FROM_START = -1
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") const val VLC_FROM_PROGRESS = -2
const val VLC_EXTRA_POSITION_OUT = "extra_position"
//TODO REFACTOR AF const val VLC_EXTRA_DURATION_OUT = "extra_duration"
open class ResultResume( const val VLC_LAST_ID_KEY = "vlc_last_open_id"
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,
// Android 13 intent restrictions fucks up specifically launching the VLC player
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.player.result"
} else {
Intent.ACTION_VIEW
},
"extra_position",
"extra_duration",
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
}
}
val MPV = object : ResultResume(
MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
position = "position",
duration = "duration",
) {
override fun getPosition(intent: Intent?): Long {
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
}
}
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
val resumeApps = arrayOf(
VLC, MPV, WEB_VIDEO
)
// Short name for requests client to make it nicer to use // Short name for requests client to make it nicer to use
@ -244,151 +131,12 @@ var app = Requests(responseParser = object : ResponseParser {
class MainActivity : AppCompatActivity(), ColorPickerDialogListener { class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object { companion object {
const val TAG = "MAINACT" const val TAG = "MAINACT"
var context : MainActivity? = null
/**
* 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
* 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) {
// TODO MUCH BETTER HANDLING
// Invalid URIs can crash
fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
return true
} else if (str.contains(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")
// Use both navigation views to support both layouts.
// It might be better to use the QuickSearch.
nav_view?.selectedItemId = R.id.navigation_search
nav_rail_view?.selectedItemId = R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8")
navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
LinkGenerator(
listOf(BasicLink(url, name)),
extract = true,
)
)
)
} else if (safeURI(str)?.scheme == 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) {
@ -422,7 +170,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val isNavVisible = listOf( val isNavVisible = listOf(
R.id.navigation_home, R.id.navigation_home,
R.id.navigation_search, R.id.navigation_search,
R.id.navigation_library,
R.id.navigation_downloads, R.id.navigation_downloads,
R.id.navigation_settings, R.id.navigation_settings,
R.id.navigation_download_child, R.id.navigation_download_child,
@ -432,34 +179,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_settings_updates, R.id.navigation_settings_updates,
R.id.navigation_settings_ui, R.id.navigation_settings_ui,
R.id.navigation_settings_account, R.id.navigation_settings_account,
R.id.navigation_settings_providers, R.id.navigation_settings_lang,
R.id.navigation_settings_general, R.id.navigation_settings_general,
R.id.navigation_settings_extensions, R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins, R.id.navigation_settings_plugins,
R.id.navigation_test_providers,
).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
@ -474,11 +199,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.isVisible = isNavVisible && !landscape nav_view?.isVisible = isNavVisible && !landscape
nav_rail_view?.isVisible = isNavVisible && landscape nav_rail_view?.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
} }
//private var mCastSession: CastSession? = null //private var mCastSession: CastSession? = null
@ -518,7 +238,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
afterPluginsLoadedEvent += ::onAllPluginsLoaded
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
//mCastSession = mSessionManager.currentCastSession //mCastSession = mSessionManager.currentCastSession
@ -531,11 +250,6 @@ 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)
@ -566,34 +280,12 @@ 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() {
@ -605,12 +297,36 @@ 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"
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java) broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
this.sendBroadcast(broadcastIntent) this.sendBroadcast(broadcastIntent)
afterPluginsLoadedEvent -= ::onAllPluginsLoaded
super.onDestroy() super.onDestroy()
} }
@ -623,7 +339,56 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return if (intent == null) return
val str = intent.dataString val str = intent.dataString
loadCache() loadCache()
handleAppIntentUrl(this, str, false) if (str != null) {
if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?")
println("Repository url: $realUrl")
loadRepository(realUrl)
} else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
val activity = this
ioSafe {
Log.i(TAG, "handleAppIntent $str")
val isSuccessful = api.handleRedirect(str)
if (isSuccessful) {
Log.i(TAG, "authenticated ${api.name}")
} else {
Log.i(TAG, "failed to authenticate ${api.name}")
}
activity.runOnUiThread {
try {
showToast(
activity,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
)
} catch (e: Exception) {
logError(e) // format might fail
}
}
}
}
}
} else if (URI(str).scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
} else {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads)
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
break
}
}
}
}
}
} }
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean = private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
@ -651,85 +416,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
private val pluginsLock = Mutex()
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
allProviders.add(it.javaClass.newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
canBeOverridden = false
})
}
}
}
// it.hashCode() is not enough to make sure they are distinct
apis =
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null
} catch (e: Exception) {
logError(e)
}
}
}
}
lateinit var viewModel: ResultViewModel2
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?) {
context = this
app.initClient(this) app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val errorFile = filesDir.resolve("last_error")
var lastError: String? = null
if (errorFile.exists() && errorFile.isFile) {
lastError = errorFile.readText(Charset.defaultCharset())
errorFile.delete()
}
val settingsForProvider = SettingsJson()
settingsForProvider.enableAdult =
settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
MainAPI.settingsForProvider = settingsForProvider
loadThemes(this) loadThemes(this)
updateLocale() updateLocale()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -742,7 +433,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 {
@ -751,155 +442,43 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
changeStatusBarState(isEmulatorSettings()) changeStatusBarState(isEmulatorSettings())
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com ioSafe {
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) { getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
main { mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
if (checkGithubConnectivity()) { } ?: run {
this.setKey(getString(R.string.jsdelivr_proxy_key), false) mainPluginsLoadedEvent.invoke(false)
} else { }
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
val parentView: View = findViewById(android.R.id.content) if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) {
Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
.let { snackbar -> } else {
snackbar.setAction(R.string.revert) { PluginManager.loadAllOnlinePlugins(this@MainActivity)
setKey(getString(R.string.jsdelivr_proxy_key), false) }
PluginManager.loadAllLocalPlugins(this@MainActivity)
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
allProviders.add(it.javaClass.newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
canBeOverridden = false
})
} }
snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
snackbar.show()
}
}
}
}
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
ioSafe {
getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
} ?: run {
mainPluginsLoadedEvent.invoke(false)
}
ioSafe {
if (settingsManager.getBoolean(
getString(R.string.auto_update_plugins_key),
true
)
) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else {
loadAllOnlinePlugins(this@MainActivity)
}
//Automatically download not existing plugins
if (settingsManager.getBoolean(
getString(R.string.auto_download_plugins_key),
false
)
) {
PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
} }
} }
apis = allProviders.distinctBy { it }
ioSafe { APIHolder.apiMap = null
PluginManager.loadAllLocalPlugins(this@MainActivity, false) } catch (e: Exception) {
} logError(e)
} }
} else {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle(R.string.safe_mode_title)
builder.setMessage(R.string.safe_mode_description)
builder.apply {
setPositiveButton(R.string.safe_mode_crash_info) { _, _ ->
val tbBuilder: AlertDialog.Builder = AlertDialog.Builder(context)
tbBuilder.setTitle(R.string.safe_mode_title)
tbBuilder.setMessage(lastError)
tbBuilder.show()
}
setNegativeButton("Ok") { _, _ -> } afterPluginsLoadedEvent.invoke(true)
}
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 {
@ -917,8 +496,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
for (api in accountManagers) { for (api in accountManagers) {
api.init() api.init()
} }
}
inAppAuths.amap { api -> ioSafe {
inAppAuths.apmap { api ->
try { try {
api.initialize() api.initialize()
} catch (e: Exception) { } catch (e: Exception) {
@ -942,17 +523,6 @@ 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()
@ -966,12 +536,7 @@ 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,
@ -1140,22 +705,10 @@ 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(5000) // delay(1000)
// println("Current focus: $currentFocus") // println("Current focus: $currentFocus")
// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
// } // }
// } // }
} }
suspend fun checkGithubConnectivity(): Boolean {
return try {
app.get(
"https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck",
timeout = 5
).text.trim() == "ok"
} catch (t: Throwable) {
false
}
}
} }

View file

@ -1,7 +1,8 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.* import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections //https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
/* /*
@ -25,25 +26,10 @@ 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() }
} }
@ -52,12 +38,6 @@ 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,

View file

@ -1,40 +0,0 @@
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
}
}

View file

@ -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.*
open class Acefile : ExtractorApi() { 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,6 +27,7 @@ open 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-")
) )
) )
} }

View file

@ -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
open class AsianLoad : ExtractorApi() { 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

View file

@ -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
open class Blogger : ExtractorApi() { 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

View file

@ -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
open class BullStream : ExtractorApi() { 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 @@ open 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,

View file

@ -1,23 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.*
open class ByteShare : ExtractorApi() {
override val name = "ByteShare"
override val mainUrl = "https://byteshare.net"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
sources.add(
ExtractorLink(
name,
name,
url.replace("/embed/", "/download/"),
"",
Qualities.Unknown.value,
)
)
return sources
}
}

View file

@ -1,97 +0,0 @@
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
)
}

View file

@ -1,105 +0,0 @@
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.M3u8Helper.Companion.generateM3u8
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 { (_, video) ->
video.forEach {
getStream(it.url, this.name, callback)
}
}
}
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
}
private suspend fun getStream(
streamLink: String,
name: String,
callback: (ExtractorLink) -> Unit
) {
return generateM3u8(
name,
streamLink,
"",
).forEach(callback)
}
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
)
}

View file

@ -7,10 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class 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"
} }
@ -38,9 +34,6 @@ class DoodWsExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.ws" override var mainUrl = "https://dood.ws"
} }
class DoodYtExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.yt"
}
open class DoodLaExtractor : ExtractorApi() { open class DoodLaExtractor : ExtractorApi() {
override var name = "DoodStream" override var name = "DoodStream"

View file

@ -1,37 +0,0 @@
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-"
)
)
)
}
}

View file

@ -16,7 +16,26 @@ open class Evoload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val id = url.replace("https://evoload.io/e/", "") // wanted media id val lang = url.substring(0, 2)
val flag =
if (lang == "vo") {
" \uD83C\uDDEC\uD83C\uDDE7"
}
else if (lang == "vf"){
" \uD83C\uDDE8\uD83C\uDDF5"
} else {
""
}
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
url.substring(2, url.length)
}
//println(lang)
//println(cleaned_url)
val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars) val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass) val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
@ -25,9 +44,9 @@ open class Evoload : ExtractorApi() {
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,
name, name + flag,
link, link,
url, cleaned_url,
Qualities.Unknown.value, Qualities.Unknown.value,
) )
) )

View file

@ -1,54 +1,39 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.apmap
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
open class Fastream: ExtractorApi() { 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,
sources: ArrayList<ExtractorLink>): Boolean{ override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
response.select("script").amap { script -> val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { val sources = mutableListOf<ExtractorLink>()
val unpacked = getAndUnpack(script.data()) val response = app.post("$mainUrl/dl",
//val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)") data = mapOf(
val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"") Pair("op","embed"),
//val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach Pair("file_code",id),
Pair("auto","1")
)).document
response.select("script").apmap { script ->
if (script.data().contains("sources")) {
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
generateM3u8( generateM3u8(
name, name,
newm3u8link, m3u8,
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
} }
} }

View file

@ -1,57 +1,38 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Filesim : ExtractorApi() {
class Ztreamhub : Filesim() {
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
override val name = "Zstreamhub"
}
class FileMoon : Filesim() {
override val mainUrl = "https://filemoon.to"
override val name = "FileMoon"
}
class FileMoonSx : Filesim() {
override val mainUrl = "https://filemoon.sx"
override val name = "FileMoonSx"
}
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
override suspend fun getUrl( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
url: String, val sources = mutableListOf<ExtractorLink>()
referer: String?, with(app.get(url).document) {
subtitleCallback: (SubtitleFile) -> Unit, this.select("script").map { script ->
callback: (ExtractorLink) -> Unit if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
) { val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
val response = app.get(url, referer = mainUrl).document tryParseJson<List<ResponseSource>>("[$data]")?.map {
response.select("script[type=text/javascript]").map { script -> M3u8Helper.generateM3u8(
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) { name,
val unpackedscript = getAndUnpack(script.data()) it.file,
val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"") "$mainUrl/",
val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: "" ).forEach { m3uData -> sources.add(m3uData) }
if (m3u8.isNotEmpty()) { }
generateM3u8(
name,
m3u8,
mainUrl
).forEach(callback)
} }
} }
} }
return sources
} }
/* private data class ResponseSource( private data class ResponseSource(
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
@JsonProperty("type") val type: String?, @JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String? @JsonProperty("label") val label: String?
) */ )
} }

View file

@ -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.Qualities import com.lagradost.cloudstream3.utils.M3u8Helper
open class GMPlayer : ExtractorApi() { 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,16 +25,11 @@ open 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 listOf( return M3u8Helper.generateM3u8(
ExtractorLink( name,
this.name, m3u8,
this.name, ref,
m3u8, headers = mapOf("accept" to "*/*")
ref,
Qualities.Unknown.value,
headers = mapOf("accept" to "*/*"),
isM3u8 = true
)
) )
} }

View file

@ -1,209 +0,0 @@
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
)
}

View file

@ -1,88 +1,36 @@
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 Vanfem : GuardareStream() {
override var name = "Vanfem"
override var mainUrl = "https://vanfem.com/"
}
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>? {
// https://cinegrabber.com/asset/userdata/224879/caption/gqdmzh-71ez76z8/876438.srt val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text
data class GuardareCaptions( val jsonvideodata = AppUtils.parseJson<GuardareJsonData>(response)
@JsonProperty("id") val id: String, return jsonvideodata.data.map {
@JsonProperty("hash") val hash: String, ExtractorLink(
@JsonProperty("language") val language: String?, it.file+".${it.type}",
@JsonProperty("extension") val extension: String this.name,
) { it.file+".${it.type}",
fun getUrl(mainUrl: String, userId: String): String { mainUrl,
return "$mainUrl/asset/userdata/$userId/caption/$hash/$id.$extension" it.label.filter{ it.isDigit() }.toInt(),
} 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
)
)
}
}
} }
} }

View file

@ -1,72 +0,0 @@
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?,
)
}

View file

@ -1,53 +1,46 @@
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
open class Linkbox : ExtractorApi() { 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( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
url: String, val id = url.substringAfter("id=")
referer: String?, val sources = mutableListOf<ExtractorLink>()
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
) { sources.add(
val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1) ExtractorLink(
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url) name,
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link -> name,
callback.invoke( link.url,
ExtractorLink( url,
name, getQualityFromName(link.resolution)
name,
link.url ?: return@map null,
url,
getQualityFromName(link.resolution)
)
) )
} )
}
return sources
} }
data class Resolutions( data class RList(
@JsonProperty("url") val url: String? = null, @JsonProperty("url") val url: String,
@JsonProperty("resolution") val resolution: String? = null, @JsonProperty("resolution") val resolution: String?,
)
data class ItemInfo(
@JsonProperty("resolutionList") val resolutionList: ArrayList<Resolutions>? = arrayListOf(),
) )
data class Data( data class Data(
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null, @JsonProperty("rList") val rList: List<RList>?,
) )
data class Responses( data class Responses(
@JsonProperty("data") val data: Data? = null, @JsonProperty("data") val data: Data?,
) )
} }

View file

@ -0,0 +1,7 @@
package com.lagradost.cloudstream3.extractors
open class Mcloud : WcoStream() {
override var name = "Mcloud"
override var mainUrl = "https://mcloud.to"
override val requiresReferer = true
}

View file

@ -1,44 +0,0 @@
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"
)
)
}
}
}
}

View file

@ -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
open class Mp4Upload : ExtractorApi() { 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\("(.*?)"""")

View file

@ -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
open class MultiQuality : ExtractorApi() { 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*['"](.*?)['"]""")

View file

@ -1,47 +0,0 @@
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-"
)
)
)
}
}
}

View file

@ -1,39 +0,0 @@
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
}
}

View file

@ -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.amap import com.lagradost.cloudstream3.apmap
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.
* */ * */
open class Pelisplus(val mainUrl: String) { 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 @@ open class Pelisplus(val mainUrl: String) {
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
): Boolean { ): Boolean {
try { try {
normalApis.amap { api -> normalApis.apmap { 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 @@ open class Pelisplus(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P") val qualityRegex = Regex("(\\d+)P")
//a[download] //a[download]
pageDoc.select(".dowload > a")?.amap { element -> pageDoc.select(".dowload > a")?.apmap { element ->
val href = element.attr("href") ?: return@amap val href = element.attr("href") ?: return@apmap
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 @@ open 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 }.amap { api -> extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
if (link.startsWith(api.mainUrl)) { if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
} }

View file

@ -1,79 +0,0 @@
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
}
}

View file

@ -1,28 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
open class Sendvid : ExtractorApi() {
override var name = "Sendvid"
override val mainUrl = "https://sendvid.com"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val doc = app.get(url).document
val urlString = doc.select("head meta[property=og:video:secure_url]").attr("content")
if (urlString.contains("m3u8")) {
generateM3u8(
name,
urlString,
mainUrl,
).forEach(callback)
}
}
}

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
open class Solidfiles : ExtractorApi() { 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

View file

@ -7,11 +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.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class SpeedoStream1 : SpeedoStream() { class SpeedoStream : ExtractorApi() {
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

View file

@ -1,34 +1,12 @@
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() {
override var mainUrl = "https://vidgomunime.xyz"
}
class Sbthe : StreamSB() {
override var mainUrl = "https://sbthe.com"
}
class Ssbstream : StreamSB() { class Ssbstream : StreamSB() {
override var mainUrl = "https://ssbstream.net" override var mainUrl = "https://ssbstream.net"
} }
@ -77,10 +55,6 @@ class StreamSB10 : StreamSB() {
override var mainUrl = "https://sbplay2.xyz" override var mainUrl = "https://sbplay2.xyz"
} }
class StreamSB11 : StreamSB() {
override var mainUrl = "https://sbbrisk.com"
}
// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt // This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE // The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
open class StreamSB : ExtractorApi() { open class StreamSB : ExtractorApi() {
@ -102,15 +76,15 @@ open class StreamSB : ExtractorApi() {
} }
data class Subs ( data class Subs (
@JsonProperty("file") val file: String? = null, @JsonProperty("file") val file: String,
@JsonProperty("label") val label: String? = null, @JsonProperty("label") val label: String,
) )
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: ArrayList<Subs>? = arrayListOf(), @JsonProperty("subs") val subs: List<Subs>?,
@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,
@ -122,42 +96,31 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int, @JsonProperty("status_code") val statusCode: Int,
) )
override suspend fun getUrl( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
url: String, 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_-]+)")
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 master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" val bytes = id.toByteArray()
val master = "$mainUrl/sources15/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" val bytesToHex = bytesToHex(bytes)
val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val headers = mapOf( val headers = mapOf(
"watchsb" to "sbstream", "watchsb" to "streamsb",
)
val mapped = app.get(
master.lowercase(),
headers = headers,
referer = url,
).parsedSafe<Main>()
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
M3u8Helper.generateM3u8(
name,
mapped?.streamData?.file ?: return,
url,
headers = headers
).forEach(callback)
mapped.streamData.subs?.map {sub ->
subtitleCallback.invoke(
SubtitleFile(
sub.label.toString(),
sub.file ?: return@map null,
)
) )
} val urltext = app.get(master,
headers = headers,
allowRedirects = false
).text
val mapped = urltext.let { parseJson<Main>(it) }
val testurl = app.get(mapped.streamData.file, headers = headers).text
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
if (urltext.contains("m3u8") && testurl.contains("EXTM3U"))
return M3u8Helper.generateM3u8(
name,
mapped.streamData.file,
url,
headers = headers
)
return null
} }
} }

View file

@ -5,15 +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.Qualities import com.lagradost.cloudstream3.utils.Qualities
class StreamTapeNet : StreamTape() { class StreamTape : ExtractorApi() {
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
@ -24,8 +16,7 @@ open 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 = val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
"https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3)}"
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,

View file

@ -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
open class Streamhub : ExtractorApi() { 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

View file

@ -1,81 +0,0 @@
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,
)
}

View file

@ -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,13 +20,10 @@ open class Supervideo : ExtractorApi() {
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 = val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",")
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,
@ -37,6 +34,8 @@ open class Supervideo : ExtractorApi() {
} }
} }
} }
return extractedLinksList return extractedLinksList
} }
} }

View file

@ -1,64 +1,41 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue import com.lagradost.cloudstream3.app
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: String = "Cinestart" override var name = "Cinestart"
override val mainUrl: String = "https://cinestart.net" override var mainUrl = "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 val mainUrl = "https://tomatomatela.com" override var 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$embeddetails","$mainUrl/$details") val link = url.replace("$mainUrl/embed.html#","$mainUrl/$details")
val sources = ArrayList<ExtractorLink>() val server = app.get(link, allowRedirects = false).text
val server = app.get(link, allowRedirects = false, val json = parseJson<Tomato>(server)
headers = mapOf( if (json.status == 200) return listOf(
"User-Agent" to USER_AGENT, ExtractorLink(
"Accept" to "application/json, text/javascript, */*; q=0.01", name,
"Accept-Language" to "en-US,en;q=0.5", name,
"X-Requested-With" to "XMLHttpRequest", json.file,
"DNT" to "1", "",
"Connection" to "keep-alive", Qualities.Unknown.value,
"Sec-Fetch-Dest" to "empty", isM3u8 = false
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "same-origin"
) )
).parsedSafe<Tomato>() )
if (server?.file != null) { return null
sources.add(
ExtractorLink(
name,
name,
server.file,
"",
Qualities.Unknown.value,
isM3u8 = false
)
)
}
return sources
} }
} }

View file

@ -1,23 +1,19 @@
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.M3u8Helper import com.lagradost.cloudstream3.utils.Qualities
open class UpstreamExtractor : ExtractorApi() { class UpstreamExtractor: ExtractorApi() {
override val name: String = "Upstream" override val name: String = "Upstream.to"
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( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
url: String, // WIP: m3u8 link fetched but sometimes not playing
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)")
@ -34,9 +30,7 @@ open class UpstreamExtractor : ExtractorApi() {
domName = "${part}.${domName}" domName = "${part}.${domName}"
} }
domName.trimEnd('.') domName.trimEnd('.')
} else { } else { "" }
""
}
} }
false -> "" false -> ""
} }
@ -48,13 +42,18 @@ open class UpstreamExtractor : ExtractorApi() {
result?.forEach { result?.forEach {
val linkUrl = "https://${domain}/hls/${it}/master.m3u8" val linkUrl = "https://${domain}/hls/${it}/master.m3u8"
M3u8Helper.generateM3u8( sources.add(
this.name, ExtractorLink(
linkUrl, name = "Upstream m3u8",
"$mainUrl/", source = this.name,
headers = mapOf("Origin" to mainUrl) url = linkUrl,
).forEach(callback) quality = Qualities.Unknown.value,
referer = referer ?: linkUrl,
isM3u8 = true
)
)
} }
} }
return sources
} }
} }

View file

@ -7,10 +7,6 @@ class Uqload1 : Uqload() {
override var mainUrl = "https://uqload.com" override var mainUrl = "https://uqload.com"
} }
class Uqload2 : Uqload() {
override var mainUrl = "https://uqload.co"
}
open class Uqload : ExtractorApi() { open class Uqload : ExtractorApi() {
override val name: String = "Uqload" override val name: String = "Uqload"
override val mainUrl: String = "https://www.uqload.com" override val mainUrl: String = "https://www.uqload.com"
@ -19,14 +15,30 @@ open class Uqload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile" val lang = url.substring(0, 2)
val flag =
if (lang == "vo") {
" \uD83C\uDDEC\uD83C\uDDE7"
}
else if (lang == "vf"){
" \uD83C\uDDE8\uD83C\uDDF5"
} else {
""
}
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
url.substring(2, url.length)
}
with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link -> srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,
name, name + flag,
link, link,
url, cleaned_url,
Qualities.Unknown.value, Qualities.Unknown.value,
) )
) )

View file

@ -1,11 +1,12 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.apmap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.ExtractorApi
import kotlinx.coroutines.delay import com.lagradost.cloudstream3.utils.ExtractorLink
import java.net.URI import com.lagradost.cloudstream3.utils.M3u8Helper
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"
@ -26,25 +27,6 @@ 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?,
@ -58,10 +40,7 @@ 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( app.get("$absoluteUrl/src/$datahash", referer = "https://source.vidsrc.me/").url
"$absoluteUrl/srcrcp/$datahash",
referer = "https://rcp.vidsrc.me/"
).url
} catch (e: Exception) { } catch (e: Exception) {
"" ""
} }
@ -69,28 +48,17 @@ open class VidSrcExtractor : ExtractorApi() {
} else "" } else ""
} }
serverslist.amap { server -> serverslist.apmap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/prorcp")) { 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@amap val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap
val passRegex = Regex("""['"](.*set_pass[^"']*)""") M3u8Helper.generateM3u8(
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( name,
Regex("""^//"""), "https://" srcm3u8,
) 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)
} }

View file

@ -11,7 +11,7 @@ class VideovardSX : WcoStream() {
override var mainUrl = "https://videovard.sx" override var mainUrl = "https://videovard.sx"
} }
open class VideoVard : ExtractorApi() { 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

View file

@ -1,69 +0,0 @@
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,
)
}

View file

@ -1,34 +0,0 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack
class Vido : ExtractorApi() {
override var name = "Vido"
override var mainUrl = "https://vido.lol"
private val srcRegex = Regex("""sources:\s*\["(.*?)"\]""")
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val methode = app.get(url.replace("/e/", "/embed-")) // fix wiflix and mesfilms
with(methode) {
if (!methode.isSuccessful) return null
//val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
srcRegex.find(this.text)?.groupValues?.get(1)?.let { link ->
return listOf(
ExtractorLink(
name,
name,
link,
url,
Qualities.Unknown.value,
true,
)
)
}
}
return null
}
}

View file

@ -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.amap import com.lagradost.cloudstream3.apmap
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.amap { api -> normalApis.apmap { 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")?.amap { element -> pageDoc.select(".dowload > a")?.apmap { element ->
val href = element.attr("href") ?: return@amap val href = element.attr("href") ?: return@apmap
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 }.amap { api -> extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
if (link.startsWith(api.mainUrl)) { if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
} }

View file

@ -1,32 +0,0 @@
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)
}
}

View file

@ -13,42 +13,39 @@ open class VoeExtractor : ExtractorApi() {
override val requiresReferer = false override val requiresReferer = false
private data class ResponseLinks( private data class ResponseLinks(
@JsonProperty("hls") val hls: String?, @JsonProperty("hls") val url: 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 html = app.get(url).text val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
if (html.isNotBlank()) { val doc = app.get(url).text
val src = html.substringAfter("const sources =").substringBefore(";") if (doc.isNotBlank()) {
// Remove last comma, it is not proper json otherwise val start = "const sources ="
var src = doc.substring(doc.indexOf(start))
src = src.substring(start.length, src.indexOf(";"))
.replace("0,", "0") .replace("0,", "0")
// Make json use the proper quotes .trim()
.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
// Always defaults to the hls link, but returns the mp4 if null val linkLabel = voelink.label?.toString() ?: ""
val linkUrl = voeLink.hls ?: voeLink.mp4
val linkLabel = voeLink.label?.toString() ?: ""
if (!linkUrl.isNullOrEmpty()) { if (!linkUrl.isNullOrEmpty()) {
return listOf( extractedLinksList.add(
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 = voeLink.hls != null isM3u8 = true
) )
) )
} }
} }
} }
return emptyList() return extractedLinksList
} }
} }

View file

@ -53,12 +53,6 @@ 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"

View file

@ -1,32 +1,12 @@
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.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils
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() {
override val mainUrl: String = "https://rasa-cintaku-semakin-berantai.xyz"
}
class LayarKaca: XStreamCdn() { class LayarKaca: XStreamCdn() {
override val name: String = "LayarKaca-xxi" override val name: String = "LayarKaca-xxi"
override val mainUrl: String = "https://layarkacaxxi.icu" override val mainUrl: String = "https://layarkacaxxi.icu"
@ -70,67 +50,44 @@ 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("player") val player: Player? = null, @JsonProperty("data") val data: List<ResponseData>?
@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( override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
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}"
app.post(newUrl, headers = headers).let { res -> val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
val sources = tryParseJson<ResponseJson?>(res.text) with(app.post(newUrl, headers = headers)) {
sources?.let { if (this.code != 200) return listOf()
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.map { source -> it.data.forEach { data ->
callback.invoke( extractedLinksList.add(
ExtractorLink( ExtractorLink(
name, name,
name = name, name = name,
source.file, data.file,
url, url,
getQualityFromName(source.label), getQualityFromName(data.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
} }
} }

View file

@ -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
open class YourUpload: ExtractorApi() { 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

View file

@ -1,76 +0,0 @@
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.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
open class Zorofile : ExtractorApi() {
override val name = "Zorofile"
override val mainUrl = "https://zorofile.com"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val id = url.split("?").first().split("/").last()
val token = app.get(
url,
referer = referer
).document.select("button.g-recaptcha").attr("data-sitekey").let { captchaKey ->
getCaptchaToken(
url,
captchaKey,
referer = referer
)
} ?: throw ErrorLoadingException("can't bypass captcha")
val data = app.post(
"$mainUrl/dl",
data = mapOf(
"op" to "embed",
"file_code" to id,
"auto" to "1",
"referer" to "$referer/",
"g-recaptcha-response" to token
),
referer = url,
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",
"Origin" to mainUrl,
"Sec-Fetch-Dest" to "iframe",
"Sec-Fetch-Mode" to "navigate",
"Sec-Fetch-Site" to "same-origin",
"Sec-Fetch-User" to "?1",
"Upgrade-Insecure-Requests" to "1",
)
).document.select("script").find { it.data().contains("var holaplayer;") }?.data()
?.substringAfter("sources: [")?.substringBefore("],")?.replace("src", "\"src\"")
?.replace("type", "\"type\"")
tryParseJson<Sources>("$data")?.let { res ->
return M3u8Helper.generateM3u8(
name,
res.src ?: return@let,
"$mainUrl/",
headers = mapOf(
"Origin" to mainUrl,
)
).forEach(callback)
}
}
private data class Sources(
@JsonProperty("src") val src: String? = null,
@JsonProperty("type") val type: String? = null,
)
}

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.apmap
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().amap { urlm3u8 -> }.toList().apmap { 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")) {

View file

@ -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.amap import com.lagradost.cloudstream3.apmap
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.amap { links.apmap {
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()) {

View file

@ -0,0 +1,30 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {
val syncApis = SyncApis
suspend fun redirect(url: String, preferredUrl: String): String {
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(preferredUrl)
} ?: run {
throw ErrorLoadingException("Page does not exist on $preferredUrl")
}
}
}
return url
}
}

View file

@ -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?.amap { (apiName, data) -> metaData.movies?.apmap { (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.amap { api -> val data = validApis.apmap { 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@amap api.search(this.name)?.first { return@apmap api.search(this.name)?.first {
if (filterName(it.name).equals( if (filterName(it.name).equals(
matchName, matchName,
ignoreCase = true ignoreCase = true

View file

@ -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).amap { url -> val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url) validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull() }.filterNotNull()

View file

@ -1,56 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
val syncApis = SyncApis
private val syncIds =
listOf(
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
)
suspend fun redirect(
url: String,
providerApi: MainAPI
): String {
// Deprecated since providers should do this instead!
// Tries built in ID -> ProviderUrl
/*
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(providerApi.mainUrl)
}?.let {
return it
}
// ?: run {
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
// }
}
}
*/
// Tries provider solution
// This goes through all sync ids and finds supported id by said provider
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
if (providerApi.supportedSyncNames.contains(syncName)) {
syncRegex.find(url)?.value?.let {
suspendSafeApiCall {
providerApi.getLoadUrl(syncName, it)
}
}
} else null
} ?: url
}
}

View file

@ -7,7 +7,6 @@ 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
@ -15,7 +14,6 @@ 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")
@ -25,12 +23,6 @@ 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()))
@ -53,10 +45,6 @@ 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
@ -121,21 +109,13 @@ suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
} }
} }
fun Throwable.getAllMessages(): String {
return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "")
}
fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else ""
return prefix + this.stackTrace.joinToString(
separator = "\n"
) {
"${it.fileName} ${it.lineNumber}"
}
}
fun <T> safeFail(throwable: Throwable): Resource<T> { fun <T> safeFail(throwable: Throwable): Resource<T> {
val stackTraceMsg = throwable.getStackTracePretty() val stackTraceMsg =
(throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
separator = "\n"
) {
"${it.fileName} ${it.lineNumber}"
}
return Resource.Failure(false, null, null, stackTraceMsg) return Resource.Failure(false, null, null, stackTraceMsg)
} }
@ -147,8 +127,8 @@ fun CoroutineScope.launchSafe(
val obj: suspend CoroutineScope.() -> Unit = { val obj: suspend CoroutineScope.() -> Unit = {
try { try {
block() block()
} catch (throwable: Throwable) { } catch (e: Exception) {
logError(throwable) logError(e)
} }
} }
@ -177,7 +157,7 @@ suspend fun <T> safeApiCall(
} }
safeFail(throwable) safeFail(throwable)
} }
is SocketTimeoutException, is InterruptedIOException -> { is SocketTimeoutException -> {
Resource.Failure( Resource.Failure(
true, true,
null, null,
@ -212,7 +192,7 @@ suspend fun <T> safeApiCall(
true, true,
null, null,
null, null,
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS." (throwable.message ?: "SSLHandshakeException") + "\nTry again later."
) )
} }
else -> safeFail(throwable) else -> safeFail(throwable)

View file

@ -0,0 +1,483 @@
package com.lagradost.cloudstream3.network
import android.app.Dialog
import android.net.http.SslError
import android.util.Log
import android.view.View
import android.webkit.*
import android.widget.RelativeLayout
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.net.URI
enum class WebViewActions {
VISIT_ADDRESS,
WAIT_FOR_PAGE_LOAD,
WAIT_FOR_X_SECONDS,
WAIT_FOR_NETWORK_CALL,
WAIT_FOR_NETWORK_IDLE,
WAIT_FOR_ELEMENT,
WAIT_FOR_ELEMENT_GONE,
EXECUTE_JAVASCRIPT,
WAIT_FOR_ELEMENT_TO_BE_CLICKABLE,
CAPTURE_REQUESTS_THAT_MATCH_REGEX,
// SEND_KEYS_TO_ELEMENT,
RETURN
}
data class WebViewAction(val actionType: WebViewActions, val parameter: Any = "", val callback: (AdvancedWebView) -> Unit = { })
class AdvancedWebView private constructor(
val url: String,
val actions: ArrayList<WebViewAction>,
val referer: String?,
val method: String,
val callback: (AdvancedWebView) -> Unit = { },
val debug: Boolean = false
) {
companion object {
const val TAG = "AdvancedWebViewTag"
}
val headers = mapOf<String, String>()
var webView: WebView? = null
val remainingActions: ArrayList<WebViewAction> = actions
var currentHTML: String = ""
// Made this a getter, because `currentHTML` changes on the fly
val document: Document?
get() = try { Jsoup.parse(currentHTML) } catch (e: Exception) { null }
private val Instance = this
data class Builder(
var url: String = "",
var actions: ArrayList<WebViewAction> = arrayListOf(),
var referer: String? = null,
var method: String = "GET",
var debug: Boolean = false
) {
fun visitAddress(url: String, cb: (AdvancedWebView) -> Unit = { }) = apply {
if (this.url != "") {
addAction(WebViewAction(WebViewActions.VISIT_ADDRESS, url, cb))
} else this.url = url
}
fun setReferer(referer: String) = apply { this.referer = referer }
fun setMethod(method: String) = apply { this.method = method }
private fun addAction(action: WebViewAction) = apply { this.actions.add(action) }
fun waitForElement(selector: String, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_ELEMENT, selector, cb))
}
fun waitForElementGone(selector: String, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_ELEMENT, selector, cb))
}
fun waitForElementToBeClickable(selector: String, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_ELEMENT_TO_BE_CLICKABLE, selector, cb))
}
fun waitForSeconds(seconds: Long, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_X_SECONDS, seconds, cb))
}
fun waitForPageLoad(cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_PAGE_LOAD, "", cb))
}
fun waitForNetworkIdle(cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_NETWORK_IDLE, "", cb))
}
fun waitForNetworkCall(targetResource: String, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.WAIT_FOR_NETWORK_CALL, targetResource, cb))
}
fun executeJavaScript(code: String, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.EXECUTE_JAVASCRIPT, code, cb))
}
fun captureReqsThatMatchRegex(regex: Regex, cb: (AdvancedWebView) -> Unit = { }) = apply {
addAction(WebViewAction(WebViewActions.CAPTURE_REQUESTS_THAT_MATCH_REGEX, regex, cb))
}
// fun sendKeysToElement(selector: String, text: String, delayInMsPerKeyPress: Long = 50, cb: (AdvancedWebView) -> Unit = { }) = apply {
// addAction(WebViewAction(WebViewActions.SEND_KEYS_TO_ELEMENT, "$selector(__++`__||__`++__)$text(__++`__||__`++__)$delayInMsPerKeyPress", cb))
// }
fun debug() = apply { debug = true }
fun close() = apply { addAction(WebViewAction(WebViewActions.RETURN, "")) }
fun build(callback: (AdvancedWebView) -> Unit = { }) = AdvancedWebView(this.url, this.actions, this.referer, this.method, callback, debug)
fun buildAndStart(callback: (AdvancedWebView) -> Unit = { }) = build(callback).apply { this.start() }
}
private var actionExecutionsPaused = false
private var networkIdleTimestamp = -1;
private var pageHasLoaded = false;
private var isInSleep = false
private var isSendingKeys = false
private var actionStartTimestamp = -1;
private fun onActionEnded() {
actionExecutionsPaused = false
isSendingKeys = false
actionStartTimestamp = -1
}
var Error = ""
private suspend fun tryExecuteAction() {
if (actionExecutionsPaused || remainingActions.size == 0) return
actionExecutionsPaused = true
actionStartTimestamp = (System.currentTimeMillis() / 1000).toInt()
main {
if (remainingActions.size > 0) {
val action = remainingActions[0]
when (action.actionType){
WebViewActions.WAIT_FOR_ELEMENT -> {
webView?.evaluateJavascript("document.querySelector(\"${action.parameter}\")") {
Log.i(TAG, "WAIT_FOR_ELEMENT:: <$it>")
if (it == "{}") {
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
}
onActionEnded()
}
}
WebViewActions.VISIT_ADDRESS -> {
webView?.loadUrl(action.parameter as String)
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
onActionEnded()
}
// WebViewActions.SEND_KEYS_TO_ELEMENT -> {
// isSendingKeys = true
// val (element, characters, timing) = (action.parameter as String).split("(__++`__||__`++__)") // discriminator
// val msPerKey: Long = timing.toLongOrNull() ?: return@main
//
// Log.i(TAG, "SEND_KEYS_TO_ELEMENT:: start")
// webView?.evaluateJavascript("document.querySelector(`$element`)?.click()") {
// main {
// delay(300)
// for (character in characters) {
// Log.i(TAG, "SEND_KEYS_TO_ELEMENT:: character :: $character")
//
// webView?.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, character.code))
// delay(70)
// webView?.dispatchKeyEvent(KeyEvent(KeyEvent.ACTION_UP, character.code))
// delay(msPerKey)
// }
//
// updateCurrentHtmlAndRun(action.callback)
// remainingActions.remove(action)
// onActionEnded()
// }
// }
// }
WebViewActions.WAIT_FOR_ELEMENT_TO_BE_CLICKABLE -> {
webView?.evaluateJavascript(
"""
((selector) => {
const elem = document.querySelector(selector)
if (elem == undefined) return
const attribute = elem.getAttribute("disabled")
if (attribute === "true" || attribute === '') return
return "" + (!elem.disabled || true)
})(`${action.parameter}`);
""".trimIndent()) {
if (it == "\"true\""){
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
}
onActionEnded()
}
}
WebViewActions.WAIT_FOR_ELEMENT_GONE -> {
webView?.evaluateJavascript("\"\"+ (document.querySelector(\"${action.parameter}\") == undefined)") {
if (it == "\"true\"") {
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
}
onActionEnded()
}
}
WebViewActions.WAIT_FOR_NETWORK_IDLE -> {
// if (!pageHasLoaded || ((System.currentTimeMillis() / 1000L) - networkIdleTimestamp) < 10) return@main
// we need at least 10 seconds of no network calls being done in order to be in an "IDLE" state
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
onActionEnded()
}
WebViewActions.WAIT_FOR_X_SECONDS -> {
Log.i(TAG, "Waiting for ${remainingActions[0].parameter} seconds...")
isInSleep = true
delay(action.parameter as Long * 1000)
isInSleep = false
Log.i(TAG, "Finished waiting!")
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
onActionEnded()
}
WebViewActions.EXECUTE_JAVASCRIPT -> {
Log.i(TAG, "Executing javascript from action...")
webView?.evaluateJavascript(action.parameter as String) {
Log.i(TAG, "JavaScript Execution done! Result: <$it>")
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
onActionEnded()
}
}
WebViewActions.RETURN -> {
updateCurrentHtmlAndRun() { /* Do nothing, we only want to update the html */ }
destroyWebView()
remainingActions.clear()
actionStartTimestamp = -1
}
else -> {
onActionEnded()
}
}
}
}
}
private fun destroyWebView() {
main {
webView?.stopLoading()
webView?.destroy()
webView = null
Log.i(TAG, "Destroyed the WebView!")
}
}
private fun updateCurrentHtmlAndRun(cb: (AdvancedWebView) -> Unit) {
if (webView == null) {
Instance.run(cb)
return
}
main {
webView?.evaluateJavascript("document.documentElement.outerHTML") {
currentHTML = it
.replace("\\u003C", "<")
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\t", "\t")
.trimStart('"').trimEnd('"')
Instance.run(cb)
}
}
}
var initialized = false
suspend fun waitUntilDone() = apply {
while (!initialized) {
delay(100)
}
while (webView != null) {
delay(100)
}
}
val capturedRequests = arrayListOf<WebResourceResponse>()
private var dialog: Dialog? = null
fun start() {
main {
try {
webView = WebView(
AcraApplication.context
?: throw RuntimeException("No base context in WebViewResolver")
).apply {
// Bare minimum to bypass captcha
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.userAgentString = USER_AGENT
settings.blockNetworkImage = true
}
} catch (e: Exception) {
Error = "Error: Failed to create an Advanced WebView, reason: <${e.message}>"
Log.e(TAG, Error)
Log.e(TAG, e.toString())
destroyWebView()
callback(this)
}
if (debug) {
webView!!.visibility = View.VISIBLE
val layout = RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT
)
dialog = Dialog(MainActivity.context!!, android.R.style.Theme_Black_NoTitleBar_Fullscreen)
dialog!!.addContentView(webView as View, layout)
dialog!!.show()
}
try {
webView?.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
pageHasLoaded = true
networkIdleTimestamp = (System.currentTimeMillis() / 1000).toInt();
if (remainingActions.size > 0 && remainingActions[0].actionType == WebViewActions.WAIT_FOR_PAGE_LOAD) {
Log.i(TAG, "PAGE FINISHED!")
val action = remainingActions[0]
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
}
}
override fun onLoadResource(view: WebView?, url: String?) {
super.onLoadResource(view, url)
networkIdleTimestamp = (System.currentTimeMillis() / 1000L).toInt();
if (remainingActions.size > 0) {
val action = remainingActions[0]
when (action.actionType) {
WebViewActions.WAIT_FOR_NETWORK_CALL -> {
if (URI(url) == URI(action.parameter as String)) {
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
}
}
else -> { /* nothing */ }
}
}
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? = runBlocking {
networkIdleTimestamp = (System.currentTimeMillis() / 1000L).toInt();
val webViewUrl = request.url.toString()
val blacklistedFiles = listOf(
".jpg", ".png", ".webp", ".mpg",
".mpeg", ".jpeg", ".webm", ".mp4",
".mp3", ".gifv", ".flv", ".asf",
".mov", ".mng", ".mkv", ".ogg",
".avi", ".wav", ".woff2", ".woff",
".ttf", ".vtt", ".srt",
".ts", ".gif",
// Warning, this might fuck some future sites, but it's used to make Sflix work.
"wss://"
)
val response = try {
when {
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
"/favicon.ico"
) -> WebResourceResponse(
"image/png",
null,
null
)
request.method == "GET" -> app.get(
webViewUrl,
headers = request.requestHeaders
).okhttpResponse.toWebResourceResponse()
request.method == "POST" -> app.post(
webViewUrl,
headers = request.requestHeaders
).okhttpResponse.toWebResourceResponse()
else -> return@runBlocking super.shouldInterceptRequest(
view,
request
)
}
} catch (e: Exception) {
null
}
if (remainingActions.size > 0){
val action = remainingActions[0]
when (action.actionType) {
WebViewActions.CAPTURE_REQUESTS_THAT_MATCH_REGEX -> {
if ((action.parameter as Regex).containsMatchIn(webViewUrl)) {
if (response != null) capturedRequests.add(response)
updateCurrentHtmlAndRun(action.callback)
remainingActions.remove(action)
}
}
else -> { /* nothing */ }
}
}
return@runBlocking response
}
override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler?,
error: SslError?
) {
handler?.proceed() // Ignore ssl issues
}
}
webView?.loadUrl(url, headers.toMap())
} catch (e: Exception){
Error = "Failed to create a WebView client!"
Log.e(TAG, Error)
destroyWebView()
Instance.run(callback)
return@main
}
initialized = true
while (remainingActions.size > 0 && webView != null) {
if (!isInSleep && !isSendingKeys && actionStartTimestamp != -1 && ((System.currentTimeMillis()/1000) - actionStartTimestamp > 20)) {
Log.e(TAG, "AdvancedWebview:: Timeout, an action failed to end in under 20 seconds...")
Error = "ActionTimeout"
break
}
delay(300)
if (!actionExecutionsPaused) tryExecuteAction()
}
try {
updateCurrentHtmlAndRun(callback)
} catch (e: Exception) {
Log.e(TAG, "Err: $e")
}
if (debug) dialog!!.hide()
destroyWebView()
}
}
fun Response.toWebResourceResponse(): WebResourceResponse {
val contentTypeValue = this.header("Content-Type")
// 1. contentType. 2. charset
val typeRegex = Regex("""(.*);(?:.*charset=(.*)(?:|;)|)""")
return if (contentTypeValue != null) {
val found = typeRegex.find(contentTypeValue)
val contentType = found?.groupValues?.getOrNull(1)?.ifBlank { null } ?: contentTypeValue
val charset = found?.groupValues?.getOrNull(2)?.ifBlank { null }
WebResourceResponse(contentType, charset, this.body?.byteStream())
} else {
WebResourceResponse("application/octet-stream", null, this.body?.byteStream())
}
}
}

View file

@ -5,12 +5,10 @@ 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
import okhttp3.* import okhttp3.*
import java.net.URI
@AnyThread @AnyThread
@ -25,27 +23,8 @@ class CloudflareKiller : Interceptor {
} }
} }
init {
// Needs to clear cookies between sessions to generate new cookies.
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()
/**
* Gets the headers with cookies, webview user agent included!
* */
fun getCookieHeaders(url: String): Headers {
val userAgentHeaders = WebViewResolver.webViewUserAgent?.let {
mapOf("user-agent" to it)
} ?: emptyMap()
return getHeaders(userAgentHeaders, savedCookies[URI(url).host] ?: emptyMap())
}
override fun intercept(chain: Interceptor.Chain): Response = runBlocking { override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
val request = chain.request() val request = chain.request()
val cookies = savedCookies[request.url.host] val cookies = savedCookies[request.url.host]
@ -64,9 +43,7 @@ class CloudflareKiller : Interceptor {
} }
private fun getWebViewCookie(url: String): String? { private fun getWebViewCookie(url: String): String? {
return normalSafeApiCall { return CookieManager.getInstance()?.getCookie(url)
CookieManager.getInstance()?.getCookie(url)
}
} }
/** /**

View file

@ -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 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
import okhttp3.Interceptor import okhttp3.Interceptor
@ -41,8 +41,7 @@ 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 {
// Somehow app.get fails app.get(it, cacheTime = 0).cookies.also { cookies ->
Requests().get(it).cookies.also { cookies ->
savedCookiesMap[request.url.host] = cookies savedCookiesMap[request.url.host] = cookies
} }
} }
@ -52,6 +51,6 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
request.newBuilder() request.newBuilder()
.headers(headers) .headers(headers)
.build() .build()
).execute() ).await()
} }
} }

View file

@ -65,23 +65,3 @@ 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",
)
))

View file

@ -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 org.conscrypt.Conscrypt import okhttp3.Request
import java.io.File import java.io.File
import java.security.Security import java.util.concurrent.TimeUnit
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,8 +36,6 @@ 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

View file

@ -7,12 +7,9 @@ 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
@ -67,15 +64,9 @@ 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 try { return resolveUsingWebView(
resolveUsingWebView( requestCreator(method, url, referer = referer), requestCallBack
requestCreator(method, url, referer = referer), requestCallBack )
)
} catch (e: java.lang.IllegalArgumentException) {
logError(e)
debugException { "ILLEGAL URL IN resolveUsingWebView!" }
return null to emptyList()
}
} }
/** /**
@ -105,7 +96,7 @@ class WebViewResolver(
} }
var fixedRequest: Request? = null var fixedRequest: Request? = null
val extraRequestList = threadSafeListOf<Request>() val extraRequestList = mutableListOf<Request>()
main { main {
// Useful for debugging // Useful for debugging
@ -137,7 +128,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")
@ -146,41 +137,22 @@ class WebViewResolver(
} }
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) { if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
request.toRequest()?.also { extraRequestList.add(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
// Less data, low chance of causing issues. // Less data, low chance of causing issues.
// blockNetworkImage also does this job but i will keep it for the future. // blockNetworkImage also does this job but i will keep it for the future.
val blacklistedFiles = listOf( val blacklistedFiles = listOf(
".jpg", ".jpg", ".png", ".webp", ".mpg",
".png", ".mpeg", ".jpeg", ".webm", ".mp4",
".webp", ".mp3", ".gifv", ".flv", ".asf",
".mpg", ".mov", ".mng", ".mkv", ".ogg",
".mpeg", ".avi", ".wav", ".woff2", ".woff",
".jpeg", ".ttf", ".vtt", ".srt",
".webm", ".ts", ".gif",
".mp4",
".mp3",
".gifv",
".flv",
".asf",
".mov",
".mng",
".mkv",
".ogg",
".avi",
".wav",
".woff2",
".woff",
".ttf",
".css",
".vtt",
".srt",
".ts",
".gif",
// Warning, this might fuck some future sites, but it's used to make Sflix work. // Warning, this might fuck some future sites, but it's used to make Sflix work.
"wss://" "wss://"
) )
@ -259,19 +231,14 @@ class WebViewResolver(
} }
fun WebResourceRequest.toRequest(): Request? { fun WebResourceRequest.toRequest(): Request {
val webViewUrl = this.url.toString() val webViewUrl = this.url.toString()
// If invalid url then it can crash with return requestCreator(
// java.lang.IllegalArgumentException: Expected URL scheme 'http' or 'https' but was 'data' this.method,
// At Request.Builder().url(addParamsToUrl(url, params)) webViewUrl,
return normalSafeApiCall { this.requestHeaders,
requestCreator( )
this.method,
webViewUrl,
this.requestHeaders,
)
}
} }
fun Response.toWebResourceResponse(): WebResourceResponse { fun Response.toWebResourceResponse(): WebResourceResponse {

Some files were not shown because too many files have changed in this diff Show more