Compare commits

..

6 commits

Author SHA1 Message Date
LagradOst
bef34c33e9 save location 2023-11-02 22:08:41 +01:00
KingLucius
fca8a55e05
another io error (#731) 2023-10-28 14:33:01 +02:00
LagradOst
49b905c089 poc 2023-10-26 21:40:45 +02:00
LagradOst
afe82140fd fix 2023-10-26 01:53:43 +02:00
LagradOst
8105231a6b testing seq torrent 2023-10-26 01:51:38 +02:00
LagradOst
d394f0e1d0 torrent testing 2023-09-14 18:46:34 +02:00
584 changed files with 9722 additions and 26415 deletions

View file

@ -80,13 +80,13 @@ body:
label: Acknowledgements label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you haven't done these steps.
options: options:
- label: I am sure my issue is related to the app and **NOT some extension**.
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**. - label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
required: true required: true
- label: If related to a provider, I have checked the site and it works, but not the app.
required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View file

@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Request a new provider or report bug with an existing provider - name: Request a new provider or report bug with an existing provider
url: https://github.com/recloudstream url: https://github.com/recloudstream
about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
- name: Discord - name: Discord
url: https://discord.gg/5Hus6fM url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues. about: Join our discord for faster support on smaller issues.

View file

@ -27,7 +27,9 @@ body:
label: Acknowledgements label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you haven't done these steps.
options: options:
- label: My suggestion is **NOT** about adding a new provider
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true required: true
- label: I have written a short but informative title.
required: true
- label: I will fill out all of the requested information in this form.
required: true

6
.github/locales.py vendored
View file

@ -1,7 +1,6 @@
import re import re
import glob import glob
import requests import requests
import os
import lxml.etree as ET # builtin library doesn't preserve comments import lxml.etree as ET # builtin library doesn't preserve comments
@ -54,16 +53,11 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
try: try:
tree = ET.parse(file) tree = ET.parse(file)
for child in tree.getroot(): for child in tree.getroot():
if not child.text:
continue
if child.text.startswith("\\@string/"): if child.text.startswith("\\@string/"):
print(f"[{file}] fixing {child.attrib['name']}") print(f"[{file}] fixing {child.attrib['name']}")
child.text = child.text.replace("\\@string/", "@string/") child.text = child.text.replace("\\@string/", "@string/")
with open(file, 'wb') as fp: with open(file, 'wb') as fp:
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n') fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False) tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
# Remove trailing new line to be consistent with weblate
fp.seek(-1, os.SEEK_END)
fp.truncate()
except ET.ParseError as ex: except ET.ParseError as ex:
print(f"[{file}] {ex}") print(f"[{file}] {ex}")

View file

@ -19,21 +19,21 @@ jobs:
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets" repository: "recloudstream/secrets"
- name: Generate access token (archive) - name: Generate access token (archive)
id: generate_archive_token id: generate_archive_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive" repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v2
with: with:
java-version: '17' java-version: '17'
distribution: 'adopt' distribution: 'adopt'
@ -58,7 +58,7 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
repository: "recloudstream/cloudstream-archive" repository: "recloudstream/cloudstream-archive"
token: ${{ steps.generate_archive_token.outputs.token }} token: ${{ steps.generate_archive_token.outputs.token }}

View file

@ -20,7 +20,7 @@ jobs:
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
@ -43,13 +43,12 @@ jobs:
rm -rf "./-cloudstream" rm -rf "./-cloudstream"
- name: Setup JDK 17 - name: Setup JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v1
with: with:
java-version: 17 java-version: 17
distribution: 'adopt'
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v2
- name: Generate Dokka - name: Generate Dokka
run: | run: |

View file

@ -10,7 +10,7 @@ jobs:
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
@ -27,7 +27,7 @@ jobs:
comment-body: '${index}. ${similarity} #${number}' comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate - name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true' if: steps.similarity.outputs.similar-issues-found =='true'
uses: actions/github-script@v7 uses: actions/github-script@v6
with: with:
github-token: ${{ steps.generate_token.outputs.token }} github-token: ${{ steps.generate_token.outputs.token }}
script: | script: |
@ -37,7 +37,7 @@ jobs:
repo: context.repo.repo, repo: context.repo.repo,
labels: ["possible duplicate"] labels: ["possible duplicate"]
}) })
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Automatically close issues that dont follow the issue template - name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2 uses: lucasbento/auto-close-issues@v1.0.2
with: with:
@ -68,7 +68,7 @@ jobs:
Found provider name: `${{ steps.provider_check.outputs.name }}` Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider - name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none' if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v7 uses: actions/github-script@v6
with: with:
github-token: ${{ steps.generate_token.outputs.token }} github-token: ${{ steps.generate_token.outputs.token }}
script: | script: |

View file

@ -18,14 +18,14 @@ jobs:
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets" repository: "recloudstream/secrets"
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v2
with: with:
java-version: '17' java-version: '17'
distribution: 'adopt' distribution: 'adopt'
@ -43,8 +43,7 @@ jobs:
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle - name: Run Gradle
run: | run: |
./gradlew assemblePrerelease build androidSourcesJar ./gradlew assemblePrerelease makeJar androidSourcesJar
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
env: env:
SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}

View file

@ -6,9 +6,9 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v2
with: with:
java-version: '17' java-version: '17'
distribution: 'adopt' distribution: 'adopt'
@ -17,7 +17,7 @@ jobs:
- name: Run Gradle - name: Run Gradle
run: ./gradlew assemblePrereleaseDebug run: ./gradlew assemblePrereleaseDebug
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v2
with: with:
name: pull-request-build name: pull-request-build
path: "app/build/outputs/apk/prerelease/debug/*.apk" path: "app/build/outputs/apk/prerelease/debug/*.apk"

View file

@ -18,12 +18,12 @@ jobs:
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream" repository: "recloudstream/cloudstream"
- uses: actions/checkout@v4 - uses: actions/checkout@v2
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
- name: Install dependencies - name: Install dependencies

6
.idea/gradle.xml generated
View file

@ -4,16 +4,16 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" /> <option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/library" />
</set> </set>
</option> </option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

View file

@ -1,14 +1,12 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.dokka.gradle.DokkaTask import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.net.URL import java.net.URL
plugins { plugins {
id("com.android.application") id("com.android.application")
id("com.google.devtools.ksp")
id("kotlin-android") id("kotlin-android")
id("kotlin-kapt")
id("org.jetbrains.dokka") id("org.jetbrains.dokka")
} }
@ -34,16 +32,16 @@ android {
enable = true enable = true
} }
/* disable this for now // disable this for now
externalNativeBuild { //externalNativeBuild {
cmake { // cmake {
path("CMakeLists.txt") // path("CMakeLists.txt")
} // }
}*/ //}
signingConfigs { signingConfigs {
if (prereleaseStoreFile != null) { create("prerelease") {
create("prerelease") { if (prereleaseStoreFile != null) {
storeFile = file(prereleaseStoreFile) storeFile = file(prereleaseStoreFile)
storePassword = System.getenv("SIGNING_STORE_PASSWORD") storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyAlias = System.getenv("SIGNING_KEY_ALIAS")
@ -52,16 +50,16 @@ android {
} }
} }
compileSdk = 34 compileSdk = 33
buildToolsVersion = "34.0.0" buildToolsVersion = "34.0.0"
defaultConfig { defaultConfig {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = 21 minSdk = 21
targetSdk = 33 /* Android 14 is Fu*ked targetSdk = 33
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
versionCode = 64 versionCode = 59
versionName = "4.4.0" versionName = "4.1.8"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -71,9 +69,9 @@ android {
val localProperties = gradleLocalProperties(rootDir) val localProperties = gradleLocalProperties(rootDir)
buildConfigField( buildConfigField(
"long", "String",
"BUILD_DATE", "BUILDDATE",
"${System.currentTimeMillis()}" "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
) )
buildConfigField( buildConfigField(
"String", "String",
@ -87,9 +85,8 @@ android {
) )
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ksp { kapt {
arg("room.schemaLocation", "$projectDir/schemas") includeCompileClasspath = true
arg("exportSchema", "true")
} }
} }
@ -112,7 +109,6 @@ android {
) )
} }
} }
flavorDimensions.add("state") flavorDimensions.add("state")
productFlavors { productFlavors {
create("stable") { create("stable") {
@ -124,31 +120,30 @@ android {
resValue("bool", "is_prerelease", "true") resValue("bool", "is_prerelease", "true")
buildConfigField("boolean", "BETA", "true") buildConfigField("boolean", "BETA", "true")
applicationIdSuffix = ".prerelease" applicationIdSuffix = ".prerelease"
if (signingConfigs.names.contains("prerelease")) { signingConfig = signingConfigs.getByName("prerelease")
signingConfig = signingConfigs.getByName("prerelease")
} else {
logger.warn("No prerelease signing config!")
}
versionNameSuffix = "-PRE" versionNameSuffix = "-PRE"
versionCode = (System.currentTimeMillis() / 60000).toInt() versionCode = (System.currentTimeMillis() / 60000).toInt()
} }
} }
//toolchain {
// languageVersion.set(JavaLanguageVersion.of(17))
// }
// jvmToolchain(17)
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
}
lint { lint {
abortOnError = false abortOnError = false
checkReleaseBuilds = false checkReleaseBuilds = false
} }
buildFeatures {
buildConfig = true
}
namespace = "com.lagradost.cloudstream3" namespace = "com.lagradost.cloudstream3"
} }
@ -157,132 +152,129 @@ repositories {
} }
dependencies { dependencies {
// Testing implementation("com.google.android.mediahome:video:1.0.0")
testImplementation("junit:junit:4.13.2") implementation("androidx.test.ext:junit-ktx:1.1.5")
testImplementation("org.json:json:20240303") testImplementation("org.json:json:20180813")
androidTestImplementation("androidx.test:core")
implementation("androidx.test.ext:junit-ktx:1.2.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
// Android Core & Lifecycle implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.core:core-ktx:1.13.1") implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
// Design & UI // dont change this to 1.6.0 it looks ugly af
implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("com.google.android.material:material:1.5.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("androidx.test:core")
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("org.jsoup:jsoup:1.13.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("com.github.bumptech.glide:glide:4.13.1")
kapt("com.github.bumptech.glide:compiler:4.13.1")
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// Glide Module // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
ksp("com.github.bumptech.glide:ksp:4.16.0")
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP // Media 3
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
implementation("com.google.guava:guava:33.2.1-android")
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
// Media 3 (ExoPlayer)
implementation("androidx.media3:media3-ui:1.1.1")
implementation("androidx.media3:media3-cast:1.1.1")
implementation("androidx.media3:media3-common:1.1.1") implementation("androidx.media3:media3-common:1.1.1")
implementation("androidx.media3:media3-session:1.1.1")
implementation("androidx.media3:media3-exoplayer:1.1.1") implementation("androidx.media3:media3-exoplayer:1.1.1")
implementation("com.google.android.mediahome:video:1.0.0") implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
implementation("androidx.media3:media3-ui:1.1.1")
implementation("androidx.media3:media3-session:1.1.1")
implementation("androidx.media3:media3-cast:1.1.1")
implementation("androidx.media3:media3-exoplayer-hls:1.1.1") implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
implementation("androidx.media3:media3-exoplayer-dash:1.1.1") implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
implementation("androidx.media3:media3-datasource-okhttp:1.1.1") // Custom ffmpeg extension for audio codecs
implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
// PlayBack //implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding
// Crash Reports (AcraApplication.kt) // Bug reports
implementation("ch.acra:acra-core:5.11.3") implementation("ch.acra:acra-core:5.11.0")
implementation("ch.acra:acra-toast:5.11.3") implementation("ch.acra:acra-toast:5.11.0")
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
//either for java sources:
annotationProcessor("com.google.auto.service:auto-service:1.0")
//or for kotlin sources (requires kapt gradle plugin):
kapt("com.google.auto.service:auto-service:1.0")
// subtitle color picker
implementation("com.jaredrummler:colorpicker:1.1.0")
//run JS
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation("org.mozilla:rhino:1.7.13")
// TorrentStream
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
// Downloading
implementation("androidx.work:work-runtime:2.8.1")
implementation("androidx.work:work-runtime-ktx:2.8.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.3")
// 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.LagradOst:SafeFile:0.0.5")
// API because cba maintaining it myself
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
implementation("com.github.discord:OverlappingPanels:0.1.5")
// debugImplementation because LeakCanary should only run in debug builds.
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// for shimmer when loading
implementation("com.facebook.shimmer:shimmer:0.5.0")
// UI Stuff
implementation("com.facebook.shimmer:shimmer:0.5.0") // Shimmering Effect (Loading Skeleton)
implementation("androidx.palette:palette-ktx:1.0.0") // Palette For Images -> Colors
implementation("androidx.tvprovider:tvprovider:1.0.0") implementation("androidx.tvprovider:tvprovider:1.0.0")
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV
// Extensions & Other Libs // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation("org.mozilla:rhino:1.7.15") // run JavaScript implementation("com.github.albfernandez:juniversalchardet:2.4.0")
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
Level 25 or Less. */
// Downloading & Networking // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
implementation("androidx.work:work-runtime:2.9.0") // this should be updated frequently to avoid trailer fu*kery
implementation("androidx.work:work-runtime-ktx:2.9.0") implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28")
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
implementation(project(":library") { // Library/extensions searching with Levenshtein distance
// There does not seem to be a good way of getting the android flavor. implementation("me.xdrop:fuzzywuzzy:1.4.0")
val isDebug = gradle.startParameter.taskRequests.any { task ->
task.args.any { arg ->
arg.contains("debug", true)
}
}
this.extra.set("isDebug", isDebug) // color palette for images -> colors
}) implementation("androidx.palette:palette-ktx:1.0.0")
implementation("com.github.recloudstream:Aria2cStream:0.0.3")
} }
tasks.register<Jar>("androidSourcesJar") { tasks.register("androidSourcesJar", Jar::class) {
archiveClassifier.set("sources") archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources from(android.sourceSets.getByName("main").java.srcDirs) //full sources
} }
tasks.register<Copy>("copyJar") { // this is used by the gradlew plugin
from( tasks.register("makeJar", Copy::class) {
"build/intermediates/compile_app_classes_jar/prereleaseDebug", from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
"../library/build/libs" into("build")
) include("classes.jar")
into("build/app-classes") dependsOn("build")
include("classes.jar", "library-jvm*.jar")
// Remove the version
rename("library-jvm.*.jar", "library-jvm.jar")
}
// Merge the app classes and the library classes into classes.jar
tasks.register<Jar>("makeJar") {
// Duplicates cause hard to catch errors, better to fail at compile time.
duplicatesStrategy = DuplicatesStrategy.FAIL
dependsOn(tasks.getByName("copyJar"))
from(
zipTree("build/app-classes/classes.jar"),
zipTree("build/app-classes/library-jvm.jar")
)
destinationDirectory.set(layout.buildDirectory)
archivesName = "classes"
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
} }
tasks.withType<DokkaTask>().configureEach { tasks.withType<DokkaTask>().configureEach {
@ -295,7 +287,6 @@ tasks.withType<DokkaTask>().configureEach {
// URL showing where the source code can be accessed through the web browser // URL showing where the source code can be accessed through the web browser
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java")) remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub // Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L") remoteLineSuffix.set("#L")
} }

View file

@ -9,8 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding import com.lagradost.cloudstream3.databinding.FragmentResultBinding
@ -19,7 +17,6 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
@ -120,12 +117,9 @@ class ExampleInstrumentedTest {
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) // testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv) // testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent) testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent) testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
} }
} }
} }
@ -154,7 +148,7 @@ class ExampleInstrumentedTest {
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().toList().amap { api -> getAllProviders().toList().amap { api ->
TestingUtils.testHomepage(api, TestingUtils.Logger()) TestingUtils.testHomepage(api, ::println)
} }
} }
println("Done providerCorrectHomepage") println("Done providerCorrectHomepage")
@ -166,6 +160,7 @@ class ExampleInstrumentedTest {
TestingUtils.getDeferredProviderTests( TestingUtils.getDeferredProviderTests(
this, this,
getAllProviders(), getAllProviders(),
::println
) { _, _ -> } ) { _, _ -> }
} }
} }

View file

@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this --> <uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide --> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update --> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
@ -14,14 +14,8 @@
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next --> <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt --> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- Required for getting arbitrary Aniyomi packages -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
@ -41,11 +35,9 @@
<application <application
android:name=".AcraApplication" android:name=".AcraApplication"
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:appCategory="video" android:appCategory="video"
android:banner="@mipmap/ic_banner" android:banner="@mipmap/ic_banner"
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
@ -53,7 +45,7 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="tiramisu"> tools:targetApi="o">
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
@ -69,9 +61,7 @@
android:exported="true" android:exported="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:screenOrientation="userLandscape" android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true">
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
android:launchMode="singleTask">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -97,11 +87,17 @@
--> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune --> <!-- cloudstreamplayer://encodedUrl?name=Dune -->
<intent-filter> <intent-filter>
@ -165,21 +161,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.account.AccountSelectActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity <activity
android:name=".ui.EasterEggMonke" android:name=".ui.EasterEggMonke"
android:exported="true" /> android:exported="true" />
@ -187,14 +168,13 @@
<receiver <receiver
android:name=".receivers.VideoDownloadRestartReceiver" android:name=".receivers.VideoDownloadRestartReceiver"
android:enabled="false" android:enabled="false"
android:exported="false"> android:exported="true">
<intent-filter android:exported="false"> <intent-filter android:exported="true">
<action android:name="restart_service" /> <action android:name="restart_service" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service <service
android:foregroundServiceType="dataSync"
android:name=".services.VideoDownloadService" android:name=".services.VideoDownloadService"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
@ -204,7 +184,6 @@
android:exported="false" /> android:exported="false" />
<service <service
android:foregroundServiceType="dataSync"
android:name=".utils.PackageInstallerService" android:name=".utils.PackageInstallerService"
android:exported="false" /> android:exported="false" />

View file

@ -5,17 +5,16 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.lagradost.api.setContext import com.google.auto.service.AutoService
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys import com.lagradost.cloudstream3.utils.DataStore.getKeys
@ -34,15 +33,17 @@ import org.acra.sender.ReportSenderFactory
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.PrintStream import java.io.PrintStream
import java.lang.Exception
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
class CustomReportSender : ReportSender { class CustomReportSender : ReportSender {
// Sends all your crashes to google forms // Sends all your crashes to google forms
override fun send(context: Context, errorContent: CrashReportData) { override fun send(context: Context, errorContent: CrashReportData) {
println("Sending report") println("Sending report")
//Log.i("Acra", "Sending report: ${errorContent.toMap().map { "${it.key}:${it.value}" }.joinToString()}")
val url = val url =
"https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse" "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
val data = mapOf( val data = mapOf(
@ -66,6 +67,7 @@ class CustomReportSender : ReportSender {
} }
} }
@AutoService(ReportSenderFactory::class)
class CustomSenderFactory : ReportSenderFactory { class CustomSenderFactory : ReportSenderFactory {
override fun create(context: Context, config: CoreConfiguration): ReportSender { override fun create(context: Context, config: CoreConfiguration): ReportSender {
return CustomReportSender() return CustomReportSender()
@ -82,8 +84,14 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
ACRA.errorReporter.handleException(error) ACRA.errorReporter.handleException(error)
try { try {
PrintStream(errorFile).use { ps -> PrintStream(errorFile).use { ps ->
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}") ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
ps.println("Fatal exception on thread ${thread.name} (${thread.id})") ps.println(
String.format(
"Fatal exception on thread %s (%d)",
thread.name,
thread.id
)
)
error.printStackTrace(ps) error.printStackTrace(ps)
} }
} catch (ignored: FileNotFoundException) { } catch (ignored: FileNotFoundException) {
@ -101,6 +109,7 @@ class AcraApplication : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
//NativeCrashHandler.initCrashHandler()
ExceptionHandler(filesDir.resolve("last_error")) { ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName) val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component)) startActivity(Intent.makeRestartActivityTask(intent!!.component))
@ -146,7 +155,6 @@ class AcraApplication : Application() {
get() = _context?.get() get() = _context?.get()
private set(value) { private set(value) {
_context = WeakReference(value) _context = WeakReference(value)
setContext(WeakReference(value))
} }
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? { fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
@ -208,7 +216,7 @@ class AcraApplication : Application() {
fun openBrowser(url: String, activity: FragmentActivity?) { fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser( openBrowser(
url, url,
isLayout(TV or EMULATOR), isTvSettings(),
activity?.supportFragmentManager?.fragments?.lastOrNull() activity?.supportFragmentManager?.fragments?.lastOrNull()
) )
} }

View file

@ -5,16 +5,17 @@ import android.app.Activity
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.NO_ID import android.view.View.NO_ID
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -30,14 +31,12 @@ import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
@ -66,11 +65,6 @@ object CommonActivity {
_activity = WeakReference(value) _activity = WeakReference(value)
} }
@MainThread
fun setActivityInstance(newActivity: Activity?) {
activity = newActivity
}
@MainThread @MainThread
fun Activity?.getCastSession(): CastSession? { fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession return (this as MainActivity?)?.mSessionManager?.currentCastSession
@ -100,7 +94,8 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
private var currentToast: Toast? = null
var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) { fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return val act = activity ?: return
@ -156,19 +151,25 @@ object CommonActivity {
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
try { try {
val binding = ToastBinding.inflate(act.layoutInflater) val inflater =
binding.text.text = message.trim() act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(
R.layout.toast,
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
)
val text = layout.findViewById(R.id.text) as TextView
text.text = message.trim()
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
val toast = Toast(act) val toast = Toast(act)
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. toast.duration = duration ?: Toast.LENGTH_SHORT
currentToast = toast toast.view = layout
//https://github.com/PureWriter/ToastCompat
toast.show() toast.show()
currentToast = toast
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
@ -202,25 +203,23 @@ object CommonActivity {
setLocale(this, localeCode) setLocale(this, localeCode)
} }
fun init(act: Activity) { fun init(act: ComponentActivity?) {
setActivityInstance(act) if (act == null) return
activity = act
val componentActivity = activity as? ComponentActivity ?: return
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture //https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode = canShowPipMode =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
componentActivity.updateLocale() act.updateLocale()
componentActivity.updateTv() act.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance()) NewPipe.init(DownloaderTestImpl.getInstance())
for (resumeApp in resumeApps) { for (resumeApp in resumeApps) {
resumeApp.launcher = resumeApp.launcher =
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode val resultCode = result.resultCode
val data = result.data val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
@ -237,11 +236,11 @@ object CommonActivity {
// Ask for notification permissions on Android 13 // Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
componentActivity, act,
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED ) != PackageManager.PERMISSION_GRANTED
) { ) {
val requestPermissionLauncher = componentActivity.registerForActivityResult( val requestPermissionLauncher = act.registerForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted: Boolean -> ) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted") Log.d(TAG, "Notification permission: $isGranted")
@ -277,35 +276,12 @@ object CommonActivity {
} }
} }
fun updateTheme(act: Activity) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
if (settingsManager
.getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
loadThemes(act)
}
}
private fun mapSystemTheme(act: Activity): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val currentNightMode =
act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme
else -> R.style.AppTheme // Night mode is active, we're using dark theme
}
} else {
return R.style.AppTheme
}
}
fun loadThemes(act: Activity?) { fun loadThemes(act: Activity?) {
if (act == null) return if (act == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
val currentTheme = val currentTheme =
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
"System" -> mapSystemTheme(act)
"Black" -> R.style.AppTheme "Black" -> R.style.AppTheme
"Light" -> R.style.LightMode "Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode "Amoled" -> R.style.AmoledMode
@ -319,15 +295,12 @@ object CommonActivity {
val currentOverlayTheme = val currentOverlayTheme =
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) { when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
"Normal" -> R.style.OverlayPrimaryColorNormal "Normal" -> R.style.OverlayPrimaryColorNormal
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink "CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
"Orange" -> R.style.OverlayPrimaryColorOrange
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen "DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
"Maroon" -> R.style.OverlayPrimaryColorMaroon "Maroon" -> R.style.OverlayPrimaryColorMaroon
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue "NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
"Grey" -> R.style.OverlayPrimaryColorGrey "Grey" -> R.style.OverlayPrimaryColorGrey
"White" -> R.style.OverlayPrimaryColorWhite "White" -> R.style.OverlayPrimaryColorWhite
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
"Brown" -> R.style.OverlayPrimaryColorBrown "Brown" -> R.style.OverlayPrimaryColorBrown
"Purple" -> R.style.OverlayPrimaryColorPurple "Purple" -> R.style.OverlayPrimaryColorPurple
"Green" -> R.style.OverlayPrimaryColorGreen "Green" -> R.style.OverlayPrimaryColorGreen
@ -336,7 +309,6 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana "Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty "Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink "Pink" -> R.style.OverlayPrimaryColorPink
"Lavender" -> R.style.OverlayPrimaryColorLavender
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
@ -376,8 +348,8 @@ object CommonActivity {
currentLook = currentLook.parent as? View ?: break currentLook = currentLook.parent as? View ?: break
}*/ }*/
private fun View.hasContent(): Boolean { private fun View.hasContent() : Boolean {
return isShown && when (this) { return isShown && when(this) {
//is RecyclerView -> this.childCount > 0 //is RecyclerView -> this.childCount > 0
is ViewGroup -> this.childCount > 0 is ViewGroup -> this.childCount > 0
else -> true else -> true
@ -488,6 +460,20 @@ object CommonActivity {
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
// Tested keycodes on remote:
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
// KeyEvent.KEYCODE_MEDIA_REWIND
// KeyEvent.KEYCODE_MENU
// KeyEvent.KEYCODE_MEDIA_NEXT
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
// 149 keycode_numpad 5 // 149 keycode_numpad 5
when (keyCode) { when (keyCode) {

View file

@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.schabi.newpipe.extractor.downloader.Downloader import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.downloader.Response
@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() private val client: OkHttpClient
override fun execute(request: Request): Response { override fun execute(request: Request): Response {
val httpMethod: String = request.httpMethod() val httpMethod: String = request.httpMethod()
val url: String = request.url() val url: String = request.url()
@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
val dataToSend: ByteArray? = request.dataToSend() val dataToSend: ByteArray? = request.dataToSend()
var requestBody: RequestBody? = null var requestBody: RequestBody? = null
if (dataToSend != null) { if (dataToSend != null) {
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size) requestBody = RequestBody.create(null, dataToSend)
} }
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url) .method(httpMethod, requestBody).url(url)
@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
return instance return instance
} }
} }
init {
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
}
} }

View file

@ -1,43 +1,48 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.util.Base64.encodeToString
import androidx.annotation.WorkerThread
import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklApi
import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.nicehttp.RequestBodyTypes import com.lagradost.nicehttp.RequestBodyTypes
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.net.URI
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
//val baseHeader = mapOf("User-Agent" to 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 * Defines the constant for the all languages preference, if this is set then it is
* the equivalent of all languages being set * the equivalent of all languages being set
**/ **/
const val AllLanguagesName = "universal" const val AllLanguagesName = "universal"
const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
class ErrorLoadingException(message: String? = null) : Exception(message)
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
val mapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
object APIHolder { object APIHolder {
val unixTime: Long val unixTime: Long
get() = System.currentTimeMillis() / 1000L get() = System.currentTimeMillis() / 1000L
@ -108,6 +113,15 @@ object APIHolder {
return null return null
} }
private fun getLoadResponseIdFromUrl(url: String, apiName: String): Int {
return url.replace(getApiFromNameNull(apiName)?.mainUrl ?: "", "").replace("/", "")
.hashCode()
}
fun LoadResponse.getId(): Int {
return getLoadResponseIdFromUrl(url, apiName)
}
/** /**
* Gets the website captcha token * Gets the website captcha token
* discovered originally by https://github.com/ahmedgamal17 * discovered originally by https://github.com/ahmedgamal17
@ -123,9 +137,10 @@ object APIHolder {
// To get the key // To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? { suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try { try {
val uri = URI.create(url) val uri = Uri.parse(url)
val domain = base64Encode( val domain = encodeToString(
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(), (uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
0
).replace("\n", "").replace("=", ".") ).replace("\n", "").replace("=", ".")
val vToken = val vToken =
@ -164,13 +179,6 @@ object APIHolder {
private var trackerCache: HashMap<String, AniSearch> = hashMapOf() private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
/** backwards compatibility, use getTracker4 instead */
suspend fun getTracker(
titles: List<String>,
types: Set<TrackerType>?,
year: Int?,
): Tracker? = getTracker(titles, types, year, false)
/** /**
* Get anime tracker information based on title, year and type. * Get anime tracker information based on title, year and type.
* Both titles are attempted to be matched with both Romaji and English title. * Both titles are attempted to be matched with both Romaji and English title.
@ -184,7 +192,7 @@ object APIHolder {
titles: List<String>, titles: List<String>,
types: Set<TrackerType>?, types: Set<TrackerType>?,
year: Int?, year: Int?,
lessAccurate: Boolean lessAccurate: Boolean = false
): Tracker? { ): Tracker? {
return try { return try {
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
@ -205,15 +213,10 @@ object APIHolder {
} ?: false } ?: false
val matchingTypes = types?.any { it.name.equals(media.format, true) } == true val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears if(lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
} ?: return null } ?: return null
Tracker( Tracker(res.idMal, res.id.toString(), res.coverImage?.extraLarge ?: res.coverImage?.large, res.bannerImage)
res.idMal,
res.id.toString(),
res.coverImage?.extraLarge ?: res.coverImage?.large,
res.bannerImage
)
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
null null
@ -260,6 +263,165 @@ object APIHolder {
return app.post("https://graphql.anilist.co", requestBody = data) return app.post("https://graphql.anilist.co", requestBody = data)
.parsedSafe() .parsedSafe()
} }
fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
.map { it.name })
/*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key),
hashSet
)?.toHashSet() ?: hashSet
val list = HashSet<String>()
for (name in set) {
val api = getApiFromNameNull(name) ?: continue
if (activeLangs.contains(api.lang)) {
list.add(name)
}
}*/
//if (list.isEmpty()) return hashSet
//return list
return hashSet
}
fun Context.getApiDubstatusSettings(): HashSet<DubStatus> {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<DubStatus>()
hashSet.addAll(DubStatus.values())
val list = settingsManager.getStringSet(
this.getString(R.string.display_sub_key),
hashSet.map { it.name }.toMutableSet()
) ?: return hashSet
val names = DubStatus.values().map { it.name }.toHashSet()
//if(realSet.isEmpty()) return hashSet
return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet()
}
fun Context.getApiProviderLangSettings(): HashSet<String> {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
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
)
if (list.isNullOrEmpty()) return hashSet
return list.toHashSet()
}
fun Context.getApiTypeSettings(): HashSet<TvType> {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<TvType>()
hashSet.addAll(TvType.values())
val list = settingsManager.getStringSet(
this.getString(R.string.search_types_list_key),
hashSet.map { it.name }.toMutableSet()
)
if (list.isNullOrEmpty()) return hashSet
val names = TvType.values().map { it.name }.toHashSet()
val realSet = list.filter { names.contains(it) }.map { TvType.valueOf(it) }.toHashSet()
if (realSet.isEmpty()) return hashSet
return realSet
}
fun Context.updateHasTrailers() {
LoadResponse.isTrailersEnabled = getHasTrailers()
}
private fun Context.getHasTrailers(): Boolean {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
}
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
// 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)
.getStringSet(this.getString(R.string.prefer_media_type_key), defaultSet)
?.mapNotNull { it.toIntOrNull() ?: return@mapNotNull null }
} catch (e: Throwable) {
null
} ?: default
val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName)
val allApis = synchronized(apis) {
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
}
return if (currentPrefMedia.isEmpty()) {
allApis
} else {
// Filter API depending on preferred media type
allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } }
}
}
fun Context.filterSearchResultByFilmQuality(data: List<SearchResponse>): List<SearchResponse> {
// Filter results omitting entries with certain quality
if (data.isNotEmpty()) {
val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this)
?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf())
?.mapNotNull { entry ->
entry.toIntOrNull() ?: return@mapNotNull null
} ?: listOf()
if (filteredSearchQuality.isNotEmpty()) {
return data.filter { item ->
val searchQualVal = item.quality?.ordinal ?: -1
//Log.i("filterSearch", "QuickSearch item => ${item.toJson()}")
!filteredSearchQuality.contains(searchQualVal)
}
}
}
return data
}
fun Context.filterHomePageListByFilmQuality(data: HomePageList): HomePageList {
// Filter results omitting entries with certain quality
if (data.list.isNotEmpty()) {
val filteredSearchQuality = PreferenceManager.getDefaultSharedPreferences(this)
?.getStringSet(getString(R.string.pref_filter_search_quality_key), setOf())
?.mapNotNull { entry ->
entry.toIntOrNull() ?: return@mapNotNull null
} ?: listOf()
if (filteredSearchQuality.isNotEmpty()) {
return HomePageList(
name = data.name,
isHorizontalImages = data.isHorizontalImages,
list = data.list.filter { item ->
val searchQualVal = item.quality?.ordinal ?: -1
//Log.i("filterSearch", "QuickSearch item => ${item.toJson()}")
!filteredSearchQuality.contains(searchQualVal)
}
)
}
}
return data
}
} }
/* /*
@ -448,7 +610,7 @@ abstract class MainAPI {
/**Used for testing and can be used to disable the providers if WebView is not available*/ /**Used for testing and can be used to disable the providers if WebView is not available*/
open val usesWebView = false open val usesWebView = false
/** Determines which plugin a given provider is from. This is the full path to the plugin. */ /** Determines which plugin a given provider is from */
var sourcePlugin: String? = null var sourcePlugin: String? = null
open val hasMainPage = false open val hasMainPage = false
@ -482,7 +644,7 @@ abstract class MainAPI {
//emptyList<MainPageData>() // //emptyList<MainPageData>() //
open val mainPage = listOf(MainPageData("", "", false)) open val mainPage = listOf(MainPageData("", "", false))
// @WorkerThread @WorkerThread
open suspend fun getMainPage( open suspend fun getMainPage(
page: Int, page: Int,
request: MainPageRequest, request: MainPageRequest,
@ -490,17 +652,17 @@ abstract class MainAPI {
throw NotImplementedError() throw NotImplementedError()
} }
// @WorkerThread @WorkerThread
open suspend fun search(query: String): List<SearchResponse>? { open suspend fun search(query: String): List<SearchResponse>? {
throw NotImplementedError() throw NotImplementedError()
} }
// @WorkerThread @WorkerThread
open suspend fun quickSearch(query: String): List<SearchResponse>? { open suspend fun quickSearch(query: String): List<SearchResponse>? {
throw NotImplementedError() throw NotImplementedError()
} }
// @WorkerThread @WorkerThread
/** /**
* Based on data from search() or getMainPage() it generates a LoadResponse, * Based on data from search() or getMainPage() it generates a LoadResponse,
* basically opening the info page from a link. * basically opening the info page from a link.
@ -518,13 +680,13 @@ abstract class MainAPI {
* This function might be updated to include exoplayer timestamps etc in the future * This function might be updated to include exoplayer timestamps etc in the future
* if the need arises. * if the need arises.
* */ * */
// @WorkerThread @WorkerThread
open suspend fun extractorVerifierJob(extractorData: String?) { open suspend fun extractorVerifierJob(extractorData: String?) {
throw NotImplementedError() throw NotImplementedError()
} }
/**Callback is fired once a link is found, will return true if method is executed successfully*/ /**Callback is fired once a link is found, will return true if method is executed successfully*/
// @WorkerThread @WorkerThread
open suspend fun loadLinks( open suspend fun loadLinks(
data: String, data: String,
isCasting: Boolean, isCasting: Boolean,
@ -549,18 +711,31 @@ abstract class MainAPI {
} }
/** Might need a different implementation for desktop*/ /** Might need a different implementation for desktop*/
@SuppressLint("NewApi")
fun base64Decode(string: String): String { fun base64Decode(string: String): String {
return String(base64DecodeArray(string), Charsets.ISO_8859_1) return String(base64DecodeArray(string), Charsets.ISO_8859_1)
} }
@OptIn(ExperimentalEncodingApi::class)
@SuppressLint("NewApi")
fun base64DecodeArray(string: String): ByteArray { fun base64DecodeArray(string: String): ByteArray {
return Base64.decode(string) return try {
android.util.Base64.decode(string, android.util.Base64.DEFAULT)
} catch (e: Exception) {
Base64.getDecoder().decode(string)
}
} }
@OptIn(ExperimentalEncodingApi::class)
@SuppressLint("NewApi")
fun base64Encode(array: ByteArray): String { fun base64Encode(array: ByteArray): String {
return Base64.encode(array) return try {
String(android.util.Base64.encode(array, android.util.Base64.NO_WRAP), Charsets.ISO_8859_1)
} catch (e: Exception) {
String(Base64.getEncoder().encode(array))
}
} }
class ErrorLoadingException(message: String? = null) : Exception(message)
fun MainAPI.fixUrlNull(url: String?): String? { fun MainAPI.fixUrlNull(url: String?): String? {
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
return null return null
@ -594,6 +769,10 @@ fun sortUrls(urls: Set<ExtractorLink>): List<ExtractorLink> {
return urls.sortedBy { t -> -t.quality } return urls.sortedBy { t -> -t.quality }
} }
fun sortSubs(subs: Set<SubtitleData>): List<SubtitleData> {
return subs.sortedBy { it.name }
}
fun capitalizeString(str: String): String { fun capitalizeString(str: String): String {
return capitalizeStringNullable(str) ?: str return capitalizeStringNullable(str) ?: str
} }
@ -677,12 +856,7 @@ enum class TvType(value: Int?) {
AsianDrama(9), AsianDrama(9),
Live(10), Live(10),
NSFW(11), NSFW(11),
Others(12), Others(12)
Music(13),
AudioBook(14),
/** Wont load the built in player, make your own interaction */
CustomMedia(15),
} }
public enum class AutoDownloadMode(val value: Int) { public enum class AutoDownloadMode(val value: Int) {
@ -1012,28 +1186,13 @@ interface LoadResponse {
var syncData: MutableMap<String, String> var syncData: MutableMap<String, String>
var posterHeaders: Map<String, String>? var posterHeaders: Map<String, String>?
var backgroundPosterUrl: String? var backgroundPosterUrl: String?
var contentRating: String?
companion object { companion object {
var malIdPrefix = "" //malApi.idPrefix private val malIdPrefix = malApi.idPrefix
var aniListIdPrefix = "" //aniListApi.idPrefix private val aniListIdPrefix = aniListApi.idPrefix
var simklIdPrefix = "" //simklApi.idPrefix private val simklIdPrefix = simklApi.idPrefix
var isTrailersEnabled = true var isTrailersEnabled = true
/**
* The ID string is a way to keep a collection of services in one single ID using a map
* This adds a database service (like imdb) to the string and returns the new string.
*/
fun addIdToString(idString: String?, database: SimklSyncServices, id: String?): String? {
if (id == null) return idString
return (readIdFromString(idString) + mapOf(database to id)).toJson()
}
/** Read the id string to get all other ids */
fun readIdFromString(idString: String?): Map<SimklSyncServices, String> {
return tryParseJson(idString) ?: return emptyMap()
}
fun LoadResponse.isMovie(): Boolean { fun LoadResponse.isMovie(): Boolean {
return this.type.isMovieType() || this is MovieLoadResponse return this.type.isMovieType() || this is MovieLoadResponse
} }
@ -1057,12 +1216,12 @@ interface LoadResponse {
* Internal helper function to add simkl ids from other databases. * Internal helper function to add simkl ids from other databases.
*/ */
private fun LoadResponse.addSimklId( private fun LoadResponse.addSimklId(
database: SimklSyncServices, database: SimklApi.Companion.SyncServices,
id: String? id: String?
) { ) {
normalSafeApiCall { normalSafeApiCall {
this.syncData[simklIdPrefix] = this.syncData[simklIdPrefix] =
addIdToString(this.syncData[simklIdPrefix], database, id.toString()) SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString())
?: return@normalSafeApiCall ?: return@normalSafeApiCall
} }
} }
@ -1080,30 +1239,18 @@ interface LoadResponse {
return this.syncData[aniListIdPrefix] return this.syncData[aniListIdPrefix]
} }
fun LoadResponse.getImdbId(): String? {
return normalSafeApiCall {
readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Imdb]
}
}
fun LoadResponse.getTMDbId(): String? {
return normalSafeApiCall {
readIdFromString(this.syncData[simklIdPrefix])[SimklSyncServices.Tmdb]
}
}
fun LoadResponse.addMalId(id: Int?) { fun LoadResponse.addMalId(id: Int?) {
this.syncData[malIdPrefix] = (id ?: return).toString() this.syncData[malIdPrefix] = (id ?: return).toString()
this.addSimklId(SimklSyncServices.Mal, id.toString()) this.addSimklId(SimklApi.Companion.SyncServices.Mal, id.toString())
} }
fun LoadResponse.addAniListId(id: Int?) { fun LoadResponse.addAniListId(id: Int?) {
this.syncData[aniListIdPrefix] = (id ?: return).toString() this.syncData[aniListIdPrefix] = (id ?: return).toString()
this.addSimklId(SimklSyncServices.AniList, id.toString()) this.addSimklId(SimklApi.Companion.SyncServices.AniList, id.toString())
} }
fun LoadResponse.addSimklId(id: Int?) { fun LoadResponse.addSimklId(id: Int?) {
this.addSimklId(SimklSyncServices.Simkl, id.toString()) this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString())
} }
fun LoadResponse.addImdbUrl(url: String?) { fun LoadResponse.addImdbUrl(url: String?) {
@ -1185,7 +1332,7 @@ interface LoadResponse {
fun LoadResponse.addImdbId(id: String?) { fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync // TODO add imdb sync
this.addSimklId(SimklSyncServices.Imdb, id) this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id)
} }
fun LoadResponse.addTrackId(id: String?) { fun LoadResponse.addTrackId(id: String?) {
@ -1198,7 +1345,7 @@ interface LoadResponse {
fun LoadResponse.addTMDbId(id: String?) { fun LoadResponse.addTMDbId(id: String?) {
// TODO add TMDb sync // TODO add TMDb sync
this.addSimklId(SimklSyncServices.Tmdb, id) this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id)
} }
fun LoadResponse.addRating(text: String?) { fun LoadResponse.addRating(text: String?) {
@ -1277,24 +1424,11 @@ fun TvType?.isEpisodeBased(): Boolean {
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama) return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
} }
data class NextAiring( data class NextAiring(
val episode: Int, val episode: Int,
val unixTime: Long, val unixTime: Long,
val season: Int? = null, )
) {
/**
* Secondary constructor for backwards compatibility without season.
* TODO Remove this constructor after there is a new stable release and extensions are updated to support season.
*/
constructor(
episode: Int,
unixTime: Long,
) : this(
episode,
unixTime,
null
)
}
/** /**
* @param season To be mapped with episode season, not shown in UI if displaySeason is defined * @param season To be mapped with episode season, not shown in UI if displaySeason is defined
@ -1312,15 +1446,6 @@ interface EpisodeResponse {
var nextAiring: NextAiring? var nextAiring: NextAiring?
var seasonNames: List<SeasonData>? var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?> fun getLatestEpisodes(): Map<DubStatus, Int?>
/** Count all episodes in all previous seasons up until this episode to get a total count.
* Example:
* Season 1: 10 episodes.
* Season 2: 6 episodes.
*
* getTotalEpisodeIndex(episode = 3, season = 2) -> 10 + 3 = 13
* */
fun getTotalEpisodeIndex(episode: Int, season: Int): Int
} }
@JvmName("addSeasonNamesString") @JvmName("addSeasonNamesString")
@ -1358,55 +1483,7 @@ data class TorrentLoadResponse(
override var syncData: MutableMap<String, String> = mutableMapOf(), override var syncData: MutableMap<String, String> = mutableMapOf(),
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null, ) : LoadResponse
) : LoadResponse {
/**
* Secondary constructor for backwards compatibility without contentRating.
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
*/
constructor(
name: String,
url: String,
apiName: String,
magnet: String?,
torrent: String?,
plot: String?,
type: TvType = TvType.Torrent,
posterUrl: String? = null,
year: Int? = null,
rating: Int? = null,
tags: List<String>? = null,
duration: Int? = null,
trailers: MutableList<TrailerData> = mutableListOf(),
recommendations: List<SearchResponse>? = null,
actors: List<ActorData>? = null,
comingSoon: Boolean = false,
syncData: MutableMap<String, String> = mutableMapOf(),
posterHeaders: Map<String, String>? = null,
backgroundPosterUrl: String? = null,
) : this(
name,
url,
apiName,
magnet,
torrent,
plot,
type,
posterUrl,
year,
rating,
tags,
duration,
trailers,
recommendations,
actors,
comingSoon,
syncData,
posterHeaders,
backgroundPosterUrl,
null
)
}
data class AnimeLoadResponse( data class AnimeLoadResponse(
var engName: String? = null, var engName: String? = null,
@ -1437,7 +1514,6 @@ data class AnimeLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null,
) : LoadResponse, EpisodeResponse { ) : LoadResponse, EpisodeResponse {
override fun getLatestEpisodes(): Map<DubStatus, Int?> { override fun getLatestEpisodes(): Map<DubStatus, Int?> {
return episodes.map { (status, episodes) -> return episodes.map { (status, episodes) ->
@ -1449,77 +1525,6 @@ data class AnimeLoadResponse(
.takeUnless { it == Int.MIN_VALUE } .takeUnless { it == Int.MIN_VALUE }
}.toMap() }.toMap()
} }
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
return this.episodes.maxOf { (_, episodes) ->
episodes.count { episodeData ->
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
val episodeSeason =
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
// Count all episodes from season 1 to below the current season.
episodeSeason in 1..<season
}
} + episode
}
/**
* Secondary constructor for backwards compatibility without contentRating.
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
*/
constructor(
engName: String? = null,
japName: String? = null,
name: String,
url: String,
apiName: String,
type: TvType,
posterUrl: String? = null,
year: Int? = null,
episodes: MutableMap<DubStatus, List<Episode>> = mutableMapOf(),
showStatus: ShowStatus? = null,
plot: String? = null,
tags: List<String>? = null,
synonyms: List<String>? = null,
rating: Int? = null,
duration: Int? = null,
trailers: MutableList<TrailerData> = mutableListOf(),
recommendations: List<SearchResponse>? = null,
actors: List<ActorData>? = null,
comingSoon: Boolean = false,
syncData: MutableMap<String, String> = mutableMapOf(),
posterHeaders: Map<String, String>? = null,
nextAiring: NextAiring? = null,
seasonNames: List<SeasonData>? = null,
backgroundPosterUrl: String? = null,
) : this(
engName,
japName,
name,
url,
apiName,
type,
posterUrl,
year,
episodes,
showStatus,
plot,
tags,
synonyms,
rating,
duration,
trailers,
recommendations,
actors,
comingSoon,
syncData,
posterHeaders,
nextAiring,
seasonNames,
backgroundPosterUrl,
null
)
} }
/** /**
@ -1571,36 +1576,7 @@ data class LiveStreamLoadResponse(
override var syncData: MutableMap<String, String> = mutableMapOf(), override var syncData: MutableMap<String, String> = mutableMapOf(),
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null, ) : LoadResponse
) : LoadResponse {
/**
* Secondary constructor for backwards compatibility without contentRating.
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
*/
constructor(
name: String,
url: String,
apiName: String,
dataUrl: String,
posterUrl: String? = null,
year: Int? = null,
plot: String? = null,
type: TvType = TvType.Live,
rating: Int? = null,
tags: List<String>? = null,
duration: Int? = null,
trailers: MutableList<TrailerData> = mutableListOf(),
recommendations: List<SearchResponse>? = null,
actors: List<ActorData>? = null,
comingSoon: Boolean = false,
syncData: MutableMap<String, String> = mutableMapOf(),
posterHeaders: Map<String, String>? = null,
backgroundPosterUrl: String? = null,
) : this(
name, url, apiName, dataUrl, posterUrl, year, plot, type, rating, tags, duration, trailers,
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
)
}
data class MovieLoadResponse( data class MovieLoadResponse(
override var name: String, override var name: String,
@ -1623,36 +1599,7 @@ data class MovieLoadResponse(
override var syncData: MutableMap<String, String> = mutableMapOf(), override var syncData: MutableMap<String, String> = mutableMapOf(),
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null, ) : LoadResponse
) : LoadResponse {
/**
* Secondary constructor for backwards compatibility without contentRating.
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
*/
constructor(
name: String,
url: String,
apiName: String,
type: TvType,
dataUrl: String,
posterUrl: String? = null,
year: Int? = null,
plot: String? = null,
rating: Int? = null,
tags: List<String>? = null,
duration: Int? = null,
trailers: MutableList<TrailerData> = mutableListOf(),
recommendations: List<SearchResponse>? = null,
actors: List<ActorData>? = null,
comingSoon: Boolean = false,
syncData: MutableMap<String, String> = mutableMapOf(),
posterHeaders: Map<String, String>? = null,
backgroundPosterUrl: String? = null,
) : this(
name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
)
}
suspend fun <T> MainAPI.newMovieLoadResponse( suspend fun <T> MainAPI.newMovieLoadResponse(
name: String, name: String,
@ -1700,17 +1647,7 @@ suspend fun MainAPI.newMovieLoadResponse(
builder.initializer() builder.initializer()
return builder return builder
} }
/** Episode information that will be passed to LoadLinks function & showed on UI
* @property data string used as main LoadLinks fun parameter.
* @property name Name of the Episode.
* @property season Season number.
* @property episode Episode number.
* @property posterUrl URL of Episode's poster image.
* @property rating Episode rating.
* @property date Episode air date, see addDate.
* @property runTime Episode runtime in seconds.
* @see[addDate]
* */
data class Episode( data class Episode(
var data: String, var data: String,
var name: String? = null, var name: String? = null,
@ -1720,25 +1657,7 @@ data class Episode(
var rating: Int? = null, var rating: Int? = null,
var description: String? = null, var description: String? = null,
var date: Long? = null, var date: Long? = null,
var runTime: Int? = null, )
) {
/**
* Secondary constructor for backwards compatibility without runTime.
* TODO Remove this constructor after there is a new stable release and extensions are updated to support runTime.
*/
constructor(
data: String,
name: String? = null,
season: Int? = null,
episode: Int? = null,
posterUrl: String? = null,
rating: Int? = null,
description: String? = null,
date: Long? = null,
) : this(
data, name, season, episode, posterUrl, rating, description, date, null
)
}
fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") { fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
try { try {
@ -1780,28 +1699,6 @@ fun <T> MainAPI.newEpisode(
return builder return builder
} }
interface IDownloadableMinimum {
val url: String
val referer: String
val headers: Map<String, String>
}
fun IDownloadableMinimum.getId(): Int {
return url.hashCode()
}
/**
* Set of sync services simkl is compatible with.
* Add more as required: https://simkl.docs.apiary.io/#reference/search/id-lookup/get-items-by-id
*/
enum class SimklSyncServices(val originalName: String) {
Simkl("simkl"),
Imdb("imdb"),
Tmdb("tmdb"),
AniList("anilist"),
Mal("mal"),
}
data class TvSeriesLoadResponse( data class TvSeriesLoadResponse(
override var name: String, override var name: String,
override var url: String, override var url: String,
@ -1826,7 +1723,6 @@ data class TvSeriesLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null,
) : LoadResponse, EpisodeResponse { ) : LoadResponse, EpisodeResponse {
override fun getLatestEpisodes(): Map<DubStatus, Int?> { override fun getLatestEpisodes(): Map<DubStatus, Int?> {
val maxSeason = val maxSeason =
@ -1837,69 +1733,6 @@ data class TvSeriesLoadResponse(
.takeUnless { it == Int.MIN_VALUE } .takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max) return mapOf(DubStatus.None to max)
} }
override fun getTotalEpisodeIndex(episode: Int, season: Int): Int {
val displayMap = this.seasonNames?.associate { it.season to it.displaySeason } ?: emptyMap()
return episodes.count { episodeData ->
// Prioritize display season as actual season may be something random to fit multiple seasons into one.
val episodeSeason =
displayMap[episodeData.season] ?: episodeData.season ?: Int.MIN_VALUE
// Count all episodes from season 1 to below the current season.
episodeSeason in 1..<season
} + episode
}
/**
* Secondary constructor for backwards compatibility without contentRating.
* Remove this constructor after there is a new stable release and extensions are updated to support contentRating.
*/
constructor(
name: String,
url: String,
apiName: String,
type: TvType,
episodes: List<Episode>,
posterUrl: String? = null,
year: Int? = null,
plot: String? = null,
showStatus: ShowStatus? = null,
rating: Int? = null,
tags: List<String>? = null,
duration: Int? = null,
trailers: MutableList<TrailerData> = mutableListOf(),
recommendations: List<SearchResponse>? = null,
actors: List<ActorData>? = null,
comingSoon: Boolean = false,
syncData: MutableMap<String, String> = mutableMapOf(),
posterHeaders: Map<String, String>? = null,
nextAiring: NextAiring? = null,
seasonNames: List<SeasonData>? = null,
backgroundPosterUrl: String? = null,
) : this(
name,
url,
apiName,
type,
episodes,
posterUrl,
year,
plot,
showStatus,
rating,
tags,
duration,
trailers,
recommendations,
actors,
comingSoon,
syncData,
posterHeaders,
nextAiring,
seasonNames,
backgroundPosterUrl,
null
)
} }
suspend fun MainAPI.newTvSeriesLoadResponse( suspend fun MainAPI.newTvSeriesLoadResponse(
@ -1962,7 +1795,6 @@ data class AniSearch(
@JsonProperty("extraLarge") var extraLarge: String? = null, @JsonProperty("extraLarge") var extraLarge: String? = null,
@JsonProperty("large") var large: String? = null, @JsonProperty("large") var large: String? = null,
) )
data class Title( data class Title(
@JsonProperty("romaji") var romaji: String? = null, @JsonProperty("romaji") var romaji: String? = null,
@JsonProperty("english") var english: String? = null, @JsonProperty("english") var english: String? = null,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
object NativeCrashHandler {
// external fun triggerNativeCrash()
/*private external fun initNativeCrashHandler()
private external fun getSignalStatus(): Int
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
//launch {
// delay(10000)
// triggerNativeCrash()
//}
while (true) {
delay(10_000)
val signal = getSignalStatus()
// Signal is initialized to zero
if (signal == 0) continue
// Do not crash in safe mode!
if (lastError != null) continue
if (checkSafeModeFile()) continue
AcraApplication.exceptionHandler?.uncaughtException(
Thread.currentThread(),
RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
)
}
}
fun initCrashHandler() {
try {
System.loadLibrary("native-lib")
initNativeCrashHandler()
} catch (t: Throwable) {
// Make debug crash.
if (BuildConfig.DEBUG) throw t
logError(t)
return
}
initSignalPolling()
}*/
}

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.api.Log import android.util.Log
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink

View file

@ -0,0 +1,39 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.*
open class Acefile : ExtractorApi() {
override val name = "Acefile"
override val mainUrl = "https://acefile.co"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
app.get(url).document.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val data = getAndUnpack(script.data())
val id = data.substringAfter("{\"id\":\"").substringBefore("\",")
val key = data.substringAfter("var nfck=\"").substringBefore("\";")
app.get("https://acefile.co/local/$id?key=$key").text.let {
base64Decode(
it.substringAfter("JSON.parse(atob(\"").substringBefore("\"))")
).let { res ->
sources.add(
ExtractorLink(
name,
name,
res.substringAfter("\"file\":\"").substringBefore("\","),
"$mainUrl/",
Qualities.Unknown.value,
)
)
}
}
}
}
return sources
}
}

View file

@ -0,0 +1,90 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.extractors.helper.*
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class Moviesapi : Chillx() {
override val name = "Moviesapi"
override val mainUrl = "https://w1.moviesapi.club"
}
class Bestx : Chillx() {
override val name = "Bestx"
override val mainUrl = "https://bestx.stream"
}
class Watchx : Chillx() {
override val name = "Watchx"
override val mainUrl = "https://watchx.top"
}
open class Chillx : ExtractorApi() {
override val name = "Chillx"
override val mainUrl = "https://chillx.top"
override val requiresReferer = true
companion object {
private const val KEY = "m4H6D9%0\$N&F6rQ&"
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val master = Regex("MasterJS\\s*=\\s*'([^']+)").find(
app.get(
url,
referer = referer
).text
)?.groupValues?.get(1)
val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
// required
val headers = mapOf(
"Accept" to "*/*",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "cross-site",
"Origin" to mainUrl,
)
callback.invoke(
ExtractorLink(
name,
name,
source ?: return,
"$mainUrl/",
Qualities.P1080.value,
headers = headers,
isM3u8 = true
)
)
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
?.filter { it.kind == "captions" }?.map { track ->
subtitleCallback.invoke(
SubtitleFile(
track.label ?: "",
track.file ?: return@map null
)
)
}
}
data class Tracks(
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
@JsonProperty("kind") val kind: String? = null,
)
}

View file

@ -7,18 +7,13 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URL import java.net.URL
class Geodailymotion : Dailymotion() {
override val name = "GeoDailymotion"
override val mainUrl = "https://geo.dailymotion.com"
}
open class Dailymotion : ExtractorApi() { open class Dailymotion : ExtractorApi() {
override val mainUrl = "https://www.dailymotion.com" override val mainUrl = "https://www.dailymotion.com"
override val name = "Dailymotion" override val name = "Dailymotion"
override val requiresReferer = false override val requiresReferer = false
private val baseUrl = "https://www.dailymotion.com"
@Suppress("RegExpSimplifiable") @Suppress("RegExpSimplifiable")
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex() private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
@ -32,16 +27,21 @@ open class Dailymotion : ExtractorApi() {
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val embedUrl = getEmbedUrl(url) ?: return val embedUrl = getEmbedUrl(url) ?: return
val req = app.get(embedUrl) val doc = app.get(embedUrl).document
val prefix = "window.__PLAYER_CONFIG__ = " val prefix = "window.__PLAYER_CONFIG__ = "
val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
val config = tryParseJson<Config>(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
val id = getVideoId(embedUrl) ?: return val id = getVideoId(embedUrl) ?: return
val dmV1st = config.dmInternalData.v1st val dmV1st = config.dmInternalData.v1st
val dmTs = config.dmInternalData.ts val dmTs = config.dmInternalData.ts
val embedder = config.context.embedder val metaDataUrl =
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0" "$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies) val cookies = mapOf(
"v1st" to dmV1st,
"dmvk" to config.context.dmvk,
"ts" to dmTs.toString()
)
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
.parsedSafe<MetaData>() ?: return .parsedSafe<MetaData>() ?: return
metaData.qualities.forEach { (_, video) -> metaData.qualities.forEach { (_, video) ->
video.forEach { video.forEach {
@ -51,19 +51,16 @@ open class Dailymotion : ExtractorApi() {
} }
private fun getEmbedUrl(url: String): String? { private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/") || url.contains("/video/")) { if (url.contains("/embed/")) {
return url return url
}
val vid = getVideoId(url) ?: return null
return "$mainUrl/embed/video/$vid"
} }
if (url.contains("geo.dailymotion.com")) {
val videoId = url.substringAfter("video=")
return "$baseUrl/embed/video/$videoId"
}
return null
}
private fun getVideoId(url: String): String? { private fun getVideoId(url: String): String? {
val path = URL(url).path val path = URL(url).path
val id = path.substringAfter("/video/") val id = path.substringAfter("video/")
if (id.matches(videoIdRegex)) { if (id.matches(videoIdRegex)) {
return id return id
} }
@ -87,13 +84,13 @@ open class Dailymotion : ExtractorApi() {
) )
data class InternalData( data class InternalData(
val ts: Long, val ts: Int,
val v1st: String val v1st: String
) )
data class Context( data class Context(
@JsonProperty("access_token") val accessToken: String?, @JsonProperty("access_token") val accessToken: String?,
val embedder: String?, val dmvk: String,
) )
data class MetaData( data class MetaData(

View file

@ -7,18 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
class D0000d : DoodLaExtractor() {
override var mainUrl = "https://d0000d.com"
}
class D000dCom : DoodLaExtractor() {
override var mainUrl = "https://d000d.com"
}
class DoodstreamCom : DoodLaExtractor() {
override var mainUrl = "https://doodstream.com"
}
class Dooood : DoodLaExtractor() { class Dooood : DoodLaExtractor() {
override var mainUrl = "https://dooood.com" override var mainUrl = "https://dooood.com"
} }
@ -68,10 +56,9 @@ open class DoodLaExtractor : ExtractorApi() {
} }
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val newUrl= url.replace(mainUrl, "https://d0000d.com") val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/... val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/... val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0) val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
return listOf( return listOf(
ExtractorLink( ExtractorLink(

View file

@ -22,9 +22,9 @@ open class Gofile : ExtractorApi() {
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1)
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token") val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
Regex("fetchData.wt\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
} }
app.get("$mainApi/getContent?contentId=$id&token=$token&wt=$websiteToken") app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken")
.parsedSafe<Source>()?.data?.contents?.forEach { .parsedSafe<Source>()?.data?.contents?.forEach {
callback.invoke( callback.invoke(
ExtractorLink( ExtractorLink(

View file

@ -18,8 +18,7 @@ open class Linkbox : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val token = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1) val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
val id = app.get("$mainUrl/api/file/share_out_list/?sortField=utime&sortAsc=0&pageNo=1&pageSize=50&shareToken=$token").parsedSafe<Responses>()?.data?.itemId
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url) app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link -> .parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
callback.invoke( callback.invoke(
@ -45,7 +44,6 @@ open class Linkbox : ExtractorApi() {
data class Data( data class Data(
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null, @JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
@JsonProperty("itemId") val itemId: String? = null,
) )
data class Responses( data class Responses(

View file

@ -0,0 +1,67 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
data class DataOptionsJson (
@JsonProperty("flashvars") var flashvars : Flashvars? = Flashvars(),
)
data class Flashvars (
@JsonProperty("metadata") var metadata : String? = null,
@JsonProperty("hlsManifestUrl") var hlsManifestUrl : String? = null, //m3u8
)
data class MetadataOkru (
@JsonProperty("videos") var videos: ArrayList<Videos> = arrayListOf(),
)
data class Videos (
@JsonProperty("name") var name : String,
@JsonProperty("url") var url : String,
@JsonProperty("seekSchema") var seekSchema : Int? = null,
@JsonProperty("disallowed") var disallowed : Boolean? = null
)
class OkRuHttps: OkRu(){
override var mainUrl = "https://ok.ru"
}
open class OkRu : ExtractorApi() {
override var name = "Okru"
override var mainUrl = "http://ok.ru"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val doc = app.get(url).document
val sources = ArrayList<ExtractorLink>()
val datajson = doc.select("div[data-options]").attr("data-options")
if (datajson.isNotBlank()) {
val main = parseJson<DataOptionsJson>(datajson)
val metadatajson = parseJson<MetadataOkru>(main.flashvars?.metadata!!)
val servers = metadatajson.videos
servers.forEach {
val quality = it.name.uppercase()
.replace("MOBILE","144p")
.replace("LOWEST","240p")
.replace("LOW","360p")
.replace("SD","480p")
.replace("HD","720p")
.replace("FULL","1080p")
.replace("QUAD","1440p")
.replace("ULTRA","4k")
val extractedurl = it.url.replace("\\\\u0026", "&")
sources.add(ExtractorLink(
name,
name = this.name,
extractedurl,
url,
getQualityFromName(quality),
isM3u8 = false
))
}
}
return sources
}
}

View file

@ -0,0 +1,30 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
open class Pixeldrain : ExtractorApi() {
override val name = "Pixeldrain"
override val mainUrl = "https://pixeldrain.com"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val mId = Regex("/([ul]/[\\da-zA-Z\\-]+)").find(url)?.groupValues?.get(1)?.split("/")
callback.invoke(
ExtractorLink(
this.name,
this.name,
"$mainUrl/api/file/${mId?.last() ?: return}?download",
url,
Qualities.Unknown.value,
)
)
}
}

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.api.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*

View file

@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
@ -17,52 +16,13 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
// No License found in https://github.com/enimax-anime/key
// special credits to @enimax for providing key
class Megacloud : Rabbitstream() { class Megacloud : Rabbitstream() {
override val name = "Megacloud" override val name = "Megacloud"
override val mainUrl = "https://megacloud.tv" override val mainUrl = "https://megacloud.tv"
override val embed = "embed-2/ajax/e-1" override val embed = "embed-2/ajax/e-1"
private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js" override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt"
override suspend fun extractRealKey(sources: String): Pair<String, String> {
val rawKeys = getKeys()
val sourcesArray = sources.toCharArray()
var extractedKey = ""
var currentIndex = 0
for (index in rawKeys) {
val start = index[0] + currentIndex
val end = start + index[1]
for (i in start until end) {
extractedKey += sourcesArray[i].toString()
sourcesArray[i] = ' '
}
currentIndex += index[1]
}
return extractedKey to sourcesArray.joinToString("").replace(" ", "")
}
private suspend fun getKeys(): List<List<Int>> {
val script = app.get(scriptUrl).text
fun matchingKey(value: String): String {
return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1)
?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key")
}
val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
val indexPairs = regex.findAll(script).toList().map { match ->
val matchKey1 = matchingKey(match.groupValues[1])
val matchKey2 = matchingKey(match.groupValues[2])
try {
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
} catch (e: NumberFormatException) {
emptyList()
}
}.filter { it.isNotEmpty() }
return indexPairs
}
} }
class Dokicloud : Rabbitstream() { class Dokicloud : Rabbitstream() {
@ -70,14 +30,12 @@ class Dokicloud : Rabbitstream() {
override val mainUrl = "https://dokicloud.one" override val mainUrl = "https://dokicloud.one"
} }
// Code found in https://github.com/eatmynerds/key
// special credits to @eatmynerds for providing key
open class Rabbitstream : ExtractorApi() { open class Rabbitstream : ExtractorApi() {
override val name = "Rabbitstream" override val name = "Rabbitstream"
override val mainUrl = "https://rabbitstream.net" override val mainUrl = "https://rabbitstream.net"
override val requiresReferer = false override val requiresReferer = false
open val embed = "ajax/embed-4" open val embed = "ajax/embed-4"
open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt" open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"
override suspend fun getUrl( override suspend fun getUrl(
url: String, url: String,
@ -98,7 +56,7 @@ open class Rabbitstream : ExtractorApi() {
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) { val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
response.parsedSafe() response.parsedSafe()
} else { } else {
val (key, encData) = extractRealKey(sources) val (key, encData) = extractRealKey(sources, getRawKey())
val decrypted = decryptMapped<List<Sources>>(encData, key) val decrypted = decryptMapped<List<Sources>>(encData, key)
SourcesResponses( SourcesResponses(
sources = decrypted, sources = decrypted,
@ -117,8 +75,8 @@ open class Rabbitstream : ExtractorApi() {
decryptedSources?.tracks?.map { track -> decryptedSources?.tracks?.map { track ->
subtitleCallback.invoke( subtitleCallback.invoke(
SubtitleFile( SubtitleFile(
track?.label ?: return@map, track?.label ?: "",
track.file ?: return@map track?.file ?: return@map
) )
) )
} }
@ -126,10 +84,23 @@ open class Rabbitstream : ExtractorApi() {
} }
open suspend fun extractRealKey(sources: String): Pair<String, String> { private suspend fun getRawKey(): String = app.get(key).text
val rawKeys = parseJson<List<Int>>(app.get(key).text)
val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray()) private fun extractRealKey(originalString: String?, stops: String): Pair<String, String> {
return extractedKey to sources val table = parseJson<List<List<Int>>>(stops)
val decryptedKey = StringBuilder()
var offset = 0
var encryptedString = originalString
table.forEach { (start, end) ->
decryptedKey.append(encryptedString?.substring(start - offset, end - offset))
encryptedString = encryptedString?.substring(
0,
start - offset
) + encryptedString?.substring(end - offset)
offset += end - start
}
return decryptedKey.toString() to encryptedString.toString()
} }
private inline fun <reified T> decryptMapped(input: String, key: String): T? { private inline fun <reified T> decryptMapped(input: String, key: String): T? {

View file

@ -7,12 +7,21 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
open class Minoplres : ExtractorApi() { class SpeedoStream2 : SpeedoStream() {
override val mainUrl = "https://speedostream.mom"
}
override val name = "Minoplres" // formerly SpeedoStream class SpeedoStream1 : SpeedoStream() {
override val mainUrl = "https://speedostream.pm"
}
open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.bond"
override val requiresReferer = true override val requiresReferer = true
override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
private val hostUrl = "https://minoplres.xyz" // .bond, .pm, .mom redirect to .bond
private val hostUrl = "https://speedostream.bond"
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>() val sources = mutableListOf<ExtractorLink>()

View file

@ -9,10 +9,6 @@ class StreamTapeNet : StreamTape() {
override var mainUrl = "https://streamtape.net" override var mainUrl = "https://streamtape.net"
} }
class StreamTapeXyz : StreamTape() {
override var mainUrl = "https://streamtape.xyz"
}
class ShaveTape : StreamTape(){ class ShaveTape : StreamTape(){
override var mainUrl = "https://shavetape.cash" override var mainUrl = "https://shavetape.cash"
} }

View file

@ -13,7 +13,7 @@ data class Files(
open class Supervideo : ExtractorApi() { open class Supervideo : ExtractorApi() {
override var name = "Supervideo" override var name = "Supervideo"
override var mainUrl = "https://supervideo.cc" override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf() val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()

View file

@ -0,0 +1,100 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import kotlinx.coroutines.delay
import java.net.URI
class VidSrcExtractor2 : VidSrcExtractor() {
override val mainUrl = "https://vidsrc.me/embed"
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val newUrl = url.lowercase().replace(mainUrl, super.mainUrl)
super.getUrl(newUrl, referer, subtitleCallback, callback)
}
}
open class VidSrcExtractor : ExtractorApi() {
override val name = "VidSrc"
private val absoluteUrl = "https://v2.vidsrc.me"
override val mainUrl = "$absoluteUrl/embed"
override val requiresReferer = false
companion object {
/** Infinite function to validate the vidSrc pass */
suspend fun validatePass(url: String) {
val uri = URI(url)
val host = uri.host
// Basically turn https://tm3p.vidsrc.stream/ -> https://vidsrc.stream/
val referer = host.split(".").let {
val size = it.size
"https://" + it.subList(maxOf(0, size - 2), size).joinToString(".") + "/"
}
while (true) {
app.get(url, referer = referer)
delay(60_000)
}
}
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val iframedoc = app.get(url).document
val serverslist =
iframedoc.select("div#sources.button_content div#content div#list div").map {
val datahash = it.attr("data-hash")
if (datahash.isNotBlank()) {
val links = try {
app.get(
"$absoluteUrl/srcrcp/$datahash",
referer = "https://rcp.vidsrc.me/"
).url
} catch (e: Exception) {
""
}
links
} else ""
}
serverslist.amap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/prorcp")) {
val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
val passRegex = Regex("""['"](.*set_pass[^"']*)""")
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
Regex("""^//"""), "https://"
)
callback.invoke(
ExtractorLink(
this.name,
this.name,
srcm3u8,
"https://vidsrc.stream/",
Qualities.Unknown.value,
extractorData = pass,
isM3u8 = true
)
)
} else {
loadExtractor(linkfixed, url, subtitleCallback, callback)
}
}
}
}

View file

@ -25,13 +25,9 @@ open class Vidmoly : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit, subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
) { ) {
val headers = mapOf(
"User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
"Sec-Fetch-Dest" to "iframe"
)
val script = app.get( val script = app.get(
url, url,
headers = headers,
referer = referer, referer = referer,
).document.select("script") ).document.select("script")
.find { it.data().contains("sources:") }?.data() .find { it.data().contains("sources:") }?.data()

View file

@ -0,0 +1,36 @@
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 Tubeless : Voe() {
override var mainUrl = "https://tubelessceliolymph.com"
}
open class Voe : ExtractorApi() {
override val name = "Voe"
override val mainUrl = "https://voe.sx"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val res = app.get(url, referer = referer).document
val script = res.select("script").find { it.data().contains("sources =") }?.data()
val link = Regex("[\"']hls[\"']:\\s*[\"'](.*)[\"']").find(script ?: return)?.groupValues?.get(1)
M3u8Helper.generateM3u8(
name,
link ?: return,
"$mainUrl/",
headers = mapOf("Origin" to "$mainUrl/")
).forEach(callback)
}
}

View file

@ -70,18 +70,19 @@ open class YoutubeExtractor : ExtractorApi() {
} }
} }
ytVideos[url]?.mapNotNull { ytVideos[url]?.mapNotNull {
if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
ExtractorLink( ExtractorLink(
this.name, this.name,
this.name, this.name,
it.content ?: return@mapNotNull null, it.url ?: return@mapNotNull null,
"", "",
it.height it.height
) )
}?.forEach(callback) }?.forEach(callback)
ytVideosSubtitles[url]?.mapNotNull { ytVideosSubtitles[url]?.mapNotNull {
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.content ?: return@mapNotNull null) SubtitleFile(it.languageTag ?: return@mapNotNull null, it.url ?: return@mapNotNull null)
}?.forEach(subtitleCallback) }?.forEach(subtitleCallback)
} }
} }

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.extractors.helper package com.lagradost.cloudstream3.extractors.helper
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.base64DecodeArray import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
@ -22,12 +23,7 @@ object AesHelper {
padding: String = HASH, padding: String = HASH,
): String? { ): String? {
val parse = AppUtils.tryParseJson<AesData>(data) ?: return null val parse = AppUtils.tryParseJson<AesData>(data) ?: return null
val (key, iv) = generateKeyAndIv( val (key, iv) = generateKeyAndIv(pass, parse.s.hexToByteArray()) ?: return null
pass,
parse.s.hexToByteArray(),
ivLength = parse.iv.length / 2,
saltLength = parse.s.length / 2
) ?: return null
val cipher = Cipher.getInstance(padding) val cipher = Cipher.getInstance(padding)
return if (!encrypt) { return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv)) cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
@ -44,8 +40,7 @@ object AesHelper {
salt: ByteArray, salt: ByteArray,
hashAlgorithm: String = KDF, hashAlgorithm: String = KDF,
keyLength: Int = 32, keyLength: Int = 32,
ivLength: Int, ivLength: Int = 16,
saltLength: Int,
iterations: Int = 1 iterations: Int = 1
): Pair<ByteArray,ByteArray>? { ): Pair<ByteArray,ByteArray>? {
@ -68,7 +63,7 @@ object AesHelper {
) )
md.update(password) md.update(password)
md.update(salt, 0, saltLength) md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength) md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) { for (i in 1 until iterations) {

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors.helper package com.lagradost.cloudstream3.extractors.helper
import com.lagradost.api.Log import android.util.Log
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app

View file

@ -1,6 +1,8 @@
package com.lagradost.cloudstream3.extractors.helper package com.lagradost.cloudstream3.extractors.helper
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
class WcoHelper { class WcoHelper {
@ -28,7 +30,9 @@ class WcoHelper {
private suspend fun getKeys() { private suspend fun getKeys() {
keys = keys keys = keys
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json") ?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
.parsedSafe<ExternalKeys>() .parsedSafe<ExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
BACKUP_KEY_DATA
)
} }
suspend fun getWcoKey(): ExternalKeys? { suspend fun getWcoKey(): ExternalKeys? {
@ -39,7 +43,9 @@ class WcoHelper {
private suspend fun getNewKeys() { private suspend fun getNewKeys() {
newKeys = newKeys newKeys = newKeys
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json") ?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
.parsedSafe<NewExternalKeys>() .parsedSafe<NewExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
BACKUP_KEY_DATA
)
} }
suspend fun getNewWcoKey(): NewExternalKeys? { suspend fun getNewWcoKey(): NewExternalKeys? {

View file

@ -0,0 +1,73 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.LoadResponse.Companion.addAniListId
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.utils.SyncUtil
// wont be implemented
class MultiAnimeProvider : MainAPI() {
override var name = "MultiAnime"
override var lang = "en"
override val usesWebView = true
override val supportedTypes = setOf(TvType.Anime)
private val syncApi: SyncAPI = aniListApi
private val syncUtilType by lazy {
when (syncApi) {
is AniListApi -> "anilist"
is MALApi -> "myanimelist"
else -> throw ErrorLoadingException("Invalid Api")
}
}
private val validApis
get() =
synchronized(APIHolder.apis) {
APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime
)
}
}
private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
}
override suspend fun search(query: String): List<SearchResponse>? {
return syncApi.search(query)?.map {
AnimeSearchResponse(it.name, it.url, this.name, TvType.Anime, it.posterUrl)
}
}
override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull()
val type =
if (data.any { it.type == TvType.AnimeMovie }) TvType.AnimeMovie else TvType.Anime
newAnimeLoadResponse(
res.title ?: throw ErrorLoadingException("No Title found"),
url,
type
) {
posterUrl = res.posterUrl
plot = res.synopsis
tags = res.genres
rating = res.publicScore
addTrailer(res.trailers)
addAniListId(res.id.toIntOrNull())
recommendations = res.recommendations
}
}
}
}

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