diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index cd3c2574..250734cd 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,8 +1,8 @@
blank_issues_enabled: false
contact_links:
- - name: Report provider bug
+ - name: Request a new provider or report bug with an existing provider
url: https://github.com/recloudstream
- about: Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
+ about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
- name: Discord
url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues.
diff --git a/.github/downloads.jpg b/.github/downloads.jpg
index 0b671edc..ca14a664 100644
Binary files a/.github/downloads.jpg and b/.github/downloads.jpg differ
diff --git a/.github/home.jpg b/.github/home.jpg
index 2ccfaff4..72370d3c 100644
Binary files a/.github/home.jpg and b/.github/home.jpg differ
diff --git a/.github/locales.py b/.github/locales.py
new file mode 100644
index 00000000..e95ec902
--- /dev/null
+++ b/.github/locales.py
@@ -0,0 +1,47 @@
+import re
+import glob
+import requests
+
+
+SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
+START_MARKER = "/* begin language list */"
+END_MARKER = "/* end language list */"
+XML_NAME = "app/src/main/res/values-"
+ISO_MAP_URL = "https://gist.githubusercontent.com/Josantonius/b455e315bc7f790d14b136d61d9ae469/raw"
+INDENT = " "*4
+
+iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
+
+# Load settings file
+src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
+before_src, rest = src.split(START_MARKER)
+rest, after_src = rest.split(END_MARKER)
+
+# Load already added langs
+languages = {}
+for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
+ flag, name, iso = lang.groups()
+ languages[iso] = (flag, name)
+
+# Add not yet added langs
+for folder in glob.glob(f"{XML_NAME}*"):
+ iso = folder[len(XML_NAME):]
+ if iso not in languages.keys():
+ languages[iso] = ("", iso_map.get(iso.lower(),iso))
+
+# Create triples
+triples = []
+for iso in sorted(languages.keys()):
+ flag, name = languages[iso]
+ triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
+
+# Update settings file
+open(SETTINGS_PATH, "w+",encoding='utf-8').write(
+ before_src +
+ START_MARKER +
+ "\n" +
+ "\n".join(triples) +
+ "\n" +
+ END_MARKER +
+ after_src
+)
\ No newline at end of file
diff --git a/.github/player.jpg b/.github/player.jpg
index 0580fb03..f6959cf3 100644
Binary files a/.github/player.jpg and b/.github/player.jpg differ
diff --git a/.github/results.jpg b/.github/results.jpg
index 5e63169f..4dbc9b8d 100644
Binary files a/.github/results.jpg and b/.github/results.jpg differ
diff --git a/.github/search.jpg b/.github/search.jpg
index 998b7753..784bec89 100644
Binary files a/.github/search.jpg and b/.github/search.jpg differ
diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml
new file mode 100644
index 00000000..83430766
--- /dev/null
+++ b/.github/workflows/build_to_archive.yml
@@ -0,0 +1,76 @@
+name: Archive build
+
+on:
+ push:
+ branches: [ master ]
+ paths-ignore:
+ - '*.md'
+ - '*.json'
+ - '**/wcokey.txt'
+ workflow_dispatch:
+
+concurrency:
+ group: "Archive-build"
+ cancel-in-progress: true
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate access token
+ id: generate_token
+ uses: tibdex/github-app-token@v1
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ repository: "recloudstream/secrets"
+ - name: Generate access token (archive)
+ id: generate_archive_token
+ uses: tibdex/github-app-token@v1
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ repository: "recloudstream/cloudstream-archive"
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
+ with:
+ java-version: '11'
+ distribution: 'adopt'
+ - name: Grant execute permission for gradlew
+ run: chmod +x gradlew
+ - name: Fetch keystore
+ id: fetch_keystore
+ run: |
+ TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
+ mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
+ curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
+ curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
+ KEY_PWD="$(cat keystore_password.txt)"
+ echo "::add-mask::${KEY_PWD}"
+ echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
+ - name: Run Gradle
+ run: |
+ ./gradlew assemblePrerelease
+ env:
+ SIGNING_KEY_ALIAS: "key0"
+ SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
+ SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
+ - uses: actions/checkout@v3
+ with:
+ repository: "recloudstream/cloudstream-archive"
+ token: ${{ steps.generate_archive_token.outputs.token }}
+ path: "archive"
+
+ - name: Move build
+ run: |
+ cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
+
+ - name: Push archive
+ run: |
+ cd $GITHUB_WORKSPACE/archive
+ git config --local user.email "actions@github.com"
+ git config --local user.name "GitHub Actions"
+ git add .
+ git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
+ git push --force
\ No newline at end of file
diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml
index 032ea8d0..3c5caad7 100644
--- a/.github/workflows/generate_dokka.yml
+++ b/.github/workflows/generate_dokka.yml
@@ -39,9 +39,8 @@ jobs:
- name: Clean old builds
run: |
- shopt -s extglob
cd $GITHUB_WORKSPACE/dokka/
- rm -rf !(.git)
+ rm -rf "./-cloudstream"
- name: Setup JDK 11
uses: actions/setup-java@v1
diff --git a/.github/workflows/issue-action.yml b/.github/workflows/issue_action.yml
similarity index 93%
rename from .github/workflows/issue-action.yml
rename to .github/workflows/issue_action.yml
index bfcb10d0..28b737b3 100644
--- a/.github/workflows/issue-action.yml
+++ b/.github/workflows/issue_action.yml
@@ -1,63 +1,63 @@
-name: Issue automatic actions
-
-on:
- issues:
- types: [opened, edited]
-
-jobs:
- issue-moderator:
- runs-on: ubuntu-latest
- steps:
- - name: Generate access token
- id: generate_token
- uses: tibdex/github-app-token@v1
- with:
- app_id: ${{ secrets.GH_APP_ID }}
- private_key: ${{ secrets.GH_APP_KEY }}
- - name: Similarity analysis
- uses: actions-cool/issues-similarity-analysis@v1
- with:
- token: ${{ steps.generate_token.outputs.token }}
- filter-threshold: 0.5
- title-excludes: ''
- comment-title: |
- ### Your issue looks similar to these issues:
- Please close if duplicate.
- comment-body: '${index}. ${similarity} #${number}'
- - uses: actions/checkout@v2
- - name: Automatically close issues that dont follow the issue template
- uses: lucasbento/auto-close-issues@v1.0.2
- with:
- github-token: ${{ steps.generate_token.outputs.token }}
- issue-close-message: |
- @${issue.user.login}: hello! :wave:
- This issue is being automatically closed because it does not follow the issue template."
- closed-issues-label: "invalid"
- - name: Check if issue mentions a provider
- id: provider_check
- env:
- GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
- run: |
- wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
- pip3 install httpx
- RES="$(python3 ./check_issue.py)"
- echo "::set-output name=name::${RES}"
- - name: Comment if issue mentions a provider
- if: steps.provider_check.outputs.name != 'none'
- uses: actions-cool/issues-helper@v3
- with:
- actions: 'create-comment'
- token: ${{ steps.generate_token.outputs.token }}
- body: |
- Hello ${{ github.event.issue.user.login }}.
- Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
-
- Found provider name: `${{ steps.provider_check.outputs.name }}`
- - name: Add eyes reaction to all issues
- uses: actions-cool/emoji-helper@v1.0.0
- with:
- type: 'issue'
- token: ${{ steps.generate_token.outputs.token }}
- emoji: 'eyes'
-
-
+name: Issue automatic actions
+
+on:
+ issues:
+ types: [opened]
+
+jobs:
+ issue-moderator:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate access token
+ id: generate_token
+ uses: tibdex/github-app-token@v1
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ - name: Similarity analysis
+ uses: actions-cool/issues-similarity-analysis@v1
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ filter-threshold: 0.60
+ title-excludes: ''
+ comment-title: |
+ ### Your issue looks similar to these issues:
+ Please close if duplicate.
+ comment-body: '${index}. ${similarity} #${number}'
+ - uses: actions/checkout@v2
+ - name: Automatically close issues that dont follow the issue template
+ uses: lucasbento/auto-close-issues@v1.0.2
+ with:
+ github-token: ${{ steps.generate_token.outputs.token }}
+ issue-close-message: |
+ @${issue.user.login}: hello! :wave:
+ This issue is being automatically closed because it does not follow the issue template."
+ closed-issues-label: "invalid"
+ - name: Check if issue mentions a provider
+ id: provider_check
+ env:
+ GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
+ run: |
+ wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
+ pip3 install httpx
+ RES="$(python3 ./check_issue.py)"
+ echo "name=${RES}" >> $GITHUB_OUTPUT
+ - name: Comment if issue mentions a provider
+ if: steps.provider_check.outputs.name != 'none'
+ uses: actions-cool/issues-helper@v3
+ with:
+ actions: 'create-comment'
+ token: ${{ steps.generate_token.outputs.token }}
+ body: |
+ Hello ${{ github.event.issue.user.login }}.
+ Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
+
+ Found provider name: `${{ steps.provider_check.outputs.name }}`
+ - name: Add eyes reaction to all issues
+ uses: actions-cool/emoji-helper@v1.0.0
+ with:
+ type: 'issue'
+ token: ${{ steps.generate_token.outputs.token }}
+ emoji: 'eyes'
+
+
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index 71301e25..4ce7dba1 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -40,12 +40,10 @@ jobs:
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
- echo "::set-output name=key_pwd::$KEY_PWD"
+ echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
- ./gradlew assemblePrerelease
- ./gradlew androidSourcesJar
- ./gradlew makeJar
+ ./gradlew assemblePrerelease makeJar androidSourcesJar
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
@@ -55,9 +53,9 @@ jobs:
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "pre-release"
- prerelease: false
+ prerelease: true
title: "Pre-release Build"
files: |
- app/build/outputs/apk/prerelease/*.apk
+ app/build/outputs/apk/prerelease/release/*.apk
app/build/libs/app-sources.jar
app/build/classes.jar
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 1a4db134..36199cd6 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -15,9 +15,9 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run Gradle
- run: ./gradlew assembleDebug
+ run: ./gradlew assemblePrereleaseDebug
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
name: pull-request-build
- path: "app/build/outputs/apk/debug/*.apk"
+ path: "app/build/outputs/apk/prerelease/debug/*.apk"
diff --git a/.github/workflows/update_locales.yml b/.github/workflows/update_locales.yml
new file mode 100644
index 00000000..93cdca44
--- /dev/null
+++ b/.github/workflows/update_locales.yml
@@ -0,0 +1,39 @@
+name: Update locale lists
+
+on:
+ workflow_dispatch:
+ push:
+ paths:
+ - '**.xml'
+ branches:
+ - master
+
+concurrency:
+ group: "locale-list"
+ cancel-in-progress: true
+
+jobs:
+ create:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Generate access token
+ id: generate_token
+ uses: tibdex/github-app-token@v1
+ with:
+ app_id: ${{ secrets.GH_APP_ID }}
+ private_key: ${{ secrets.GH_APP_KEY }}
+ repository: "recloudstream/cloudstream"
+ - uses: actions/checkout@v2
+ with:
+ token: ${{ steps.generate_token.outputs.token }}
+ - name: Edit files
+ run: |
+ python3 .github/locales.py
+ - name: Commit to the repo
+ run: |
+ git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
+ git config --local user.name "recloudstream[bot]"
+ git add .
+ # "echo" returns true so the build succeeds, even if no changed files
+ git commit -m 'update list of locales' || echo
+ git push
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index 652d9f3f..333d4937 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -31,5 +31,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 0035daf7..3430d626 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,23 @@
# CloudStream
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
-You can find the list of community-maintained extension repositories [here
-](https://recloudstream.github.io/repos/)
-[![Discord](https://img.shields.io/discord/737724143126052974?style=for-the-badge)](https://discord.gg/5Hus6fM)
+[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
-***Features:***
+### Features:
+ **AdFree**, No ads whatsoever
+ No tracking/analytics
+ Bookmarks
+ Download and stream movies, tv-shows and anime
+ Chromecast
-***Screenshots:***
+### Screenshots:
-***The list of supported languages:***
-* 🇱🇧 Arabic
-* 🇨🇿 Czech
-* 🇳🇱 Dutch
-* 🇬🇧 English
-* 🇫🇷 French
-* 🇩🇪 German
-* 🇬🇷 Greek
-* 🇮🇳 Hindi
-* 🇮🇩 Indonesian
-* 🇮🇹 Italian
-* 🇲🇰 Macedonian
-* 🇮🇳 Malayalam
-* 🇳🇴 Norsk
-* 🇵🇱 Polish
-* 🇧🇷 Portuguese (Brazil)
-* 🇷🇴 Romanian
-* 🇪🇸 Spanish
-* 🇸🇪 Swedish
-* 🇵🇭 Tagalog
-* 🇹🇷 Turkish
-* 🇻🇳 Vietnamese
-
+### Supported languages:
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
deleted file mode 100644
index b1cb3a6b..00000000
--- a/app/build.gradle
+++ /dev/null
@@ -1,215 +0,0 @@
-plugins {
- id 'com.android.application'
- id 'kotlin-android'
- id 'kotlin-kapt'
- id 'kotlin-android-extensions'
- id 'org.jetbrains.dokka'
-}
-
-def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
-def allFilesFromDir = new File(tmpFilePath).listFiles()
-def prereleaseStoreFile = null
-if (allFilesFromDir != null) {
- prereleaseStoreFile = allFilesFromDir.first()
-}
-
-android {
- testOptions {
- unitTests.returnDefaultValues = true
- }
- signingConfigs {
- prerelease {
- if (prereleaseStoreFile != null) {
- storeFile = file(prereleaseStoreFile)
- storePassword System.getenv("SIGNING_STORE_PASSWORD")
- keyAlias System.getenv("SIGNING_KEY_ALIAS")
- keyPassword System.getenv("SIGNING_KEY_PASSWORD")
- }
- }
- }
- compileSdkVersion 31
- buildToolsVersion "30.0.3"
-
- defaultConfig {
- applicationId "com.lagradost.cloudstream3"
- minSdkVersion 21
- targetSdkVersion 30
-
- versionCode 50
- versionName "3.1.4"
-
- resValue "string", "app_version",
- "${defaultConfig.versionName}${versionNameSuffix ?: ""}"
-
- resValue "string", "commit_hash",
- ("git rev-parse --short HEAD".execute().text.trim() ?: "")
-
- resValue "bool", "is_prerelease", "false"
-
- buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
-
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
- kapt {
- includeCompileClasspath = true
- }
- }
-
- buildTypes {
- // release {
- // debuggable false
- // minifyEnabled false
- // shrinkResources false
- // proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- // resValue "bool", "is_prerelease", "false"
- // }
- prerelease {
- applicationIdSuffix ".prerelease"
- buildConfigField("boolean", "BETA", "true")
- signingConfig signingConfigs.prerelease
- versionNameSuffix '-PRE'
- debuggable false
- minifyEnabled false
- shrinkResources false
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- resValue "bool", "is_prerelease", "true"
- }
- debug {
- debuggable true
- applicationIdSuffix ".debug"
- proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
- resValue "bool", "is_prerelease", "true"
- }
- }
- compileOptions {
- coreLibraryDesugaringEnabled true
-
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
- kotlinOptions {
- jvmTarget = '1.8'
- freeCompilerArgs = ['-Xjvm-default=compatibility']
- }
- lintOptions {
- checkReleaseBuilds false
- abortOnError false
- }
-}
-
-repositories {
- maven { url 'https://jitpack.io' }
-}
-
-dependencies {
- implementation 'com.google.android.mediahome:video:1.0.0'
- implementation 'androidx.test.ext:junit-ktx:1.1.3'
- testImplementation 'org.json:json:20180813'
-
- implementation 'androidx.core:core-ktx:1.8.0'
- implementation 'androidx.appcompat:appcompat:1.4.2' // need target 32 for 1.5.0
-
- // dont change this to 1.6.0 it looks ugly af
- implementation 'com.google.android.material:material:1.5.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
- implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
- implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
- testImplementation 'junit:junit:4.13.2'
- androidTestImplementation 'androidx.test.ext:junit:1.1.3'
- androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
-
- //implementation "io.karn:khttp-android:0.1.2" //okhttp instead
-// implementation 'org.jsoup:jsoup:1.13.1'
- implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
-
- implementation "androidx.preference:preference-ktx:1.2.0"
-
- implementation 'com.github.bumptech.glide:glide:4.13.1'
- kapt 'com.github.bumptech.glide:compiler:4.13.1'
- implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
-
- implementation 'jp.wasabeef:glide-transformations:4.3.0'
-
- implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
-
- // implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
-
- // Exoplayer
- implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
- implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
- implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
- implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
-
- //implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
-
- // Bug reports
- implementation "ch.acra:acra-core:5.8.4"
- implementation "ch.acra:acra-toast:5.8.4"
-
- compileOnly "com.google.auto.service:auto-service-annotations:1.0"
- //either for java sources:
- annotationProcessor "com.google.auto.service:auto-service:1.0"
- //or for kotlin sources (requires kapt gradle plugin):
- kapt "com.google.auto.service:auto-service:1.0"
-
- // subtitle color picker
- implementation 'com.jaredrummler:colorpicker:1.1.0'
-
- //run JS
- implementation 'org.mozilla:rhino:1.7.14'
-
- // TorrentStream
- //implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
-
- // Downloading
- implementation "androidx.work:work-runtime:2.7.1"
- implementation "androidx.work:work-runtime-ktx:2.7.1"
-
- // Networking
-// implementation "com.squareup.okhttp3:okhttp:4.9.2"
-// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
- implementation 'com.github.Blatzar:NiceHttp:0.3.2'
-
- // Util to skip the URI file fuckery 🙏
- implementation "com.github.tachiyomiorg:unifile:17bec43"
-
- // API because cba maintaining it myself
- implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
-
- implementation 'com.github.discord:OverlappingPanels:0.1.3'
- // debugImplementation because LeakCanary should only run in debug builds.
- // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
-
- // for shimmer when loading
- implementation 'com.facebook.shimmer:shimmer:0.5.0'
-
- implementation "androidx.tvprovider:tvprovider:1.0.0"
-
- // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
- implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
-
- // slow af yt
- //implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
-
- // newpipe yt
- implementation 'com.github.recloudstream:NewPipeExtractor:0.22.1'
- coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
-
- // Library/extensions searching with Levenshtein distance
- implementation 'me.xdrop:fuzzywuzzy:1.4.0'
-}
-
-task androidSourcesJar(type: Jar) {
- getArchiveClassifier().set('sources')
- from android.sourceSets.main.java.srcDirs//full sources
-}
-
-task makeJar(type: Copy) {
- // after modifying here, you can export. Jar
- from('build/intermediates/compile_app_classes_jar/debug')
- into('build') // output location
- include('classes.jar') // the classes file of the imported rack package
- dependsOn build
-}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 00000000..f8e0091c
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,253 @@
+import com.android.build.gradle.api.BaseVariantOutput
+import org.jetbrains.dokka.gradle.DokkaTask
+import java.io.ByteArrayOutputStream
+import java.net.URL
+
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ id("kotlin-kapt")
+ id("kotlin-android-extensions")
+ id("org.jetbrains.dokka")
+}
+
+val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
+val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
+
+fun String.execute() = ByteArrayOutputStream().use { baot ->
+ if (project.exec {
+ workingDir = projectDir
+ commandLine = this@execute.split(Regex("\\s"))
+ standardOutput = baot
+ }.exitValue == 0)
+ String(baot.toByteArray()).trim()
+ else null
+}
+
+android {
+ testOptions {
+ unitTests.isReturnDefaultValues = true
+ }
+ signingConfigs {
+ create("prerelease") {
+ if (prereleaseStoreFile != null) {
+ storeFile = file(prereleaseStoreFile)
+ storePassword = System.getenv("SIGNING_STORE_PASSWORD")
+ keyAlias = System.getenv("SIGNING_KEY_ALIAS")
+ keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
+ }
+ }
+ }
+
+ compileSdk = 33
+ buildToolsVersion = "30.0.3"
+
+ defaultConfig {
+ applicationId = "com.lagradost.cloudstream3"
+ minSdk = 21
+ targetSdk = 33
+
+ versionCode = 55
+ versionName = "3.4.0"
+
+ resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
+
+ resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
+
+ resValue("bool", "is_prerelease", "false")
+
+ buildConfigField(
+ "String",
+ "BUILDDATE",
+ "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
+ )
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+ kapt {
+ includeCompileClasspath = true
+ }
+ }
+
+ buildTypes {
+ release {
+ isDebuggable = false
+ isMinifyEnabled = false
+ isShrinkResources = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ debug {
+ isDebuggable = true
+ applicationIdSuffix = ".debug"
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+ flavorDimensions.add("state")
+ productFlavors {
+ create("stable") {
+ dimension = "state"
+ resValue("bool", "is_prerelease", "false")
+ }
+ create("prerelease") {
+ dimension = "state"
+ resValue("bool", "is_prerelease", "true")
+ buildConfigField("boolean", "BETA", "true")
+ applicationIdSuffix = ".prerelease"
+ signingConfig = signingConfigs.getByName("prerelease")
+ versionNameSuffix = "-PRE"
+ versionCode = (System.currentTimeMillis() / 60000).toInt()
+ }
+ }
+ compileOptions {
+ isCoreLibraryDesugaringEnabled = true
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ freeCompilerArgs = listOf("-Xjvm-default=compatibility")
+ }
+ lint {
+ abortOnError = false
+ checkReleaseBuilds = false
+ }
+ namespace = "com.lagradost.cloudstream3"
+}
+
+repositories {
+ maven("https://jitpack.io")
+}
+
+dependencies {
+ implementation("com.google.android.mediahome:video:1.0.0")
+ implementation("androidx.test.ext:junit-ktx:1.1.3")
+ testImplementation("org.json:json:20180813")
+
+ implementation("androidx.core:core-ktx:1.8.0")
+ implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
+
+ // dont change this to 1.6.0 it looks ugly af
+ implementation("com.google.android.material:material:1.5.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
+ implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+ //implementation("io.karn:khttp-android:0.1.2") //okhttp instead
+// implementation("org.jsoup:jsoup:1.13.1")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
+
+ implementation("androidx.preference:preference-ktx:1.2.0")
+
+ implementation("com.github.bumptech.glide:glide:4.13.1")
+ kapt("com.github.bumptech.glide:compiler:4.13.1")
+ implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
+
+ implementation("jp.wasabeef:glide-transformations:4.3.0")
+
+ implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
+
+ // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
+
+ // Exoplayer
+ implementation("com.google.android.exoplayer:exoplayer:2.18.2")
+ implementation("com.google.android.exoplayer:extension-cast:2.18.2")
+ implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
+ implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
+
+ //implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
+
+ // Bug reports
+ implementation("ch.acra:acra-core:5.8.4")
+ implementation("ch.acra:acra-toast:5.8.4")
+
+ compileOnly("com.google.auto.service:auto-service-annotations:1.0")
+ //either for java sources:
+ annotationProcessor("com.google.auto.service:auto-service:1.0")
+ //or for kotlin sources (requires kapt gradle plugin):
+ kapt("com.google.auto.service:auto-service:1.0")
+
+ // subtitle color picker
+ implementation("com.jaredrummler:colorpicker:1.1.0")
+
+ //run JS
+ // do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
+ // available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
+ implementation("org.mozilla:rhino:1.7.13")
+
+ // TorrentStream
+ //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
+
+ // Downloading
+ implementation("androidx.work:work-runtime:2.7.1")
+ implementation("androidx.work:work-runtime-ktx:2.7.1")
+
+ // Networking
+// implementation("com.squareup.okhttp3:okhttp:4.9.2")
+// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
+ implementation("com.github.Blatzar:NiceHttp:0.4.1")
+ // To fix SSL fuckery on android 9
+ implementation("org.conscrypt:conscrypt-android:2.2.1")
+ // Util to skip the URI file fuckery 🙏
+ implementation("com.github.tachiyomiorg:unifile:17bec43")
+
+ // API because cba maintaining it myself
+ implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
+
+ implementation("com.github.discord:OverlappingPanels:0.1.3")
+ // debugImplementation because LeakCanary should only run in debug builds.
+ // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
+
+ // for shimmer when loading
+ implementation("com.facebook.shimmer:shimmer:0.5.0")
+
+ implementation("androidx.tvprovider:tvprovider:1.0.0")
+
+ // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
+ implementation("com.github.albfernandez:juniversalchardet:2.4.0")
+
+ // slow af yt
+ //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
+
+ // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
+ implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
+
+ // Library/extensions searching with Levenshtein distance
+ implementation("me.xdrop:fuzzywuzzy:1.4.0")
+}
+
+tasks.register("androidSourcesJar", Jar::class) {
+ archiveClassifier.set("sources")
+ from(android.sourceSets.getByName("main").java.srcDirs) //full sources
+}
+
+// this is used by the gradlew plugin
+tasks.register("makeJar", Copy::class) {
+ from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
+ into("build")
+ include("classes.jar")
+ dependsOn("build")
+}
+
+tasks.withType().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")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 481bb434..ff59496d 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
+# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
index 201ddea3..81753f6b 100644
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
@@ -138,7 +138,7 @@ class ExampleInstrumentedTest {
}
break
}
- if(!validResults) {
+ if (!validResults) {
System.err.println("Api ${api.name} did not load on any")
}
@@ -180,10 +180,12 @@ class ExampleInstrumentedTest {
@Test
fun providerCorrectHomepage() {
runBlocking {
- getAllProviders().apmap { api ->
+ getAllProviders().amap { api ->
if (api.hasMainPage) {
try {
- val homepage = api.getMainPage()
+ val f = api.mainPage.first()
+ val homepage =
+ api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
when {
homepage == null -> {
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
@@ -192,7 +194,7 @@ class ExampleInstrumentedTest {
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!")
+ System.err.println("Homepage provider ${api.name} does not have any items on result!")
}
}
} catch (e: Exception) {
@@ -217,7 +219,7 @@ class ExampleInstrumentedTest {
runBlocking {
val invalidProvider = ArrayList>()
val providers = getAllProviders()
- providers.apmap { api ->
+ providers.amap { api ->
try {
println("Trying $api")
if (testSingleProviderApi(api)) {
@@ -231,7 +233,7 @@ class ExampleInstrumentedTest {
invalidProvider.add(Pair(api, e))
}
}
- if(invalidProvider.isEmpty()) {
+ if (invalidProvider.isEmpty()) {
println("No Invalid providers! :D")
} else {
println("Invalid providers are: ")
diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png
index 3c4e788c..8c374dd9 100644
Binary files a/app/src/debug/ic_launcher-playstore.png and b/app/src/debug/ic_launcher-playstore.png differ
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png
index bf8e595f..c947f526 100644
Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
index bf8e595f..c947f526 100644
Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png
index 935b7108..e841896f 100644
Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
index 935b7108..e841896f 100644
Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_banner.png b/app/src/debug/res/mipmap-xhdpi/ic_banner.png
index 16c4fdd1..6e23cfcf 100644
Binary files a/app/src/debug/res/mipmap-xhdpi/ic_banner.png and b/app/src/debug/res/mipmap-xhdpi/ic_banner.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png
index d62f3f79..c80f9a10 100644
Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
index d62f3f79..c80f9a10 100644
Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
index 38d6ede0..f0b781bb 100644
Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
index 38d6ede0..f0b781bb 100644
Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
index 81c5621b..d5fa9d70 100644
Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
index 81c5621b..d5fa9d70 100644
Binary files a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 460a47ea..871c4f69 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,7 +1,6 @@
+ xmlns:tools="http://schemas.android.com/tools">
@@ -11,7 +10,11 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -138,6 +173,10 @@
android:name=".ui.ControllerActivity"
android:exported="false" />
+
+
Unit)): Thread.UncaughtExceptionHandler {
+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))
+ ps.println(
+ String.format(
+ "Fatal exception on thread %s (%d)",
+ thread.name,
+ thread.id
+ )
+ )
error.printStackTrace(ps)
}
- } catch (ignored: FileNotFoundException) { }
+ } catch (ignored: FileNotFoundException) {
+ }
try {
onError.invoke()
- } catch (ignored: Exception) { }
+ } catch (ignored: Exception) {
+ }
exitProcess(1)
}
@@ -95,7 +106,7 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)): Thread.U
class AcraApplication : Application() {
override fun onCreate() {
super.onCreate()
- Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")){
+ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
})
@@ -183,5 +194,15 @@ class AcraApplication : Application() {
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
context?.openBrowser(url, fallbackWebview, fragment)
}
+
+ /** Will fallback to webview if in TV layout */
+ fun openBrowser(url: String, activity: FragmentActivity?) {
+ openBrowser(
+ url,
+ isTvSettings(),
+ activity?.supportFragmentManager?.fragments?.lastOrNull()
+ )
+ }
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index e56b3bd9..89f0ae51 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -1,5 +1,6 @@
package com.lagradost.cloudstream3
+import android.Manifest
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
@@ -10,16 +11,23 @@ import android.util.Log
import android.view.*
import android.widget.TextView
import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.MainThread
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
+import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
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.ui.player.PlayerEventType
+import com.lagradost.cloudstream3.ui.result.ResultFragment
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.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
@@ -34,6 +42,7 @@ object CommonActivity {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
}
+
var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false
@@ -54,7 +63,9 @@ object CommonActivity {
}
}
- fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
+ /** duration is Toast.LENGTH_SHORT if null*/
+ @MainThread
+ fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
if (act == null) return
showToast(act, act.getString(message), duration)
}
@@ -62,6 +73,7 @@ object CommonActivity {
const val TAG = "COMPACT"
/** duration is Toast.LENGTH_SHORT if null*/
+ @MainThread
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
if (act == null || message == null) {
Log.w(TAG, "invalid showToast act = $act message = $message")
@@ -98,9 +110,18 @@ object CommonActivity {
}
}
+ /**
+ * Not all languages can be fetched from locale with a code.
+ * This map allows sidestepping the default Locale(languageCode)
+ * when setting the app language.
+ **/
+ val appLanguageExceptions = hashMapOf(
+ "zh-rTW" to Locale.TRADITIONAL_CHINESE
+ )
+
fun setLocale(context: Context?, languageCode: String?) {
if (context == null || languageCode == null) return
- val locale = Locale(languageCode)
+ val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
val resources: Resources = context.resources
val config = resources.configuration
Locale.setDefault(locale)
@@ -117,7 +138,7 @@ object CommonActivity {
setLocale(this, localeCode)
}
- fun init(act: Activity?) {
+ fun init(act: ComponentActivity?) {
if (act == null) return
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
@@ -129,6 +150,39 @@ object CommonActivity {
act.updateLocale()
act.updateTv()
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() {
@@ -166,6 +220,8 @@ object CommonActivity {
"Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode
"AmoledLight" -> R.style.AmoledModeLight
+ "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
+ R.style.MonetMode else R.style.AppTheme
else -> R.style.AppTheme
}
@@ -186,6 +242,10 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty
"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
}
act.theme.applyStyle(currentTheme, true)
@@ -283,7 +343,7 @@ object CommonActivity {
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
PlayerEventType.Play
}
- KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7 -> {
+ KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
PlayerEventType.Lock
}
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
@@ -292,22 +352,25 @@ object CommonActivity {
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
PlayerEventType.ToggleMute
}
- KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9 -> {
+ KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
PlayerEventType.ShowMirrors
}
// OpenSubtitles shortcut
- KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
+ KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
- KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
+ KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
PlayerEventType.ShowSpeed
}
- KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
+ KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
PlayerEventType.Resize
}
- KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4 -> {
+ KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp
}
+ KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
+ PlayerEventType.SkipCurrentChapter
+ }
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}
@@ -386,4 +449,4 @@ object CommonActivity {
}
return null
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt
new file mode 100644
index 00000000..045a7963
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/HeaderDecorationBindingAdapter.kt
@@ -0,0 +1,11 @@
+package com.lagradost.cloudstream3
+
+import android.view.LayoutInflater
+import androidx.annotation.LayoutRes
+import androidx.recyclerview.widget.RecyclerView
+import com.lagradost.cloudstream3.ui.HeaderViewDecoration
+
+fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
+ val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
+ view.addItemDecoration(HeaderViewDecoration(headerView))
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index 677bf123..d8c36053 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -18,11 +18,12 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
+import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
+import com.lagradost.cloudstream3.utils.ExtractorLink
import okhttp3.Interceptor
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.absoluteValue
-import kotlin.collections.MutableList
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"
@@ -31,6 +32,12 @@ const val USER_AGENT =
val mapper = JsonMapper.builder().addModule(KotlinModule())
.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 {
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
@@ -39,7 +46,8 @@ object APIHolder {
private const val defProvider = 0
- val allProviders: MutableList = arrayListOf()
+ // ConcurrentModificationException is possible!!!
+ val allProviders = threadSafeListOf()
fun initAll() {
for (api in allProviders) {
@@ -52,7 +60,7 @@ object APIHolder {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
}
- var apis: List = arrayListOf()
+ var apis: List = threadSafeListOf()
var apiMap: Map? = null
fun addPluginMapping(plugin: MainAPI) {
@@ -72,16 +80,20 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null
- initMap()
- return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
- ?: allProviders.firstOrNull { it.name == apiName }
+ synchronized(allProviders) {
+ initMap()
+ return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
+ // Leave the ?. null check, it can crash regardless
+ ?: allProviders.firstOrNull { it?.name == apiName }
+ }
}
fun getApiFromUrlNull(url: String?): MainAPI? {
if (url == null) return null
- for (api in allProviders) {
- if (url.startsWith(api.mainUrl))
- return api
+ synchronized(allProviders) {
+ allProviders.forEach { api ->
+ if (url.startsWith(api.mainUrl)) return api
+ }
}
return null
}
@@ -155,7 +167,9 @@ object APIHolder {
val hashSet = HashSet()
val activeLangs = getApiProviderLangSettings()
- hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name })
+ val hasUniversal = activeLangs.contains(AllLanguagesName)
+ hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
+ .map { it.name })
/*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key),
@@ -191,11 +205,11 @@ object APIHolder {
fun Context.getApiProviderLangSettings(): HashSet {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
- val hashSet = HashSet()
- hashSet.add("en") // def is only en
+ val hashSet = hashSetOf(AllLanguagesName) // def is all languages
+// hashSet.add("en") // def is only en
val list = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key),
- hashSet.toMutableSet()
+ hashSet
)
if (list.isNullOrEmpty()) return hashSet
@@ -225,13 +239,24 @@ object APIHolder {
}
private fun Context.getHasTrailers(): Boolean {
- if (isTvSettings()) return false
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
}
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List {
- val default = enumValues().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
+ // We are getting the weirdest crash ever done:
+ // java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType
+ // Trying fixing using classloader fuckery
+ val oldLoader = Thread.currentThread().contextClassLoader
+ Thread.currentThread().contextClassLoader = TvType::class.java.classLoader
+
+ val default = TvType.values()
+ .sorted()
+ .filter { it != TvType.NSFW }
+ .map { it.ordinal }
+
+ Thread.currentThread().contextClassLoader = oldLoader
+
val defaultSet = default.map { it.toString() }.toSet()
val currentPrefMedia = try {
PreferenceManager.getDefaultSharedPreferences(this)
@@ -241,7 +266,8 @@ object APIHolder {
null
} ?: default
val langs = this.getApiProviderLangSettings()
- val allApis = apis.filter { langs.contains(it.lang) }
+ val hasUniversal = langs.contains(AllLanguagesName)
+ val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
return if (currentPrefMedia.isEmpty()) {
allApis
@@ -322,13 +348,24 @@ data class SettingsJson(
data class MainPageData(
val name: String,
val data: String,
+ val horizontalImages: Boolean = false
)
data class MainPageRequest(
val name: String,
val data: String,
+ val horizontalImages: Boolean,
+ //TODO genre selection or smth
)
+fun mainPage(url: String, name: String, horizontalImages: Boolean = false): MainPageData {
+ return MainPageData(name = name, data = url, horizontalImages = horizontalImages)
+}
+
+fun mainPageOf(vararg elements: MainPageData): List {
+ return elements.toList()
+}
+
/** return list of MainPageData with url to name, make for more readable code */
fun mainPageOf(vararg elements: Pair): List {
return elements.map { (url, name) -> MainPageData(name = name, data = url) }
@@ -337,7 +374,7 @@ fun mainPageOf(vararg elements: Pair): List {
fun newHomePageResponse(
name: String,
list: List,
- hasNext: Boolean? = null
+ hasNext: Boolean? = null,
): HomePageResponse {
return HomePageResponse(
listOf(HomePageList(name, list)),
@@ -345,6 +382,17 @@ fun newHomePageResponse(
)
}
+fun newHomePageResponse(
+ data: MainPageRequest,
+ list: List,
+ hasNext: Boolean? = null,
+): HomePageResponse {
+ return HomePageResponse(
+ listOf(HomePageList(data.name, list, data.horizontalImages)),
+ hasNext = hasNext ?: list.isNotEmpty()
+ )
+}
+
fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse {
return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty())
}
@@ -379,7 +427,19 @@ abstract class MainAPI {
open var storedCredentials: String? = null
open var canBeOverridden: Boolean = true
- //open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id
+ /** if this is turned on then it will request the homepage one after the other,
+ used to delay if they block many request at the same time*/
+ open var sequentialMainPage: Boolean = false
+
+ /** in milliseconds, this can be used to add more delay between homepage requests
+ * on first load if sequentialMainPage is turned on */
+ open var sequentialMainPageDelay: Long = 0L
+
+ /** in milliseconds, this can be used to add more delay between homepage requests when scrolling */
+ open var sequentialMainPageScrollDelay: Long = 0L
+
+ /** used to keep track when last homepage request was in unixtime ms */
+ var lastHomepageRequest: Long = 0L
open var lang = "en" // ISO_639_1 check SubtitleHelper
@@ -425,7 +485,9 @@ abstract class MainAPI {
open val vpnStatus = VPNStatus.None
open val providerType = ProviderType.DirectProvider
- open val mainPage = listOf(MainPageData("", ""))
+
+ //emptyList() //
+ open val mainPage = listOf(MainPageData("", "", false))
@WorkerThread
open suspend fun getMainPage(
@@ -1039,7 +1101,7 @@ interface LoadResponse {
) {
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()
val subs = arrayListOf()
if (!loadExtractor(
@@ -1100,18 +1162,43 @@ interface LoadResponse {
fun getDurationFromString(input: String?): Int? {
val cleanInput = input?.trim()?.replace(" ", "") ?: return null
+ //Use first as sometimes the text passes on the 2 other Regex, but failed to provide accurate return value
+ Regex("(\\d+\\shr)|(\\d+\\shour)|(\\d+\\smin)|(\\d+\\ssec)").findAll(input).let { values ->
+ var seconds = 0
+ values.forEach {
+ val time_text = it.value
+ if (time_text.isNotBlank()) {
+ val time = time_text.filter { s -> s.isDigit() }.trim().toInt()
+ val scale = time_text.filter { s -> !s.isDigit() }.trim()
+ //println("Scale: $scale")
+ val timeval = when (scale) {
+ "hr", "hour" -> time * 60 * 60
+ "min" -> time * 60
+ "sec" -> time
+ else -> 0
+ }
+ seconds += timeval
+ }
+ }
+ if (seconds > 0) {
+ return seconds / 60
+ }
+ }
Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 3) {
val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull()
- return if (minutes != null && hours != null) {
- hours * 60 + minutes
- } else null
+ if (minutes != null && hours != null) {
+ return hours * 60 + minutes
+ }
}
}
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 2) {
- return values[1].toIntOrNull()
+ val return_value = values[1].toIntOrNull()
+ if (return_value != null) {
+ return return_value
+ }
}
}
return null
@@ -1138,6 +1225,11 @@ data class NextAiring(
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(
val season: Int,
val name: String? = null,
@@ -1218,9 +1310,12 @@ data class AnimeLoadResponse(
override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse
+/**
+ * If episodes already exist appends the list.
+ * */
fun AnimeLoadResponse.addEpisodes(status: DubStatus, episodes: List?) {
if (episodes.isNullOrEmpty()) return
- this.episodes[status] = episodes
+ this.episodes[status] = (this.episodes[status] ?: emptyList()) + episodes
}
suspend fun MainAPI.newAnimeLoadResponse(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 9937f183..2b925ff1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -1,20 +1,23 @@
package com.lagradost.cloudstream3
import android.content.ComponentName
+import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.os.Bundle
+import android.util.AttributeSet
import android.util.Log
-import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
-import android.view.WindowManager
+import android.view.*
import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
+import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
@@ -27,6 +30,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.android.gms.cast.framework.*
+import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders
@@ -34,77 +38,167 @@ import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.initAll
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.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
-import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
+import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
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.result.ResultFragment
+import com.lagradost.cloudstream3.ui.home.HomeViewModel
+import com.lagradost.cloudstream3.ui.result.ResultViewModel2
+import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
+import com.lagradost.cloudstream3.ui.result.setImage
+import com.lagradost.cloudstream3.ui.result.setText
+import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.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.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
+import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
+import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.getKey
-import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
-import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
-import com.lagradost.cloudstream3.utils.Event
-import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
+import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
+import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
-import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
-import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
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.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.net.URI
+import java.net.URLDecoder
import java.nio.charset.Charset
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_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result"
-val VLC_COMPONENT: ComponentName =
- ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity")
-const val VLC_REQUEST_CODE = 42
+const val MPV_PACKAGE = "is.xyz.mpv"
+const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
-const val VLC_FROM_START = -1
-const val VLC_FROM_PROGRESS = -2
-const val VLC_EXTRA_POSITION_OUT = "extra_position"
-const val VLC_EXTRA_DURATION_OUT = "extra_duration"
-const val VLC_LAST_ID_KEY = "vlc_last_open_id"
+val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
+val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
+
+//TODO REFACTOR AF
+open class ResultResume(
+ val packageString: String,
+ val action: String = Intent.ACTION_VIEW,
+ val position: String? = null,
+ val duration: String? = null,
+ var launcher: ActivityResultLauncher? = null,
+) {
+ val defaultTime = -1L
+
+ val lastId get() = "${packageString}_last_open_id"
+ suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
+ val intent = Intent(action)
+
+ if (id != null)
+ setKey(lastId, id)
+ else
+ removeKey(lastId)
+
+ intent.setPackage(packageString)
+ callback.invoke(intent)
+ launcher?.launch(intent)
+ }
+
+ open fun getPosition(intent: Intent?): Long {
+ return defaultTime
+ }
+
+ open fun getDuration(intent: Intent?): Long {
+ return defaultTime
+ }
+}
+
+val VLC = object : ResultResume(
+ VLC_PACKAGE,
+ "org.videolan.vlc.player.result",
+ "extra_position",
+ "extra_duration",
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
+ }
+}
+
+val MPV = object : ResultResume(
+ MPV_PACKAGE,
+ //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
+ position = "position",
+ duration = "duration",
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+}
+
+val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
+
+val resumeApps = arrayOf(
+ VLC, MPV, WEB_VIDEO
+)
// Short name for requests client to make it nicer to use
@@ -137,13 +231,130 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
const val TAG = "MAINACT"
+ /**
+ * Setting this will automatically enter the query in the search
+ * next time the search fragment is opened.
+ * This variable will clear itself after one use. Null does nothing.
+ *
+ * This is a very bad solution but I was unable to find a better one.
+ **/
+ private var nextSearchQuery: String? = null
+
/**
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
+ * Boolean signifies if stuff should be force reloaded (true if force reload, false if reload when necessary).
+ *
+ * The force reloading are used for plugin development to instantly reload the page on deployWithAdb
* */
val afterPluginsLoadedEvent = Event()
val mainPluginsLoadedEvent =
Event() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event()
+
+ // kinda shitty solution, but cant com main->home otherwise for popups
+ val bookmarksUpdatedEvent = Event()
+
+
+ /**
+ * @return true if the str has launched an app task (be it successful or not)
+ * @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
+ * */
+ fun handleAppIntentUrl(
+ activity: FragmentActivity?,
+ str: String?,
+ isWebview: Boolean
+ ): Boolean =
+ with(activity) {
+ // Invalid URIs can crash
+ fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
+
+ if (str != null && this != null) {
+ if (str.startsWith("https://cs.repo")) {
+ val realUrl = "https://" + str.substringAfter("?")
+ println("Repository url: $realUrl")
+ loadRepository(realUrl)
+ return true
+ } else if (str.contains(appString)) {
+ for (api in OAuth2Apis) {
+ if (str.contains("/${api.redirectUrl}")) {
+ ioSafe {
+ Log.i(TAG, "handleAppIntent $str")
+ val isSuccessful = api.handleRedirect(str)
+
+ if (isSuccessful) {
+ Log.i(TAG, "authenticated ${api.name}")
+ } else {
+ Log.i(TAG, "failed to authenticate ${api.name}")
+ }
+
+ this@with.runOnUiThread {
+ try {
+ showToast(
+ this@with,
+ getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
+ api.name
+ )
+ )
+ } catch (e: Exception) {
+ logError(e) // format might fail
+ }
+ }
+ }
+ return true
+ }
+ }
+ // This specific intent is used for the gradle deployWithAdb
+ // https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
+ if (str == "$appString:") {
+ PluginManager.hotReloadAllLocalPlugins(activity)
+ }
+ } else if (safeURI(str)?.scheme == appStringRepo) {
+ val url = str.replaceFirst(appStringRepo, "https")
+ loadRepository(url)
+ return true
+ } else if (safeURI(str)?.scheme == appStringSearch) {
+ nextSearchQuery =
+ URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
+ nav_view.selectedItemId = R.id.navigation_search
+ } else if (safeURI(str)?.scheme == appStringResumeWatching) {
+ val id =
+ str.substringAfter("$appStringResumeWatching://").toIntOrNull()
+ ?: return false
+ ioSafe {
+ val resumeWatchingCard =
+ HomeViewModel.getResumeWatching()?.firstOrNull { it.id == id }
+ ?: return@ioSafe
+ activity.loadSearchResult(
+ resumeWatchingCard,
+ START_ACTION_RESUME_LATEST
+ )
+ }
+ } else if (!isWebview) {
+ if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
+ this.navigate(R.id.navigation_downloads)
+ return true
+ } else {
+ for (api in apis) {
+ if (str.startsWith(api.mainUrl)) {
+ loadResult(str, api.name)
+ return true
+ }
+ }
+ }
+ }
+ }
+ return false
+ }
+ }
+
+ var lastPopup : SearchResponse? = null
+ fun loadPopup(result: SearchResponse) {
+ lastPopup = result
+ viewModel.load(
+ this, result.url, result.apiName, false, if (getApiDubstatusSettings()
+ .contains(DubStatus.Dubbed)
+ ) DubStatus.Dubbed else DubStatus.Subbed, null
+ )
}
override fun onColorSelected(dialogId: Int, color: Int) {
@@ -193,6 +404,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_settings_plugins,
).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) {
Configuration.ORIENTATION_LANDSCAPE -> {
true
@@ -259,6 +491,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onPause() {
super.onPause()
+
+ // Start any delayed updates
+ if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
+ Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
+ }
try {
if (isCastApiAvailable()) {
mSessionManager.removeSessionManagerListener(mSessionManagerListener)
@@ -289,12 +526,34 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
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() {
this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale()
- super.onBackPressed()
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() {
@@ -306,31 +565,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (requestCode == VLC_REQUEST_CODE) {
- if (resultCode == RESULT_OK && data != null) {
- val pos: Long =
- data.getLongExtra(
- VLC_EXTRA_POSITION_OUT,
- -1
- ) //Last position in media when player exited
- val dur: Long =
- data.getLongExtra(
- VLC_EXTRA_DURATION_OUT,
- -1
- ) //Last position in media when player exited
- val id = getKey(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() {
val broadcastIntent = Intent()
broadcastIntent.action = "restart_service"
@@ -349,56 +583,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (intent == null) return
val str = intent.dataString
loadCache()
- 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
- }
- }
- }
- }
- }
+ handleAppIntentUrl(this, str, false)
}
private fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
@@ -446,7 +631,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
// it.hashCode() is not enough to make sure they are distinct
- apis = allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
+ apis =
+ allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null
} catch (e: Exception) {
logError(e)
@@ -455,6 +641,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
+ lateinit var viewModel: ResultViewModel2
+
+ override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
+ viewModel =
+ ViewModelProvider(this)[ResultViewModel2::class.java]
+
+ return super.onCreateView(name, context, attrs)
+ }
+
+ private fun hidePreviewPopupDialog() {
+ viewModel.clear()
+ bottomPreviewPopup.dismissSafe(this)
+ }
+
+ var bottomPreviewPopup: BottomSheetDialog? = null
+ private fun showPreviewPopupDialog(): BottomSheetDialog {
+ val ret = (bottomPreviewPopup ?: run {
+ val builder =
+ BottomSheetDialog(this)
+ builder.setContentView(R.layout.bottom_resultview_preview)
+ builder.setOnDismissListener {
+ bottomPreviewPopup = null
+ viewModel.clear()
+ }
+ builder.setCanceledOnTouchOutside(true)
+ builder.show()
+ builder
+ })
+ bottomPreviewPopup = ret
+ return ret
+ }
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
@@ -466,9 +683,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
lastError = errorFile.readText(Charset.defaultCharset())
errorFile.delete()
}
-
+
val settingsForProvider = SettingsJson()
- settingsForProvider.enableAdult = settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
+ settingsForProvider.enableAdult =
+ settingsManager.getBoolean(getString(R.string.enable_nsfw_on_providers_key), false)
MainAPI.settingsForProvider = settingsForProvider
@@ -484,7 +702,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
-
+ updateTv()
if (isTvSettings()) {
setContentView(R.layout.activity_main_tv)
} else {
@@ -502,15 +720,28 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
ioSafe {
- if (settingsManager.getBoolean(getString(R.string.auto_update_plugins_key), true)) {
+ if (settingsManager.getBoolean(
+ getString(R.string.auto_update_plugins_key),
+ true
+ )
+ ) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else {
- PluginManager.loadAllOnlinePlugins(this@MainActivity)
+ loadAllOnlinePlugins(this@MainActivity)
+ }
+
+ //Automatically download not existing plugins
+ if (settingsManager.getBoolean(
+ getString(R.string.auto_download_plugins_key),
+ false
+ )
+ ) {
+ PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
}
}
ioSafe {
- PluginManager.loadAllLocalPlugins(this@MainActivity)
+ PluginManager.loadAllLocalPlugins(this@MainActivity, false)
}
}
} else {
@@ -527,9 +758,81 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setNegativeButton("Ok") { _, _ -> }
}
- builder.show()
+ builder.show().setDefaultFocus()
}
+ observeNullable(viewModel.page) { resource ->
+ if (resource == null) {
+ bottomPreviewPopup.dismissSafe(this)
+ return@observeNullable
+ }
+ when (resource) {
+ is Resource.Failure -> {
+ showToast(this, R.string.error)
+ hidePreviewPopupDialog()
+ }
+ is Resource.Loading -> {
+ showPreviewPopupDialog().apply {
+ resultview_preview_loading?.isVisible = true
+ resultview_preview_result?.isVisible = false
+ resultview_preview_loading_shimmer?.startShimmer()
+ }
+ }
+ is Resource.Success -> {
+ val d = resource.value
+ showPreviewPopupDialog().apply {
+ resultview_preview_loading?.isVisible = false
+ resultview_preview_result?.isVisible = true
+ resultview_preview_loading_shimmer?.stopShimmer()
+
+ resultview_preview_title?.text = d.title
+
+ resultview_preview_meta_type.setText(d.typeText)
+ resultview_preview_meta_year.setText(d.yearText)
+ resultview_preview_meta_duration.setText(d.durationText)
+ resultview_preview_meta_rating.setText(d.ratingText)
+
+ resultview_preview_description?.setText(d.plotText)
+ resultview_preview_poster?.setImage(
+ d.posterImage ?: d.posterBackgroundImage
+ )
+
+ resultview_preview_poster?.setOnClickListener {
+ //viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
+ val value = viewModel.watchStatus.value ?: WatchType.NONE
+
+ this@MainActivity.showBottomDialog(
+ WatchType.values().map { getString(it.stringRes) }.toList(),
+ value.ordinal,
+ this@MainActivity.getString(R.string.action_add_to_bookmarks),
+ showApply = false,
+ {}) {
+ viewModel.updateWatchStatus(WatchType.values()[it])
+ bookmarksUpdatedEvent(true)
+ }
+ }
+
+ if (!isTvSettings()) // dont want this clickable on tv layout
+ resultview_preview_description?.setOnClickListener { view ->
+ view.context?.let { ctx ->
+ val builder: AlertDialog.Builder =
+ AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
+ builder.setMessage(d.plotText.asString(ctx).html())
+ .setTitle(d.plotHeaderText.asString(ctx))
+ .show()
+ }
+ }
+
+ resultview_preview_more_info?.setOnClickListener {
+ hidePreviewPopupDialog()
+ lastPopup?.let {
+ loadSearchResult(it)
+ }
+ }
+ }
+ }
+ }
+ }
// ioSafe {
// val plugins =
@@ -546,10 +849,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
for (api in accountManagers) {
api.init()
}
- }
- ioSafe {
- inAppAuths.apmap { api ->
+ inAppAuths.amap { api ->
try {
api.initialize()
} catch (e: Exception) {
@@ -573,6 +874,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
+
+ navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
+ // Intercept search and add a query
+ 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)
/*navOptions = NavOptions.Builder()
@@ -586,7 +898,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.setupWithNavController(navController)
val nav_rail = findViewById(R.id.nav_rail_view)
nav_rail?.setupWithNavController(navController)
+ if (isTvSettings()) {
+ nav_rail?.background?.alpha = 200
+ } else {
+ nav_rail?.background?.alpha = 255
+ }
nav_rail?.setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
@@ -755,8 +1072,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Used to check current focus for TV
// main {
// while (true) {
-// delay(1000)
+// delay(5000)
// println("Current focus: $currentFocus")
+// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
// }
// }
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
index badb6631..46955427 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
@@ -1,8 +1,7 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.mvvm.logError
-import kotlinx.coroutines.async
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.*
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
/*
@@ -26,10 +25,25 @@ fun Iterable.pmap(
return ArrayList(destination)
}*/
+
+@OptIn(DelicateCoroutinesApi::class)
+suspend fun Map.amap(f: suspend (Map.Entry) -> R): List =
+ with(CoroutineScope(GlobalScope.coroutineContext)) {
+ map { async { f(it) } }.map { it.await() }
+ }
+
fun Map.apmap(f: suspend (Map.Entry) -> R): List = runBlocking {
map { async { f(it) } }.map { it.await() }
}
+
+@OptIn(DelicateCoroutinesApi::class)
+suspend fun List.amap(f: suspend (A) -> B): List =
+ with(CoroutineScope(GlobalScope.coroutineContext)) {
+ map { async { f(it) } }.map { it.await() }
+ }
+
+
fun List.apmap(f: suspend (A) -> B): List = runBlocking {
map { async { f(it) } }.map { it.await() }
}
@@ -38,6 +52,12 @@ fun List.apmapIndexed(f: suspend (index: Int, A) -> B): List = runB
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
}
+@OptIn(DelicateCoroutinesApi::class)
+suspend fun List.amapIndexed(f: suspend (index: Int, A) -> B): List =
+ with(CoroutineScope(GlobalScope.coroutineContext)) {
+ mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
+ }
+
// run code in parallel
/*fun argpmap(
vararg transforms: () -> R,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt
new file mode 100644
index 00000000..b0051ba7
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt
@@ -0,0 +1,40 @@
+package com.lagradost.cloudstream3.extractors
+
+import android.util.Log
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+
+open class AStreamHub : ExtractorApi() {
+ override val name = "AStreamHub"
+ override val mainUrl = "https://astreamhub.com"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val sources = mutableListOf()
+ 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
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
index fe46791b..c782b29d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
@@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.*
-class Acefile : ExtractorApi() {
+open class Acefile : ExtractorApi() {
override val name = "Acefile"
override val mainUrl = "https://acefile.co"
override val requiresReferer = false
@@ -27,7 +27,6 @@ class Acefile : ExtractorApi() {
res.substringAfter("\"file\":\"").substringBefore("\","),
"$mainUrl/",
Qualities.Unknown.value,
- headers = mapOf("range" to "bytes=0-")
)
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
index cf16f200..7a62fb52 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
-class AsianLoad : ExtractorApi() {
+open class AsianLoad : ExtractorApi() {
override var name = "AsianLoad"
override var mainUrl = "https://asianembed.io"
override val requiresReferer = true
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
index cae77322..44e700b1 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
@@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
-class Blogger : ExtractorApi() {
+open class Blogger : ExtractorApi() {
override val name = "Blogger"
override val mainUrl = "https://www.blogger.com"
override val requiresReferer = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
index d4f87f4c..71fa7066 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
@@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-class BullStream : ExtractorApi() {
+open class BullStream : ExtractorApi() {
override val name = "BullStream"
override val mainUrl = "https://bullstream.xyz"
override val requiresReferer = false
@@ -18,7 +18,7 @@ class BullStream : ExtractorApi() {
?: return null
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
- println("shiv : $m3u8")
+ //println("shiv : $m3u8")
return M3u8Helper.generateM3u8(
name,
m3u8,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt
new file mode 100644
index 00000000..6a2f399d
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt
@@ -0,0 +1,97 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import android.util.Log
+import java.net.URLDecoder
+
+open class Cda: ExtractorApi() {
+ override var mainUrl = "https://ebd.cda.pl"
+ override var name = "Cda"
+ override val requiresReferer = false
+
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ 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(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 = mapOf(),
+ val quality: String?,
+ val ts: Int?,
+ val hash2: String?
+ )
+
+ data class PlayerData(
+ val video: VideoPlayerData
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
new file mode 100644
index 00000000..125e4bcf
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
@@ -0,0 +1,102 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import java.net.URL
+
+open class Dailymotion : ExtractorApi() {
+ override val mainUrl = "https://www.dailymotion.com"
+ override val name = "Dailymotion"
+ override val requiresReferer = false
+
+ @Suppress("RegExpSimplifiable")
+ private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
+
+ // https://www.dailymotion.com/video/k3JAHfletwk94ayCVIu
+ // https://www.dailymotion.com/embed/video/k3JAHfletwk94ayCVIu
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val embedUrl = getEmbedUrl(url) ?: return
+ val doc = app.get(embedUrl).document
+ val prefix = "window.__PLAYER_CONFIG__ = "
+ val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
+ val config = tryParseJson(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() ?: return
+ metaData.qualities.forEach { (key, video) ->
+ video.forEach {
+ callback.invoke(
+ ExtractorLink(
+ name,
+ "$name $key",
+ it.url,
+ "",
+ Qualities.Unknown.value,
+ true
+ )
+ )
+ }
+ }
+ }
+
+ private fun getEmbedUrl(url: String): String? {
+ if (url.contains("/embed/")) {
+ return url
+ }
+ val vid = getVideoId(url) ?: return null
+ return "$mainUrl/embed/video/$vid"
+ }
+
+ private fun getVideoId(url: String): String? {
+ val path = URL(url).path
+ val id = path.substringAfter("video/")
+ if (id.matches(videoIdRegex)) {
+ return id
+ }
+ return null
+ }
+
+ data class Config(
+ val context: Context,
+ val dmInternalData: InternalData
+ )
+
+ data class InternalData(
+ val ts: Int,
+ val v1st: String
+ )
+
+ data class Context(
+ @JsonProperty("access_token") val accessToken: String?,
+ val dmvk: String,
+ )
+
+ data class MetaData(
+ val qualities: Map>
+ )
+
+ data class VideoLink(
+ val type: String,
+ val url: String
+ )
+
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
index c5eaf40e..7ec1fb22 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
@@ -7,6 +7,10 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay
+class DoodWfExtractor : DoodLaExtractor() {
+ override var mainUrl = "https://dood.wf"
+}
+
class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx"
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt
new file mode 100644
index 00000000..45a06dcc
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt
@@ -0,0 +1,37 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.getQualityFromName
+import com.lagradost.cloudstream3.utils.httpsify
+
+open class Embedgram : ExtractorApi() {
+ override val name = "Embedgram"
+ override val mainUrl = "https://embedgram.com"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val document = app.get(url, referer = referer).document
+ val link = document.select("video source:last-child").attr("src")
+ val quality = document.select("video source:last-child").attr("title")
+ callback.invoke(
+ ExtractorLink(
+ this.name,
+ this.name,
+ httpsify(link),
+ "$mainUrl/",
+ getQualityFromName(quality),
+ headers = mapOf(
+ "Range" to "bytes=0-"
+ )
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
index 4a9f2f52..eddbf6df 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
@@ -26,7 +26,7 @@ open class Evoload : ExtractorApi() {
} else {
""
}
-
+
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
index 16b109be..e8f8c49a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
@@ -1,39 +1,54 @@
package com.lagradost.cloudstream3.extractors
-import com.lagradost.cloudstream3.apmap
+import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
+import com.lagradost.cloudstream3.utils.getAndUnpack
+import org.jsoup.nodes.Document
-class Fastream: ExtractorApi() {
+open class Fastream: ExtractorApi() {
override var mainUrl = "https://fastream.to"
override var name = "Fastream"
override val requiresReferer = false
-
-
- override suspend fun getUrl(url: String, referer: String?): List? {
- val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
- val sources = mutableListOf()
- val response = app.post("$mainUrl/dl",
- data = mapOf(
- Pair("op","embed"),
- Pair("file_code",id),
- Pair("auto","1")
- )).document
- response.select("script").apmap { script ->
- if (script.data().contains("sources")) {
- val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
- val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
+ suspend fun getstream(
+ response: Document,
+ sources: ArrayList): Boolean{
+ response.select("script").amap { script ->
+ if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
+ val unpacked = getAndUnpack(script.data())
+ //val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
+ val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"")
+ //val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach
generateM3u8(
name,
- m3u8,
+ newm3u8link,
mainUrl
).forEach { link ->
sources.add(link)
}
}
}
+ return true
+ }
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val sources = ArrayList()
+ 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
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
index 5c8af1c5..8e3dc730 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
@@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
-class Filesim : ExtractorApi() {
+open class Filesim : ExtractorApi() {
override val name = "Filesim"
override val mainUrl = "https://files.im"
override val requiresReferer = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
index e36a03d3..52c45096 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
@@ -3,9 +3,9 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.M3u8Helper
+import com.lagradost.cloudstream3.utils.Qualities
-class GMPlayer : ExtractorApi() {
+open class GMPlayer : ExtractorApi() {
override val name = "GM Player"
override val mainUrl = "https://gmplayer.xyz"
override val requiresReferer = true
@@ -25,11 +25,16 @@ class GMPlayer : ExtractorApi() {
data = mapOf("hash" to id, "r" to ref)
).parsed().videoSource ?: return null
- return M3u8Helper.generateM3u8(
- name,
- m3u8,
- ref,
- headers = mapOf("accept" to "*/*")
+ return listOf(
+ ExtractorLink(
+ this.name,
+ this.name,
+ m3u8,
+ ref,
+ Qualities.Unknown.value,
+ headers = mapOf("accept" to "*/*"),
+ isM3u8 = true
+ )
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
new file mode 100644
index 00000000..df9c74a4
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
@@ -0,0 +1,209 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import org.jsoup.nodes.Element
+import java.security.DigestException
+import java.security.MessageDigest
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+class DatabaseGdrive2 : Gdriveplayer() {
+ override var mainUrl = "https://databasegdriveplayer.co"
+}
+
+class DatabaseGdrive : Gdriveplayer() {
+ override var mainUrl = "https://series.databasegdriveplayer.co"
+}
+
+class Gdriveplayerapi : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayerapi.com"
+}
+
+class Gdriveplayerapp : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.app"
+}
+
+class Gdriveplayerfun : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.fun"
+}
+
+class Gdriveplayerio : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.io"
+}
+
+class Gdriveplayerme : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.me"
+}
+
+class Gdriveplayerbiz : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.biz"
+}
+
+class Gdriveplayerorg : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.org"
+}
+
+class Gdriveplayerus : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.us"
+}
+
+class Gdriveplayerco : Gdriveplayer() {
+ override val mainUrl: String = "https://gdriveplayer.co"
+}
+
+open class Gdriveplayer : ExtractorApi() {
+ override val name = "Gdrive"
+ override val mainUrl = "https://gdriveplayer.to"
+ override val requiresReferer = false
+
+ private fun unpackJs(script: Element): String? {
+ return script.select("script").find { it.data().contains("eval(function(p,a,c,k,e,d)") }
+ ?.data()?.let { getAndUnpack(it) }
+ }
+
+ private fun String.decodeHex(): ByteArray {
+ check(length % 2 == 0) { "Must have an even length" }
+ return chunked(2)
+ .map { it.toInt(16).toByte() }
+ .toByteArray()
+ }
+
+ // https://stackoverflow.com/a/41434590/8166854
+ private fun GenerateKeyAndIv(
+ password: ByteArray,
+ salt: ByteArray,
+ hashAlgorithm: String = "MD5",
+ keyLength: Int = 32,
+ ivLength: Int = 16,
+ iterations: Int = 1
+ ): List? {
+
+ 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(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>("[$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
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
index 57435161..f25cb5ba 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
@@ -1,36 +1,83 @@
package com.lagradost.cloudstream3.extractors
+
import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
+class CineGrabber : GuardareStream() {
+ override var name = "CineGrabber"
+ override var mainUrl = "https://cinegrabber.com"
+}
+
open class GuardareStream : ExtractorApi() {
override var name = "Guardare"
override var mainUrl = "https://guardare.stream"
override val requiresReferer = false
- data class GuardareJsonData (
- @JsonProperty("data") val data : List,
+ data class GuardareJsonData(
+ @JsonProperty("data") val data: List,
+ @JsonProperty("captions") val captions: List?,
)
- data class GuardareData (
- @JsonProperty("file") val file : String,
- @JsonProperty("label") val label : String,
- @JsonProperty("type") val type : String
+ data class GuardareData(
+ @JsonProperty("file") val file: String,
+ @JsonProperty("label") val label: String,
+ @JsonProperty("type") val type: String
)
- override suspend fun getUrl(url: String, referer: String?): List? {
- val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text
- val jsonvideodata = AppUtils.parseJson(response)
- return jsonvideodata.data.map {
- ExtractorLink(
- it.file+".${it.type}",
- this.name,
- it.file+".${it.type}",
- mainUrl,
- it.label.filter{ it.isDigit() }.toInt(),
- false
+
+ // https://cinegrabber.com/asset/userdata/224879/caption/gqdmzh-71ez76z8/876438.srt
+ data class GuardareCaptions(
+ @JsonProperty("id") val id: String,
+ @JsonProperty("hash") val hash: String,
+ @JsonProperty("language") val language: String?,
+ @JsonProperty("extension") val extension: String
+ ) {
+ fun getUrl(mainUrl: String, userId: String): String {
+ return "$mainUrl/asset/userdata/$userId/caption/$hash/$id.$extension"
+ }
+ }
+
+ 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(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
+ )
+ )
+ }
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt
new file mode 100644
index 00000000..11b66d99
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt
@@ -0,0 +1,72 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+
+open class Jeniusplay : ExtractorApi() {
+ override val name = "Jeniusplay"
+ override val mainUrl = "https://jeniusplay.com"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val document = app.get(url, referer = "$mainUrl/").document
+ val hash = url.split("/").last().substringAfter("data=")
+
+ val m3uLink = app.post(
+ url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
+ data = mapOf("hash" to hash, "r" to "$referer"),
+ referer = url,
+ headers = mapOf("X-Requested-With" to "XMLHttpRequest")
+ ).parsed().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>("[$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?,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
index 52fc5532..c28a8900 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
@@ -1,22 +1,26 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
-class Linkbox : ExtractorApi() {
+open class Linkbox : ExtractorApi() {
override val name = "Linkbox"
override val mainUrl = "https://www.linkbox.to"
override val requiresReferer = true
- override suspend fun getUrl(url: String, referer: String?): List {
- val id = url.substringAfter("id=")
- val sources = mutableListOf()
-
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2)
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe()?.data?.rList?.map { link ->
- sources.add(
+ callback.invoke(
ExtractorLink(
name,
name,
@@ -26,8 +30,6 @@ class Linkbox : ExtractorApi() {
)
)
}
-
- return sources
}
data class RList(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mcloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mcloud.kt
deleted file mode 100644
index 29d98557..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mcloud.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package com.lagradost.cloudstream3.extractors
-
-open class Mcloud : WcoStream() {
- override var name = "Mcloud"
- override var mainUrl = "https://mcloud.to"
- override val requiresReferer = true
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
new file mode 100644
index 00000000..aaa33ca1
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
@@ -0,0 +1,44 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper
+
+class MoviehabNet : Moviehab() {
+ override var mainUrl = "https://play.moviehab.net"
+}
+
+open class Moviehab : ExtractorApi() {
+ override var name = "Moviehab"
+ override var mainUrl = "https://play.moviehab.com"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val res = app.get(url)
+ res.document.select("video#player").let {
+ //should redirect first for making it works
+ val link = app.get("$mainUrl/${it.select("source").attr("src")}", referer = url).url
+ M3u8Helper.generateM3u8(
+ this.name,
+ link,
+ url
+ ).forEach(callback)
+
+ Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub ->
+ subtitleCallback.invoke(
+ SubtitleFile(
+ it.select("track").attr("label"),
+ "$mainUrl/$sub"
+ )
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
index 68a4a103..93a280ed 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
@@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack
-class Mp4Upload : ExtractorApi() {
+open class Mp4Upload : ExtractorApi() {
override var name = "Mp4Upload"
override var mainUrl = "https://www.mp4upload.com"
private val srcRegex = Regex("""player\.src\("(.*?)"""")
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
index 0c0b5c68..44657196 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
-class MultiQuality : ExtractorApi() {
+open class MultiQuality : ExtractorApi() {
override var name = "MultiQuality"
override var mainUrl = "https://gogo-play.net"
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt
new file mode 100644
index 00000000..9e5f5e74
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt
@@ -0,0 +1,47 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+
+open class Mvidoo : ExtractorApi() {
+ override val name = "Mvidoo"
+ override val mainUrl = "https://mvidoo.com"
+ override val requiresReferer = true
+
+ private fun String.decodeHex(): String {
+ require(length % 2 == 0) { "Must have an even length" }
+ return String(
+ chunked(2)
+ .map { it.toInt(16).toByte() }
+ .toByteArray()
+ )
+ }
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val document = app.get(url, referer = referer).text
+ val data = Regex("""\{var\s*[^\s]+\s*=\s*(\[[^]]+])""").find(document)?.groupValues?.get(1)
+ ?.removeSurrounding("[", "]")?.replace("\"", "")?.replace("\\x", "")?.split(",")?.map { it.decodeHex() }?.reversed()?.joinToString("") ?: return
+ Regex("source\\s*src=\"([^\"]+)").find(data)?.groupValues?.get(1)?.let { link ->
+ callback.invoke(
+ ExtractorLink(
+ this.name,
+ this.name,
+ link,
+ "$mainUrl/",
+ Qualities.Unknown.value,
+ headers = mapOf(
+ "Range" to "bytes=0-"
+ )
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt
new file mode 100644
index 00000000..37bb09e3
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt
@@ -0,0 +1,39 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+
+data class Okrulinkdata (
+ @JsonProperty("status" ) var status : String? = null,
+ @JsonProperty("url" ) var url : String? = null
+)
+
+open class Okrulink: ExtractorApi() {
+ override var mainUrl = "https://okru.link"
+ override var name = "Okrulink"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val sources = mutableListOf()
+ val key = url.substringAfter("html?t=")
+ val request = app.post("https://apizz.okru.link/decoding", allowRedirects = false,
+ data = mapOf("video" to key)
+ ).parsedSafe()
+ if (request?.url != null) {
+ sources.add(
+ ExtractorLink(
+ name,
+ name,
+ request.url!!,
+ "",
+ Qualities.Unknown.value,
+ isM3u8 = false
+ )
+ )
+ }
+ return sources
+ }
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
index cc743d5e..45ec4c2f 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
@@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
-import com.lagradost.cloudstream3.apmap
+import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
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
* If they diverge it'd be better to make them separate.
* */
-class Pelisplus(val mainUrl: String) {
+open class Pelisplus(val mainUrl: String) {
val name: String = "Vidstream"
private fun getExtractorUrl(id: String): String {
@@ -35,7 +35,7 @@ class Pelisplus(val mainUrl: String) {
callback: (ExtractorLink) -> Unit
): Boolean {
try {
- normalApis.apmap { api ->
+ normalApis.amap { api ->
val url = api.getExtractorUrl(id)
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
}
@@ -51,8 +51,8 @@ class Pelisplus(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P")
//a[download]
- pageDoc.select(".dowload > a")?.apmap { element ->
- val href = element.attr("href") ?: return@apmap
+ pageDoc.select(".dowload > a")?.amap { element ->
+ val href = element.attr("href") ?: return@amap
val qual = if (element.text()
.contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
@@ -84,7 +84,7 @@ class Pelisplus(val mainUrl: String) {
//val name = element.text()
// Matches vidstream links with extractors
- extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api ->
+ extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
new file mode 100644
index 00000000..2b286abb
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
@@ -0,0 +1,79 @@
+package com.lagradost.cloudstream3.extractors
+
+import android.util.Log
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+
+open class PlayLtXyz: ExtractorApi() {
+ override val name: String = "PlayLt"
+ override val mainUrl: String = "https://play.playlt.xyz"
+ override val requiresReferer = true
+
+ private data class ResponseData(
+ @JsonProperty("data") val data: String? = null
+ )
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val extractedLinksList = mutableListOf()
+ //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(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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
index 849f5fc8..cc34781c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
@@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
-class Solidfiles : ExtractorApi() {
+open class Solidfiles : ExtractorApi() {
override val name = "Solidfiles"
override val mainUrl = "https://www.solidfiles.com"
override val requiresReferer = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
index 6153a7c1..8ef6c463 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
@@ -7,7 +7,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-class SpeedoStream : ExtractorApi() {
+class SpeedoStream1 : SpeedoStream() {
+ override val mainUrl = "https://speedostream.nl"
+}
+
+open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.com"
override val requiresReferer = true
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
index a933c484..958d63fb 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
@@ -1,12 +1,26 @@
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.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
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"
}
@@ -84,15 +98,15 @@ open class StreamSB : ExtractorApi() {
}
data class Subs (
- @JsonProperty("file") val file: String,
- @JsonProperty("label") val label: String,
+ @JsonProperty("file") val file: String? = null,
+ @JsonProperty("label") val label: String? = null,
)
data class StreamData (
@JsonProperty("file") val file: String,
@JsonProperty("cdn_img") val cdnImg: String,
@JsonProperty("hash") val hash: String,
- @JsonProperty("subs") val subs: List?,
+ @JsonProperty("subs") val subs: ArrayList? = arrayListOf(),
@JsonProperty("length") val length: String,
@JsonProperty("id") val id: String,
@JsonProperty("title") val title: String,
@@ -104,31 +118,42 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int,
)
- override suspend fun getUrl(url: String, referer: String?): List? {
- val regexID = Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|\\/e\\/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val regexID =
+ Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
val id = regexID.findAll(url).map {
- it.value.replace(Regex("(embed-|\\/e\\/)"),"")
+ it.value.replace(Regex("(embed-|/e/)"), "")
}.first()
- val bytes = id.toByteArray()
- val bytesToHex = bytesToHex(bytes)
- val master = "$mainUrl/sources43/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
+// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
+ val master = "$mainUrl/sources50/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val headers = mapOf(
- "watchsb" to "streamsb",
- )
- val urltext = app.get(master,
+ "watchsb" to "sbstream",
+ )
+ val mapped = app.get(
+ master.lowercase(),
headers = headers,
- allowRedirects = false
- ).text
- val mapped = urltext.let { parseJson(it) }
- val testurl = app.get(mapped.streamData.file, headers = headers).text
+ referer = url,
+ ).parsedSafe()
// val urlmain = mapped.streamData.file.substringBefore("/hls/")
- if (urltext.contains("m3u8") && testurl.contains("EXTM3U"))
- return M3u8Helper.generateM3u8(
- name,
- mapped.streamData.file,
- url,
- headers = headers
+ 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,
+ )
)
- return null
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
index af436ff3..ece8dc4b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
@@ -5,7 +5,15 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
-class StreamTape : ExtractorApi() {
+class StreamTapeNet : StreamTape() {
+ override var mainUrl = "https://streamtape.net"
+}
+
+class ShaveTape : StreamTape(){
+ override var mainUrl = "https://shavetape.cash"
+}
+
+open class StreamTape : ExtractorApi() {
override var name = "StreamTape"
override var mainUrl = "https://streamtape.com"
override val requiresReferer = false
@@ -16,7 +24,8 @@ class StreamTape : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List? {
with(app.get(url)) {
linkRegex.find(this.text)?.let {
- val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}"
+ val extractedUrl =
+ "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3)}"
return listOf(
ExtractorLink(
name,
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
index 2765ae17..c7689c58 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.JsUnpacker
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URI
-class Streamhub : ExtractorApi() {
+open class Streamhub : ExtractorApi() {
override var mainUrl = "https://streamhub.to"
override var name = "Streamhub"
override val requiresReferer = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt
new file mode 100644
index 00000000..e6bbfeba
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt
@@ -0,0 +1,81 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.APIHolder.getCaptchaToken
+import com.lagradost.cloudstream3.ErrorLoadingException
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import java.net.URI
+
+open class Streamplay : ExtractorApi() {
+ override val name = "Streamplay"
+ override val mainUrl = "https://streamplay.to"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val request = app.get(url, referer = referer)
+ val redirectUrl = request.url
+ val mainServer = URI(redirectUrl).let {
+ "${it.scheme}://${it.host}"
+ }
+ val key = redirectUrl.substringAfter("embed-").substringBefore(".html")
+ val token =
+ request.document.select("script").find { it.data().contains("sitekey:") }?.data()
+ ?.substringAfterLast("sitekey: '")?.substringBefore("',")?.let { captchaKey ->
+ getCaptchaToken(
+ redirectUrl,
+ captchaKey,
+ referer = "$mainServer/"
+ )
+ } ?: throw ErrorLoadingException("can't bypass captcha")
+ app.post(
+ "$mainServer/player-$key-488x286.html", data = mapOf(
+ "op" to "embed",
+ "token" to token
+ ),
+ referer = redirectUrl,
+ headers = mapOf(
+ "Accept" to "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
+ "Content-Type" to "application/x-www-form-urlencoded"
+ )
+ ).document.select("script").find { script ->
+ script.data().contains("eval(function(p,a,c,k,e,d)")
+ }?.let {
+ val data = getAndUnpack(it.data()).substringAfter("sources=[").substringBefore(",desc")
+ .replace("file", "\"file\"")
+ .replace("label", "\"label\"")
+ tryParseJson>("[$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,
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
index 955345aa..dd49d994 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
@@ -11,7 +11,7 @@ data class Files(
@JsonProperty("label") val label: String? = null,
)
- open class Supervideo : ExtractorApi() {
+open class Supervideo : ExtractorApi() {
override var name = "Supervideo"
override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false
@@ -20,10 +20,13 @@ data class Files(
val response = app.get(url).text
val jstounpack = Regex("eval((.|\\n)*?)").find(response)?.groups?.get(1)?.value
val unpacjed = JsUnpacker(jstounpack).unpack()
- val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",")
+ val extractedUrl =
+ unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString()
+ .replace("file", """"file"""").replace("label", """"label"""")
+ .substringBeforeLast(",")
val parsedlinks = parseJson>(extractedUrl)
parsedlinks.forEach { data ->
- if (data.label.isNullOrBlank()){ // mp4 links (with labels) are slow. Use only m3u8 link.
+ if (data.label.isNullOrBlank()) { // mp4 links (with labels) are slow. Use only m3u8 link.
M3u8Helper.generateM3u8(
name,
data.id,
@@ -34,8 +37,6 @@ data class Files(
}
}
}
-
-
return extractedLinksList
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
index 20bd69ba..28a2eb20 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
@@ -1,41 +1,64 @@
package com.lagradost.cloudstream3.extractors
-import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.lagradost.cloudstream3.USER_AGENT
+import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
-import com.lagradost.cloudstream3.utils.ExtractorApi
-import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.Qualities
+
class Cinestart: Tomatomatela() {
- override var name = "Cinestart"
- override var mainUrl = "https://cinestart.net"
+ override var name: String = "Cinestart"
+ override val mainUrl: String = "https://cinestart.net"
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() {
override var name = "Tomatomatela"
- override var mainUrl = "https://tomatomatela.com"
+ override val mainUrl = "https://tomatomatela.com"
override val requiresReferer = false
private data class Tomato (
@JsonProperty("status") val status: Int,
- @JsonProperty("file") val file: String
+ @JsonProperty("file") val file: String?
)
open val details = "details.php?v="
+ open val embeddetails = "/embed.html#"
override suspend fun getUrl(url: String, referer: String?): List? {
- val link = url.replace("$mainUrl/embed.html#","$mainUrl/$details")
- val server = app.get(link, allowRedirects = false).text
- val json = parseJson(server)
- if (json.status == 200) return listOf(
- ExtractorLink(
- name,
- name,
- json.file,
- "",
- Qualities.Unknown.value,
- isM3u8 = false
+ val link = url.replace("$mainUrl$embeddetails","$mainUrl/$details")
+ val sources = ArrayList()
+ val server = app.get(link, allowRedirects = false,
+ headers = mapOf(
+ "User-Agent" to USER_AGENT,
+ "Accept" to "application/json, text/javascript, */*; q=0.01",
+ "Accept-Language" to "en-US,en;q=0.5",
+ "X-Requested-With" to "XMLHttpRequest",
+ "DNT" to "1",
+ "Connection" to "keep-alive",
+ "Sec-Fetch-Dest" to "empty",
+ "Sec-Fetch-Mode" to "cors",
+ "Sec-Fetch-Site" to "same-origin"
+
)
- )
- return null
+ ).parsedSafe()
+ if (server?.file != null) {
+ sources.add(
+ ExtractorLink(
+ name,
+ name,
+ server.file,
+ "",
+ Qualities.Unknown.value,
+ isM3u8 = false
+ )
+ )
+ }
+ return sources
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt
index 79c657b6..09e47d03 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt
@@ -1,19 +1,23 @@
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
+import com.lagradost.cloudstream3.utils.M3u8Helper
-class UpstreamExtractor: ExtractorApi() {
- override val name: String = "Upstream.to"
+open class UpstreamExtractor : ExtractorApi() {
+ override val name: String = "Upstream"
override val mainUrl: String = "https://upstream.to"
override val requiresReferer = true
- override suspend fun getUrl(url: String, referer: String?): List {
- // WIP: m3u8 link fetched but sometimes not playing
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
//Log.i(this.name, "Result => (no extractor) ${url}")
- val sources: MutableList = mutableListOf()
val doc = app.get(url, referer = referer).text
if (doc.isNotBlank()) {
var reg = Regex("(?<=master)(.*)(?=hls)")
@@ -30,7 +34,9 @@ class UpstreamExtractor: ExtractorApi() {
domName = "${part}.${domName}"
}
domName.trimEnd('.')
- } else { "" }
+ } else {
+ ""
+ }
}
false -> ""
}
@@ -42,18 +48,13 @@ class UpstreamExtractor: ExtractorApi() {
result?.forEach {
val linkUrl = "https://${domain}/hls/${it}/master.m3u8"
- sources.add(
- ExtractorLink(
- name = "Upstream m3u8",
- source = this.name,
- url = linkUrl,
- quality = Qualities.Unknown.value,
- referer = referer ?: linkUrl,
- isM3u8 = true
- )
- )
+ M3u8Helper.generateM3u8(
+ this.name,
+ linkUrl,
+ "$mainUrl/",
+ headers = mapOf("Origin" to mainUrl)
+ ).forEach(callback)
}
}
- return sources
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
index e5d2875f..5109acc3 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
@@ -25,7 +25,7 @@ open class Uqload : ExtractorApi() {
} else {
""
}
-
+
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
index 7b087157..b910f9dd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
@@ -1,12 +1,11 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
-import com.lagradost.cloudstream3.apmap
+import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.utils.ExtractorApi
-import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.M3u8Helper
-import com.lagradost.cloudstream3.utils.loadExtractor
+import com.lagradost.cloudstream3.utils.*
+import kotlinx.coroutines.delay
+import java.net.URI
class VidSrcExtractor2 : VidSrcExtractor() {
override val mainUrl = "https://vidsrc.me/embed"
@@ -27,6 +26,25 @@ open class VidSrcExtractor : ExtractorApi() {
override val mainUrl = "$absoluteUrl/embed"
override val requiresReferer = false
+ companion object {
+ /** Infinite function to validate the vidSrc pass */
+ suspend fun validatePass(url: String) {
+ val uri = URI(url)
+ val host = uri.host
+
+ // Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/
+ val referer = host.split(".").let {
+ val size = it.size
+ "https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/"
+ }
+
+ while (true) {
+ app.get(url, referer = referer)
+ delay(60_000)
+ }
+ }
+ }
+
override suspend fun getUrl(
url: String,
referer: String?,
@@ -40,7 +58,10 @@ open class VidSrcExtractor : ExtractorApi() {
val datahash = it.attr("data-hash")
if (datahash.isNotBlank()) {
val links = try {
- app.get("$absoluteUrl/src/$datahash", referer = "https://source.vidsrc.me/").url
+ app.get(
+ "$absoluteUrl/src/$datahash",
+ referer = "https://source.vidsrc.me/"
+ ).url
} catch (e: Exception) {
""
}
@@ -48,17 +69,28 @@ open class VidSrcExtractor : ExtractorApi() {
} else ""
}
- serverslist.apmap { server ->
+ serverslist.amap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/pro")) {
val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
- val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap
- M3u8Helper.generateM3u8(
- name,
- srcm3u8,
- absoluteUrl
- ).forEach(callback)
+ val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
+ val passRegex = Regex("""['"](.*set_pass[^"']*)""")
+ val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
+ Regex("""^//"""), "https://"
+ )
+
+ callback.invoke(
+ ExtractorLink(
+ this.name,
+ this.name,
+ srcm3u8,
+ "https://vidsrc.stream/",
+ Qualities.Unknown.value,
+ extractorData = pass,
+ isM3u8 = true
+ )
+ )
} else {
loadExtractor(linkfixed, url, subtitleCallback, callback)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt
index 41e77967..30a1d8fe 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt
@@ -11,7 +11,7 @@ class VideovardSX : WcoStream() {
override var mainUrl = "https://videovard.sx"
}
-class VideoVard : ExtractorApi() {
+open class VideoVard : ExtractorApi() {
override var name = "Videovard" // Cause works for animekisa and wco
override var mainUrl = "https://videovard.to"
override val requiresReferer = false
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
new file mode 100644
index 00000000..615cfd74
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
@@ -0,0 +1,69 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+
+class Vidmolyme : Vidmoly() {
+ override val mainUrl = "https://vidmoly.me"
+}
+
+open class Vidmoly : ExtractorApi() {
+ override val name = "Vidmoly"
+ override val mainUrl = "https://vidmoly.to"
+ override val requiresReferer = true
+
+ private fun String.addMarks(str: String): String {
+ return this.replace(Regex("\"?$str\"?"), "\"$str\"")
+ }
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+
+ val script = app.get(
+ url,
+ referer = referer,
+ ).document.select("script")
+ .find { it.data().contains("sources:") }?.data()
+ val videoData = script?.substringAfter("sources: [")
+ ?.substringBefore("],")?.addMarks("file")
+ val subData = script?.substringAfter("tracks: [")?.substringBefore("]")?.addMarks("file")
+ ?.addMarks("label")?.addMarks("kind")
+
+ tryParseJson