diff --git a/.devcontainer.json b/.devcontainer.json
new file mode 100644
index 00000000..bed109a0
--- /dev/null
+++ b/.devcontainer.json
@@ -0,0 +1,17 @@
+{
+ "image": "mcr.microsoft.com/devcontainers/universal:2",
+ "postCreateCommand": "sudo chown $(whoami) /opt/android/",
+ "features": {
+ "ghcr.io/devcontainers/features/java:latest": {
+ "version": "17"
+ },
+ "ghcr.io/akhildevelops/devcontainer-features/android-cli:0": {}
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "fwcd.kotlin"
+ ]
+ }
+ }
+}
diff --git a/.github/ISSUE_TEMPLATE/application-bug.yml b/.github/ISSUE_TEMPLATE/application-bug.yml
index f3590067..931db3bd 100644
--- a/.github/ISSUE_TEMPLATE/application-bug.yml
+++ b/.github/ISSUE_TEMPLATE/application-bug.yml
@@ -80,13 +80,13 @@ body:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
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.
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
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.
required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index b56cdf8e..250734cd 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links:
- name: Request a new provider or report bug with an existing provider
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
url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
index e18daebb..9c35ba56 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -27,7 +27,9 @@ body:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
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.
- required: true
\ No newline at end of file
+ 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
diff --git a/.github/locales.py b/.github/locales.py
index a74d7258..7d6d6b90 100644
--- a/.github/locales.py
+++ b/.github/locales.py
@@ -1,7 +1,6 @@
import re
import glob
import requests
-import os
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:
tree = ET.parse(file)
for child in tree.getroot():
- if not child.text:
- continue
if child.text.startswith("\\@string/"):
print(f"[{file}] fixing {child.attrib['name']}")
child.text = child.text.replace("\\@string/", "@string/")
with open(file, 'wb') as fp:
fp.write(b'\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
- # Remove trailing new line to be consistent with weblate
- fp.seek(-1, os.SEEK_END)
- fp.truncate()
except ET.ParseError as ex:
print(f"[{file}] {ex}")
diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml
index e84bb08b..3b7aa9ae 100644
--- a/.github/workflows/build_to_archive.yml
+++ b/.github/workflows/build_to_archive.yml
@@ -19,21 +19,21 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- name: Generate access token (archive)
id: generate_archive_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive"
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v2
- name: Set up JDK 17
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
@@ -58,7 +58,7 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v3
with:
repository: "recloudstream/cloudstream-archive"
token: ${{ steps.generate_archive_token.outputs.token }}
diff --git a/.github/workflows/generate_dokka.yml b/.github/workflows/generate_dokka.yml
index 96e61644..abeee0b2 100644
--- a/.github/workflows/generate_dokka.yml
+++ b/.github/workflows/generate_dokka.yml
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
@@ -43,13 +43,12 @@ jobs:
rm -rf "./-cloudstream"
- name: Setup JDK 17
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v1
with:
java-version: 17
- distribution: 'adopt'
- name: Setup Android SDK
- uses: android-actions/setup-android@v3
+ uses: android-actions/setup-android@v2
- name: Generate Dokka
run: |
diff --git a/.github/workflows/issue_action.yml b/.github/workflows/issue_action.yml
index 88ab3656..108cec82 100644
--- a/.github/workflows/issue_action.yml
+++ b/.github/workflows/issue_action.yml
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
@@ -27,7 +27,7 @@ jobs:
comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true'
- uses: actions/github-script@v7
+ uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
@@ -37,7 +37,7 @@ jobs:
repo: context.repo.repo,
labels: ["possible duplicate"]
})
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v2
- name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2
with:
@@ -68,7 +68,7 @@ jobs:
Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
- uses: actions/github-script@v7
+ uses: actions/github-script@v6
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml
index f35cd58c..58009a7a 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -18,14 +18,14 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v2
- name: Set up JDK 17
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
@@ -43,8 +43,7 @@ jobs:
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
- ./gradlew assemblePrerelease build androidSourcesJar
- ./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
+ ./gradlew assemblePrerelease makeJar androidSourcesJar
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 7f6dd412..b6177710 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -6,9 +6,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v2
- name: Set up JDK 17
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v2
with:
java-version: '17'
distribution: 'adopt'
@@ -17,7 +17,7 @@ jobs:
- name: Run Gradle
run: ./gradlew assemblePrereleaseDebug
- name: Upload Artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v2
with:
name: pull-request-build
path: "app/build/outputs/apk/prerelease/debug/*.apk"
diff --git a/.github/workflows/update_locales.yml b/.github/workflows/update_locales.yml
index ce140e55..628e9bc9 100644
--- a/.github/workflows/update_locales.yml
+++ b/.github/workflows/update_locales.yml
@@ -18,12 +18,12 @@ jobs:
steps:
- name: Generate access token
id: generate_token
- uses: tibdex/github-app-token@v2
+ uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream"
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v2
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Install dependencies
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d7c08c9c..a8a2961a 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,16 +4,16 @@
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d0c86bab..333fbfb8 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,14 +1,12 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
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.net.URL
plugins {
id("com.android.application")
- id("com.google.devtools.ksp")
id("kotlin-android")
+ id("kotlin-kapt")
id("org.jetbrains.dokka")
}
@@ -34,16 +32,16 @@ android {
enable = true
}
- /* disable this for now
- externalNativeBuild {
- cmake {
- path("CMakeLists.txt")
- }
- }*/
+ // disable this for now
+ //externalNativeBuild {
+ // cmake {
+ // path("CMakeLists.txt")
+ // }
+ //}
signingConfigs {
- if (prereleaseStoreFile != null) {
- create("prerelease") {
+ create("prerelease") {
+ if (prereleaseStoreFile != null) {
storeFile = file(prereleaseStoreFile)
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
@@ -52,16 +50,16 @@ android {
}
}
- compileSdk = 34
- buildToolsVersion = "34.0.0"
+ compileSdk = 33
+ buildToolsVersion = "30.0.3"
defaultConfig {
applicationId = "com.lagradost.cloudstream3"
minSdk = 21
- targetSdk = 33 /* Android 14 is Fu*ked
- ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
- versionCode = 64
- versionName = "4.4.0"
+ targetSdk = 29
+
+ versionCode = 59
+ versionName = "4.1.8"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@@ -71,9 +69,9 @@ android {
val localProperties = gradleLocalProperties(rootDir)
buildConfigField(
- "long",
- "BUILD_DATE",
- "${System.currentTimeMillis()}"
+ "String",
+ "BUILDDATE",
+ "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
)
buildConfigField(
"String",
@@ -87,9 +85,8 @@ android {
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- ksp {
- arg("room.schemaLocation", "$projectDir/schemas")
- arg("exportSchema", "true")
+ kapt {
+ includeCompileClasspath = true
}
}
@@ -112,7 +109,6 @@ android {
)
}
}
-
flavorDimensions.add("state")
productFlavors {
create("stable") {
@@ -124,31 +120,30 @@ android {
resValue("bool", "is_prerelease", "true")
buildConfigField("boolean", "BETA", "true")
applicationIdSuffix = ".prerelease"
- if (signingConfigs.names.contains("prerelease")) {
- signingConfig = signingConfigs.getByName("prerelease")
- } else {
- logger.warn("No prerelease signing config!")
- }
+ signingConfig = signingConfigs.getByName("prerelease")
versionNameSuffix = "-PRE"
versionCode = (System.currentTimeMillis() / 60000).toInt()
}
}
+ //toolchain {
+ // languageVersion.set(JavaLanguageVersion.of(17))
+ // }
+ // jvmToolchain(17)
compileOptions {
isCoreLibraryDesugaringEnabled = true
+
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
-
+ kotlinOptions {
+ jvmTarget = "1.8"
+ freeCompilerArgs = listOf("-Xjvm-default=compatibility")
+ }
lint {
abortOnError = false
checkReleaseBuilds = false
}
-
- buildFeatures {
- buildConfig = true
- }
-
namespace = "com.lagradost.cloudstream3"
}
@@ -157,132 +152,127 @@ repositories {
}
dependencies {
- // Testing
- testImplementation("junit:junit:4.13.2")
- testImplementation("org.json:json:20240303")
- 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")
+ implementation("com.google.android.mediahome:video:1.0.0")
+ implementation("androidx.test.ext:junit-ktx:1.1.5")
+ testImplementation("org.json:json:20180813")
- // Android Core & Lifecycle
- implementation("androidx.core:core-ktx:1.13.1")
- 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")
+ implementation("androidx.core:core-ktx:1.10.1")
+ implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
- // Design & UI
- implementation("jp.wasabeef:glide-transformations:4.3.0")
- implementation("androidx.preference:preference-ktx:1.2.1")
- implementation("com.google.android.material:material:1.12.0")
+ // dont change this to 1.6.0 it looks ugly af
+ implementation("com.google.android.material:material:1.5.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.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")
- // Glide Module
- 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")
+ // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
- // For KSP -> Official Annotation Processors are Not Yet Supported for KSP
- 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")
+ // Media 3
implementation("androidx.media3:media3-common:1.1.1")
- implementation("androidx.media3:media3-session:1.1.1")
implementation("androidx.media3:media3-exoplayer:1.1.1")
- implementation("com.google.android.mediahome:video:1.0.0")
+ implementation("androidx.media3:media3-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-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.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
+ //implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
- // Crash Reports (AcraApplication.kt)
- implementation("ch.acra:acra-core:5.11.3")
- implementation("ch.acra:acra-toast:5.11.3")
+ // Bug reports
+ implementation("ch.acra:acra-core:5.11.0")
+ 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.2")
+
+ // 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("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
- implementation("org.mozilla:rhino:1.7.15") // run JavaScript
- 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. */
+ // used for subtitle decoding https://github.com/albfernandez/juniversalchardet
+ implementation("com.github.albfernandez:juniversalchardet:2.4.0")
- // Downloading & Networking
- implementation("androidx.work:work-runtime:2.9.0")
- implementation("androidx.work:work-runtime-ktx:2.9.0")
- implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
+ // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
+ // this should be updated frequently to avoid trailer fu*kery
+ implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
- implementation(project(":library") {
- // There does not seem to be a good way of getting the android flavor.
- val isDebug = gradle.startParameter.taskRequests.any { task ->
- task.args.any { arg ->
- arg.contains("debug", true)
- }
- }
+ // Library/extensions searching with Levenshtein distance
+ implementation("me.xdrop:fuzzywuzzy:1.4.0")
- this.extra.set("isDebug", isDebug)
- })
+ // color palette for images -> colors
+ implementation("androidx.palette:palette-ktx:1.0.0")
}
-tasks.register("androidSourcesJar") {
+tasks.register("androidSourcesJar", Jar::class) {
archiveClassifier.set("sources")
- from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
+ from(android.sourceSets.getByName("main").java.srcDirs) //full sources
}
-tasks.register("copyJar") {
- from(
- "build/intermediates/compile_app_classes_jar/prereleaseDebug",
- "../library/build/libs"
- )
- into("build/app-classes")
- 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("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 {
- kotlinOptions {
- jvmTarget = "1.8"
- freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
- }
+// this is used by the gradlew plugin
+tasks.register("makeJar", Copy::class) {
+ from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
+ into("build")
+ include("classes.jar")
+ dependsOn("build")
}
tasks.withType().configureEach {
@@ -295,7 +285,6 @@ tasks.withType().configureEach {
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
-
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
index c7f02baf..df41ef91 100644
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
@@ -9,8 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
-import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
-import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
@@ -19,7 +17,6 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
-import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
@@ -120,12 +117,9 @@ class ExampleInstrumentedTest {
// testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
// testAllLayouts(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
- testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
- testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
- testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
+ testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
+ testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
- testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
- testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
}
}
}
@@ -154,7 +148,7 @@ class ExampleInstrumentedTest {
fun providerCorrectHomepage() {
runBlocking {
getAllProviders().toList().amap { api ->
- TestingUtils.testHomepage(api, TestingUtils.Logger())
+ TestingUtils.testHomepage(api, ::println)
}
}
println("Done providerCorrectHomepage")
@@ -166,6 +160,7 @@ class ExampleInstrumentedTest {
TestingUtils.getDeferredProviderTests(
this,
getAllProviders(),
+ ::println
) { _, _ -> }
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 888be999..15767d7b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,7 +6,7 @@
-
+
@@ -14,14 +14,8 @@
-
-
-
-
-
+
+ tools:targetApi="o">
+ android:supportsPictureInPicture="true">
@@ -97,11 +87,17 @@
-->
+
+
+
+
+
+
@@ -165,21 +161,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
@@ -187,14 +168,13 @@
-
+ android:exported="true">
+
@@ -204,7 +184,6 @@
android:exported="false" />
diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
index d6f978fe..5f3162b4 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
@@ -8,14 +8,12 @@ import android.content.Intent
import android.widget.Toast
import androidx.fragment.app.Fragment
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.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
-import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
-import com.lagradost.cloudstream3.ui.settings.Globals.TV
-import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
-import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
+import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
@@ -34,11 +32,12 @@ import org.acra.sender.ReportSenderFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.PrintStream
+import java.lang.Exception
import java.lang.ref.WeakReference
-import java.util.Locale
import kotlin.concurrent.thread
import kotlin.system.exitProcess
+
class CustomReportSender : ReportSender {
// Sends all your crashes to google forms
override fun send(context: Context, errorContent: CrashReportData) {
@@ -66,6 +65,7 @@ class CustomReportSender : ReportSender {
}
}
+@AutoService(ReportSenderFactory::class)
class CustomSenderFactory : ReportSenderFactory {
override fun create(context: Context, config: CoreConfiguration): ReportSender {
return CustomReportSender()
@@ -82,8 +82,14 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
ACRA.errorReporter.handleException(error)
try {
PrintStream(errorFile).use { ps ->
- ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
- ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
+ ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
+ ps.println(
+ String.format(
+ "Fatal exception on thread %s (%d)",
+ thread.name,
+ thread.id
+ )
+ )
error.printStackTrace(ps)
}
} catch (ignored: FileNotFoundException) {
@@ -101,6 +107,7 @@ class AcraApplication : Application() {
override fun onCreate() {
super.onCreate()
+ //NativeCrashHandler.initCrashHandler()
ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
@@ -146,7 +153,6 @@ class AcraApplication : Application() {
get() = _context?.get()
private set(value) {
_context = WeakReference(value)
- setContext(WeakReference(value))
}
fun getKeyClass(path: String, valueType: Class): T? {
@@ -208,7 +214,7 @@ class AcraApplication : Application() {
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
- isLayout(TV or EMULATOR),
+ isTvSettings(),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index ee3a5d12..0bcd4152 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -5,16 +5,12 @@ import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import android.content.pm.PackageManager
-import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
-import android.util.DisplayMetrics
import android.util.Log
-import android.view.Gravity
-import android.view.KeyEvent
-import android.view.View
+import android.view.*
import android.view.View.NO_ID
-import android.view.ViewGroup
+import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@@ -30,14 +26,12 @@ import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
-import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
-import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
-import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
-import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
+import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
@@ -46,9 +40,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference
-import java.util.Locale
-import kotlin.math.max
-import kotlin.math.min
+import java.util.*
enum class FocusDirection {
Start,
@@ -66,29 +58,11 @@ object CommonActivity {
_activity = WeakReference(value)
}
- @MainThread
- fun setActivityInstance(newActivity: Activity?) {
- activity = newActivity
- }
-
@MainThread
fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
}
- val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
-
- // screenWidth and screenHeight does always
- // refer to the screen while in landscape mode
- val screenWidth: Int
- get() {
- return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
- }
- val screenHeight: Int
- get() {
- return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
- }
-
var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false
@@ -100,7 +74,8 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair) -> Boolean)? = null
- private var currentToast: Toast? = null
+
+ var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return
@@ -156,19 +131,25 @@ object CommonActivity {
} catch (e: Exception) {
logError(e)
}
-
try {
- val binding = ToastBinding.inflate(act.layoutInflater)
- binding.text.text = message.trim()
+ val inflater =
+ act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+
+ val layout: View = inflater.inflate(
+ R.layout.toast,
+ act.findViewById(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)
- toast.duration = duration ?: Toast.LENGTH_SHORT
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.
- currentToast = toast
+ toast.duration = duration ?: Toast.LENGTH_SHORT
+ toast.view = layout
+ //https://github.com/PureWriter/ToastCompat
toast.show()
-
+ currentToast = toast
} catch (e: Exception) {
logError(e)
}
@@ -202,25 +183,23 @@ object CommonActivity {
setLocale(this, localeCode)
}
- fun init(act: Activity) {
- setActivityInstance(act)
-
- val componentActivity = activity as? ComponentActivity ?: return
-
+ fun init(act: ComponentActivity?) {
+ if (act == null) return
+ activity = act
//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
canShowPipMode =
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
- componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
+ act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
+ act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
- componentActivity.updateLocale()
- componentActivity.updateTv()
+ act.updateLocale()
+ act.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance())
for (resumeApp in resumeApps) {
resumeApp.launcher =
- componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
@@ -237,11 +216,11 @@ object CommonActivity {
// Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
- componentActivity,
+ act,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
- val requestPermissionLauncher = componentActivity.registerForActivityResult(
+ val requestPermissionLauncher = act.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted")
@@ -277,35 +256,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?) {
if (act == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
val currentTheme =
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
- "System" -> mapSystemTheme(act)
"Black" -> R.style.AppTheme
"Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode
@@ -319,15 +275,12 @@ object CommonActivity {
val currentOverlayTheme =
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
"Normal" -> R.style.OverlayPrimaryColorNormal
- "DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
- "Orange" -> R.style.OverlayPrimaryColorOrange
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
"Maroon" -> R.style.OverlayPrimaryColorMaroon
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
"Grey" -> R.style.OverlayPrimaryColorGrey
"White" -> R.style.OverlayPrimaryColorWhite
- "CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
"Brown" -> R.style.OverlayPrimaryColorBrown
"Purple" -> R.style.OverlayPrimaryColorPurple
"Green" -> R.style.OverlayPrimaryColorGreen
@@ -336,7 +289,6 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink
- "Lavender" -> R.style.OverlayPrimaryColorLavender
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
@@ -376,14 +328,6 @@ object CommonActivity {
currentLook = currentLook.parent as? View ?: break
}*/
- private fun View.hasContent(): Boolean {
- return isShown && when (this) {
- //is RecyclerView -> this.childCount > 0
- is ViewGroup -> this.childCount > 0
- else -> true
- }
- }
-
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
fun continueGetNextFocus(
root: Any?,
@@ -404,17 +348,16 @@ object CommonActivity {
} ?: return null
next = localLook(view, nextId) ?: next
- val shown = next.hasContent()
// if cant focus but visible then break and let android decide
// the exception if is the view is a parent and has children that wants focus
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
} ?: false
- if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
+ if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null
// if not shown then continue because we will "skip" over views to get to a replacement
- if (!shown) {
+ if (!next.isShown) {
// we don't want a while true loop, so we let android decide if we find a recursive view
if (next == view) return null
return getNextFocus(root, next, direction, depth + 1)
@@ -488,6 +431,20 @@ object CommonActivity {
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
when (keyCode) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
index 8da7ca38..0a2db2bd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
@@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
import okhttp3.OkHttpClient
import okhttp3.RequestBody
-import okhttp3.RequestBody.Companion.toRequestBody
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response
@@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
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 {
val httpMethod: String = request.httpMethod()
val url: String = request.url()
@@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
val dataToSend: ByteArray? = request.dataToSend()
var requestBody: RequestBody? = null
if (dataToSend != null) {
- requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
+ requestBody = RequestBody.create(null, dataToSend)
}
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)
@@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
return instance
}
}
+
+ init {
+ client = builder.readTimeout(30, TimeUnit.SECONDS).build()
+ }
}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
similarity index 74%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
rename to app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index 50dd667b..80332445 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -1,43 +1,46 @@
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.databind.DeserializationFeature
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.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.providers.SimklApi
+import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.*
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.threadSafeListOf
-import com.lagradost.nicehttp.RequestBodyTypes
import okhttp3.Interceptor
-import okhttp3.MediaType.Companion.toMediaTypeOrNull
-import okhttp3.RequestBody.Companion.toRequestBody
-import java.net.URI
+import org.mozilla.javascript.Scriptable
import java.text.SimpleDateFormat
import java.util.*
-import kotlin.io.encoding.Base64
-import kotlin.io.encoding.ExperimentalEncodingApi
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
* the equivalent of all languages being set
**/
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 {
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
@@ -108,6 +111,15 @@ object APIHolder {
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
* discovered originally by https://github.com/ahmedgamal17
@@ -123,9 +135,10 @@ object APIHolder {
// To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try {
- val uri = URI.create(url)
- val domain = base64Encode(
+ val uri = Uri.parse(url)
+ val domain = encodeToString(
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
+ 0
).replace("\n", "").replace("=", ".")
val vToken =
@@ -164,17 +177,10 @@ object APIHolder {
private var trackerCache: HashMap = hashMapOf()
- /** backwards compatibility, use getTracker4 instead */
- suspend fun getTracker(
- titles: List,
- types: Set?,
- year: Int?,
- ): Tracker? = getTracker(titles, types, year, false)
-
/**
* Get anime tracker information based on title, year and type.
* Both titles are attempted to be matched with both Romaji and English title.
- * Uses the anilist api.
+ * Uses the consumet api.
*
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
@@ -183,8 +189,7 @@ object APIHolder {
suspend fun getTracker(
titles: List,
types: Set?,
- year: Int?,
- lessAccurate: Boolean
+ year: Int?
): Tracker? {
return try {
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
@@ -192,73 +197,187 @@ object APIHolder {
val mainTitle = titles[0]
val search =
trackerCache[mainTitle]
- ?: searchAnilist(mainTitle)?.also {
- trackerCache[mainTitle] = it
- } ?: return null
+ ?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
+ .parsedSafe()?.also {
+ trackerCache[mainTitle] = it
+ } ?: return null
- val res = search.data?.page?.media?.find { media ->
- val matchingYears = year == null || media.seasonYear == year
+ val res = search.results?.find { media ->
+ val matchingYears = year == null || media.releaseDate == year
val matchingTitles = media.title?.let { title ->
titles.any { userTitle ->
title.isMatchingTitles(userTitle)
}
} ?: false
- val matchingTypes = types?.any { it.name.equals(media.format, true) } == true
- if (lessAccurate) matchingTitles || matchingTypes && matchingYears else matchingTitles && matchingTypes && matchingYears
+ val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
+ matchingTitles && matchingTypes && matchingYears
} ?: return null
- Tracker(
- res.idMal,
- res.id.toString(),
- res.coverImage?.extraLarge ?: res.coverImage?.large,
- res.bannerImage
- )
+ Tracker(res.malId, res.aniId, res.image, res.cover)
} catch (t: Throwable) {
logError(t)
null
}
}
- private suspend fun searchAnilist(
- title: String?,
- ): AniSearch? {
- val query = """
- query (
- ${'$'}page: Int = 1
- ${'$'}search: String
- ${'$'}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]
- ${'$'}type: MediaType
- ) {
- Page(page: ${'$'}page, perPage: 20) {
- media(
- search: ${'$'}search
- sort: ${'$'}sort
- type: ${'$'}type
- ) {
- id
- idMal
- title { romaji english }
- coverImage { extraLarge large }
- bannerImage
- seasonYear
- format
+
+ fun Context.getApiSettings(): HashSet {
+ //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+
+ val hashSet = HashSet()
+ 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()
+ 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 {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ val hashSet = HashSet()
+ 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 {
+ 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 {
+ val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
+ val hashSet = HashSet()
+ 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 {
+ // 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) }
}
- """.trimIndent().trim()
+ return if (currentPrefMedia.isEmpty()) {
+ allApis
+ } else {
+ // Filter API depending on preferred media type
+ allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } }
+ }
+ }
- val data = mapOf(
- "query" to query,
- "variables" to mapOf(
- "search" to title,
- "sort" to "SEARCH_MATCH",
- "type" to "ANIME",
- )
- ).toJson().toRequestBody(RequestBodyTypes.JSON.toMediaTypeOrNull())
+ fun Context.filterSearchResultByFilmQuality(data: List): List {
+ // 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
+ }
- return app.post("https://graphql.anilist.co", requestBody = data)
- .parsedSafe()
+ 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 +567,7 @@ abstract class MainAPI {
/**Used for testing and can be used to disable the providers if WebView is not available*/
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
open val hasMainPage = false
@@ -482,7 +601,7 @@ abstract class MainAPI {
//emptyList() //
open val mainPage = listOf(MainPageData("", "", false))
- // @WorkerThread
+ @WorkerThread
open suspend fun getMainPage(
page: Int,
request: MainPageRequest,
@@ -490,17 +609,17 @@ abstract class MainAPI {
throw NotImplementedError()
}
- // @WorkerThread
+ @WorkerThread
open suspend fun search(query: String): List? {
throw NotImplementedError()
}
- // @WorkerThread
+ @WorkerThread
open suspend fun quickSearch(query: String): List? {
throw NotImplementedError()
}
- // @WorkerThread
+ @WorkerThread
/**
* Based on data from search() or getMainPage() it generates a LoadResponse,
* basically opening the info page from a link.
@@ -518,13 +637,13 @@ abstract class MainAPI {
* This function might be updated to include exoplayer timestamps etc in the future
* if the need arises.
* */
- // @WorkerThread
+ @WorkerThread
open suspend fun extractorVerifierJob(extractorData: String?) {
throw NotImplementedError()
}
/**Callback is fired once a link is found, will return true if method is executed successfully*/
- // @WorkerThread
+ @WorkerThread
open suspend fun loadLinks(
data: String,
isCasting: Boolean,
@@ -549,18 +668,31 @@ abstract class MainAPI {
}
/** Might need a different implementation for desktop*/
+@SuppressLint("NewApi")
fun base64Decode(string: String): String {
return String(base64DecodeArray(string), Charsets.ISO_8859_1)
}
-@OptIn(ExperimentalEncodingApi::class)
+
+@SuppressLint("NewApi")
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 {
- 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? {
if (url.isNullOrEmpty()) {
return null
@@ -594,6 +726,10 @@ fun sortUrls(urls: Set): List {
return urls.sortedBy { t -> -t.quality }
}
+fun sortSubs(subs: Set): List {
+ return subs.sortedBy { it.name }
+}
+
fun capitalizeString(str: String): String {
return capitalizeStringNullable(str) ?: str
}
@@ -677,12 +813,7 @@ enum class TvType(value: Int?) {
AsianDrama(9),
Live(10),
NSFW(11),
- Others(12),
- Music(13),
- AudioBook(14),
-
- /** Wont load the built in player, make your own interaction */
- CustomMedia(15),
+ Others(12)
}
public enum class AutoDownloadMode(val value: Int) {
@@ -1012,28 +1143,13 @@ interface LoadResponse {
var syncData: MutableMap
var posterHeaders: Map?
var backgroundPosterUrl: String?
- var contentRating: String?
companion object {
- var malIdPrefix = "" //malApi.idPrefix
- var aniListIdPrefix = "" //aniListApi.idPrefix
- var simklIdPrefix = "" //simklApi.idPrefix
+ private val malIdPrefix = malApi.idPrefix
+ private val aniListIdPrefix = aniListApi.idPrefix
+ private val simklIdPrefix = simklApi.idPrefix
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 {
- return tryParseJson(idString) ?: return emptyMap()
- }
-
fun LoadResponse.isMovie(): Boolean {
return this.type.isMovieType() || this is MovieLoadResponse
}
@@ -1057,12 +1173,12 @@ interface LoadResponse {
* Internal helper function to add simkl ids from other databases.
*/
private fun LoadResponse.addSimklId(
- database: SimklSyncServices,
+ database: SimklApi.Companion.SyncServices,
id: String?
) {
normalSafeApiCall {
this.syncData[simklIdPrefix] =
- addIdToString(this.syncData[simklIdPrefix], database, id.toString())
+ SimklApi.addIdToString(this.syncData[simklIdPrefix], database, id.toString())
?: return@normalSafeApiCall
}
}
@@ -1080,30 +1196,18 @@ interface LoadResponse {
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?) {
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?) {
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?) {
- this.addSimklId(SimklSyncServices.Simkl, id.toString())
+ this.addSimklId(SimklApi.Companion.SyncServices.Simkl, id.toString())
}
fun LoadResponse.addImdbUrl(url: String?) {
@@ -1185,7 +1289,7 @@ interface LoadResponse {
fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync
- this.addSimklId(SimklSyncServices.Imdb, id)
+ this.addSimklId(SimklApi.Companion.SyncServices.Imdb, id)
}
fun LoadResponse.addTrackId(id: String?) {
@@ -1198,7 +1302,7 @@ interface LoadResponse {
fun LoadResponse.addTMDbId(id: String?) {
// TODO add TMDb sync
- this.addSimklId(SimklSyncServices.Tmdb, id)
+ this.addSimklId(SimklApi.Companion.SyncServices.Tmdb, id)
}
fun LoadResponse.addRating(text: String?) {
@@ -1277,24 +1381,11 @@ fun TvType?.isEpisodeBased(): Boolean {
return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
}
+
data class NextAiring(
val episode: Int,
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
@@ -1312,15 +1403,6 @@ interface EpisodeResponse {
var nextAiring: NextAiring?
var seasonNames: List?
fun getLatestEpisodes(): Map
-
- /** 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")
@@ -1358,55 +1440,7 @@ data class TorrentLoadResponse(
override var syncData: MutableMap = mutableMapOf(),
override var posterHeaders: Map? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : 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? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = 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
- )
-}
+) : LoadResponse
data class AnimeLoadResponse(
var engName: String? = null,
@@ -1437,7 +1471,6 @@ data class AnimeLoadResponse(
override var nextAiring: NextAiring? = null,
override var seasonNames: List? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
) : LoadResponse, EpisodeResponse {
override fun getLatestEpisodes(): Map {
return episodes.map { (status, episodes) ->
@@ -1449,77 +1482,6 @@ data class AnimeLoadResponse(
.takeUnless { it == Int.MIN_VALUE }
}.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..> = mutableMapOf(),
- showStatus: ShowStatus? = null,
- plot: String? = null,
- tags: List? = null,
- synonyms: List? = null,
- rating: Int? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- nextAiring: NextAiring? = null,
- seasonNames: List? = 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 +1533,7 @@ data class LiveStreamLoadResponse(
override var syncData: MutableMap = mutableMapOf(),
override var posterHeaders: Map? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : 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? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- name, url, apiName, dataUrl, posterUrl, year, plot, type, rating, tags, duration, trailers,
- recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
- )
-}
+) : LoadResponse
data class MovieLoadResponse(
override var name: String,
@@ -1623,36 +1556,7 @@ data class MovieLoadResponse(
override var syncData: MutableMap = mutableMapOf(),
override var posterHeaders: Map? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
-) : 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? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- backgroundPosterUrl: String? = null,
- ) : this(
- name, url, apiName, type, dataUrl, posterUrl, year, plot, rating, tags, duration, trailers,
- recommendations, actors, comingSoon, syncData, posterHeaders, backgroundPosterUrl, null
- )
-}
+) : LoadResponse
suspend fun MainAPI.newMovieLoadResponse(
name: String,
@@ -1700,17 +1604,7 @@ suspend fun MainAPI.newMovieLoadResponse(
builder.initializer()
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(
var data: String,
var name: String? = null,
@@ -1720,25 +1614,7 @@ data class Episode(
var rating: Int? = null,
var description: String? = 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") {
try {
@@ -1780,28 +1656,6 @@ fun MainAPI.newEpisode(
return builder
}
-interface IDownloadableMinimum {
- val url: String
- val referer: String
- val headers: Map
-}
-
-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(
override var name: String,
override var url: String,
@@ -1826,7 +1680,6 @@ data class TvSeriesLoadResponse(
override var nextAiring: NextAiring? = null,
override var seasonNames: List? = null,
override var backgroundPosterUrl: String? = null,
- override var contentRating: String? = null,
) : LoadResponse, EpisodeResponse {
override fun getLatestEpisodes(): Map {
val maxSeason =
@@ -1837,69 +1690,6 @@ data class TvSeriesLoadResponse(
.takeUnless { it == Int.MIN_VALUE }
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..,
- posterUrl: String? = null,
- year: Int? = null,
- plot: String? = null,
- showStatus: ShowStatus? = null,
- rating: Int? = null,
- tags: List? = null,
- duration: Int? = null,
- trailers: MutableList = mutableListOf(),
- recommendations: List? = null,
- actors: List? = null,
- comingSoon: Boolean = false,
- syncData: MutableMap = mutableMapOf(),
- posterHeaders: Map? = null,
- nextAiring: NextAiring? = null,
- seasonNames: List? = 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(
@@ -1940,43 +1730,30 @@ data class Tracker(
val cover: String? = null,
)
-data class AniSearch(
- @JsonProperty("data") var data: Data? = Data()
+data class Title(
+ @JsonProperty("romaji") val romaji: String? = null,
+ @JsonProperty("english") val english: String? = null,
) {
- data class Data(
- @JsonProperty("Page") var page: Page? = Page()
- ) {
- data class Page(
- @JsonProperty("media") var media: ArrayList = arrayListOf()
- ) {
- data class Media(
- @JsonProperty("title") var title: Title? = null,
- @JsonProperty("id") var id: Int? = null,
- @JsonProperty("idMal") var idMal: Int? = null,
- @JsonProperty("seasonYear") var seasonYear: Int? = null,
- @JsonProperty("format") var format: String? = null,
- @JsonProperty("coverImage") var coverImage: CoverImage? = null,
- @JsonProperty("bannerImage") var bannerImage: String? = null,
- ) {
- data class CoverImage(
- @JsonProperty("extraLarge") var extraLarge: String? = null,
- @JsonProperty("large") var large: String? = null,
- )
-
- data class Title(
- @JsonProperty("romaji") var romaji: String? = null,
- @JsonProperty("english") var english: String? = null,
- ) {
- fun isMatchingTitles(title: String?): Boolean {
- if (title == null) return false
- return english.equals(title, true) || romaji.equals(title, true)
- }
- }
- }
- }
+ fun isMatchingTitles(title: String?): Boolean {
+ if (title == null) return false
+ return english.equals(title, true) || romaji.equals(title, true)
}
}
+data class Results(
+ @JsonProperty("id") val aniId: String? = null,
+ @JsonProperty("malId") val malId: Int? = null,
+ @JsonProperty("title") val title: Title? = null,
+ @JsonProperty("releaseDate") val releaseDate: Int? = null,
+ @JsonProperty("type") val type: String? = null,
+ @JsonProperty("image") val image: String? = null,
+ @JsonProperty("cover") val cover: String? = null,
+)
+
+data class AniSearch(
+ @JsonProperty("results") val results: ArrayList? = arrayListOf()
+)
+
/**
* used for the getTracker() method
**/
diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
index 5408d2a8..fbad4fce 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -6,7 +6,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.content.res.Configuration
-import android.graphics.Rect
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -19,7 +18,6 @@ import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
-import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.annotation.MainThread
@@ -28,7 +26,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isGone
-import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.marginStart
import androidx.fragment.app.FragmentActivity
@@ -44,6 +41,9 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSnapHelper
import androidx.recyclerview.widget.RecyclerView
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.Session
import com.google.android.gms.cast.framework.SessionManager
@@ -52,108 +52,85 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
-import com.google.common.collect.Comparators.min
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis
+import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.initAll
-import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.loadThemes
import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
-import com.lagradost.cloudstream3.CommonActivity.screenHeight
-import com.lagradost.cloudstream3.CommonActivity.setActivityInstance
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
-import com.lagradost.cloudstream3.CommonActivity.updateTheme
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.mvvm.Resource
+import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
-import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
-import com.lagradost.cloudstream3.services.SubscriptionWorkManager
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_PLAYER
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_REPO
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_RESUME_WATCHING
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING_SEARCH
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
-import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.localListApi
-import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.APIRepository
-import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeViewModel
-import com.lagradost.cloudstream3.ui.library.LibraryViewModel
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator
import com.lagradost.cloudstream3.ui.result.LinearListLayout
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
-import com.lagradost.cloudstream3.ui.result.SyncViewModel
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
-import com.lagradost.cloudstream3.ui.result.setTextHtml
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
-import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
-import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
-import com.lagradost.cloudstream3.ui.settings.Globals.TV
-import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
-import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
+import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
import com.lagradost.cloudstream3.utils.ApkInstaller
-import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings
-import com.lagradost.cloudstream3.utils.AppContextUtils.html
-import com.lagradost.cloudstream3.utils.AppContextUtils.isCastApiAvailable
-import com.lagradost.cloudstream3.utils.AppContextUtils.isLtr
-import com.lagradost.cloudstream3.utils.AppContextUtils.isNetworkAvailable
-import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadCache
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadResult
-import com.lagradost.cloudstream3.utils.AppContextUtils.loadSearchResult
-import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
-import com.lagradost.cloudstream3.utils.AppContextUtils.updateHasTrailers
-import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.attachBackPressedCallback
-import com.lagradost.cloudstream3.utils.BackPressedCallbackHelper.detachBackPressedCallback
+import com.lagradost.cloudstream3.utils.AppUtils.html
+import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
+import com.lagradost.cloudstream3.utils.AppUtils.isLtr
+import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
+import com.lagradost.cloudstream3.utils.AppUtils.isRtl
+import com.lagradost.cloudstream3.utils.AppUtils.loadCache
+import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
+import com.lagradost.cloudstream3.utils.AppUtils.loadResult
+import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
+import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.BiometricCallback
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.biometricPrompt
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.isAuthEnabled
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.promptInfo
-import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
-import com.lagradost.cloudstream3.utils.DataStoreHelper
-import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
+import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
-import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@@ -165,7 +142,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
-import com.lagradost.cloudstream3.utils.fcast.FcastManager
+import com.lagradost.nicehttp.Requests
+import com.lagradost.nicehttp.ResponseParser
import com.lagradost.safefile.SafeFile
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -176,8 +154,10 @@ import java.net.URLDecoder
import java.nio.charset.Charset
import kotlin.math.abs
import kotlin.math.absoluteValue
+import kotlin.reflect.KClass
import kotlin.system.exitProcess
+
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
//https://wiki.videolan.org/Android_Player_Intents/
@@ -188,114 +168,119 @@ import kotlin.system.exitProcess
//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225
-class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCallback {
+const val VLC_PACKAGE = "org.videolan.vlc"
+const val MPV_PACKAGE = "is.xyz.mpv"
+const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
+
+val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
+val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
+
+//TODO REFACTOR AF
+open class ResultResume(
+ val packageString: String,
+ val action: String = Intent.ACTION_VIEW,
+ val position: String? = null,
+ val duration: String? = null,
+ var launcher: ActivityResultLauncher? = null,
+) {
+ val defaultTime = -1L
+
+ val lastId get() = "${packageString}_last_open_id"
+ suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
+ val intent = Intent(action)
+
+ if (id != null)
+ setKey(lastId, id)
+ else
+ removeKey(lastId)
+
+ intent.setPackage(packageString)
+ callback.invoke(intent)
+ launcher?.launch(intent)
+ }
+
+ open fun getPosition(intent: Intent?): Long {
+ return defaultTime
+ }
+
+ open fun getDuration(intent: Intent?): Long {
+ return defaultTime
+ }
+}
+
+val VLC = object : ResultResume(
+ VLC_PACKAGE,
+ // Android 13 intent restrictions fucks up specifically launching the VLC player
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ "org.videolan.vlc.player.result"
+ } else {
+ Intent.ACTION_VIEW
+ },
+ "extra_position",
+ "extra_duration",
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
+ }
+}
+
+val MPV = object : ResultResume(
+ MPV_PACKAGE,
+ //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
+ position = "position",
+ duration = "duration",
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+}
+
+val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
+
+val resumeApps = arrayOf(
+ VLC, MPV, WEB_VIDEO
+)
+
+// Short name for requests client to make it nicer to use
+
+var app = Requests(responseParser = object : ResponseParser {
+ val mapper: ObjectMapper = jacksonObjectMapper().configure(
+ DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
+ false
+ )
+
+ override fun parse(text: String, kClass: KClass): T {
+ return mapper.readValue(text, kClass.java)
+ }
+
+ override fun parseSafe(text: String, kClass: KClass): T? {
+ return try {
+ mapper.readValue(text, kClass.java)
+ } catch (e: Exception) {
+ null
+ }
+ }
+
+ override fun writeValueAsString(obj: Any): String {
+ return mapper.writeValueAsString(obj)
+ }
+}).apply {
+ defaultHeaders = mapOf("user-agent" to USER_AGENT)
+}
+
+class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object {
- const val VLC_PACKAGE = "org.videolan.vlc"
- const val MPV_PACKAGE = "is.xyz.mpv"
- const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo"
-
- val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity")
- val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
-
- //TODO REFACTOR AF
- open class ResultResume(
- val packageString: String,
- val action: String = Intent.ACTION_VIEW,
- val position: String? = null,
- val duration: String? = null,
- var launcher: ActivityResultLauncher? = null,
- ) {
- val defaultTime = -1L
-
- val lastId get() = "${packageString}_last_open_id"
- suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
- val intent = Intent(action)
-
- if (id != null)
- setKey(lastId, id)
- else
- removeKey(lastId)
-
- intent.setPackage(packageString)
- callback.invoke(intent)
- launcher?.launch(intent)
- }
-
- open fun getPosition(intent: Intent?): Long {
- return defaultTime
- }
-
- open fun getDuration(intent: Intent?): Long {
- return defaultTime
- }
- }
-
- val VLC = object : ResultResume(
- VLC_PACKAGE,
- // Android 13 intent restrictions fucks up specifically launching the VLC player
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
- "org.videolan.vlc.player.result"
- } else {
- Intent.ACTION_VIEW
- },
- "extra_position",
- "extra_duration",
- ) {
- override fun getPosition(intent: Intent?): Long {
- return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
- }
-
- override fun getDuration(intent: Intent?): Long {
- return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
- }
- }
-
- val MPV = object : ResultResume(
- MPV_PACKAGE,
- //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
- position = "position",
- duration = "duration",
- ) {
- override fun getPosition(intent: Intent?): Long {
- return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong()
- ?: defaultTime
- }
-
- override fun getDuration(intent: Intent?): Long {
- return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong()
- ?: defaultTime
- }
- }
-
- val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
-
- val resumeApps = arrayOf(
- VLC, MPV, WEB_VIDEO
- )
-
-
const val TAG = "MAINACT"
- const val ANIMATED_OUTLINE: Boolean = false
var lastError: String? = null
- private const val FILE_DELETE_KEY = "FILES_TO_DELETE_KEY"
-
- /**
- * Transient files to delete on application exit.
- * Deletes files on onDestroy().
- */
- private var filesToDelete: Set
- // This needs to be persistent because the application may exit without calling onDestroy.
- get() = getKey>(FILE_DELETE_KEY) ?: setOf()
- private set(value) = setKey(FILE_DELETE_KEY, value)
-
- /**
- * Add file to delete on Exit.
- */
- fun deleteFileOnExit(file: File) {
- filesToDelete = filesToDelete + file.path
- }
-
/**
* Setting this will automatically enter the query in the search
* next time the search fragment is opened.
@@ -319,16 +304,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
// kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event()
- /**
- * Used by DataStoreHelper to fully reload home when switching accounts
- */
- val reloadHomeEvent = Event()
-
- /**
- * Used by DataStoreHelper to fully reload library when switching accounts
- */
- val reloadLibraryEvent = Event()
-
/**
* @return true if the str has launched an app task (be it successful or not)
@@ -351,7 +326,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
println("Repository url: $realUrl")
loadRepository(realUrl)
return true
- } else if (str.contains(APP_STRING)) {
+ } else if (str.contains(appString)) {
for (api in OAuth2Apis) {
if (str.contains("/${api.redirectUrl}")) {
ioSafe {
@@ -381,15 +356,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
// This specific intent is used for the gradle deployWithAdb
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
- if (str == "$APP_STRING:") {
+ if (str == "$appString:") {
PluginManager.hotReloadAllLocalPlugins(activity)
}
- } else if (safeURI(str)?.scheme == APP_STRING_REPO) {
- val url = str.replaceFirst(APP_STRING_REPO, "https")
+ } else if (safeURI(str)?.scheme == appStringRepo) {
+ val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url)
return true
- } else if (safeURI(str)?.scheme == APP_STRING_SEARCH) {
- val query = str.substringAfter("$APP_STRING_SEARCH://")
+ } else if (safeURI(str)?.scheme == appStringSearch) {
+ val query = str.substringAfter("$appStringSearch://")
nextSearchQuery =
try {
URLDecoder.decode(query, "UTF-8")
@@ -403,7 +378,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_search
activity?.findViewById(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search
- } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
+ } else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8")
@@ -417,9 +392,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
)
)
)
- } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
+ } else if (safeURI(str)?.scheme == appStringResumeWatching) {
val id =
- str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
+ str.substringAfter("$appStringResumeWatching://").toIntOrNull()
?: return false
ioSafe {
val resumeWatchingCard =
@@ -451,30 +426,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
var lastPopup: SearchResponse? = null
- fun loadPopup(result: SearchResponse, load: Boolean = true) {
+ fun loadPopup(result: SearchResponse) {
lastPopup = result
- val syncName = syncViewModel.syncName(result.apiName)
-
- // based on apiName we decide on if it is a local list or not, this is because
- // we want to show a bit of extra UI to sync apis
- if (result is SyncAPI.LibraryItem && syncName != null) {
- isLocalList = false
- syncViewModel.setSync(syncName, result.syncId)
- syncViewModel.updateMetaAndUser()
- } else {
- isLocalList = true
- syncViewModel.clear()
- }
-
- if (load) {
- viewModel.load(
- this, result.url, result.apiName, false, if (getApiDubstatusSettings()
- .contains(DubStatus.Dubbed)
- ) DubStatus.Dubbed else DubStatus.Subbed, null
- )
- } else {
- viewModel.loadSmall(result)
- }
+ viewModel.load(
+ this, result.url, result.apiName, false, if (getApiDubstatusSettings()
+ .contains(DubStatus.Dubbed)
+ ) DubStatus.Dubbed else DubStatus.Subbed, null
+ )
}
override fun onColorSelected(dialogId: Int, color: Int) {
@@ -488,7 +446,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
updateLocale() // android fucks me by chaining lang when rotating the phone
- updateTheme(this) // Update if system theme
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
@@ -533,13 +490,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_results_phone,
R.id.navigation_results_tv,
R.id.navigation_player,
- R.id.navigation_quick_search,
).contains(destination.id)
binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
val push =
- if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
+ if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
if (!this.isLtr()) {
params.setMargins(
@@ -566,51 +522,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
Configuration.ORIENTATION_PORTRAIT -> {
- isLayout(TV or EMULATOR)
+ isTvSettings()
}
else -> {
false
}
}
-
binding?.apply {
- navRailView.isVisible = isNavVisible && landscape
navView.isVisible = isNavVisible && !landscape
+ navRailView.isVisible = isNavVisible && landscape
- /**
- * We need to make sure if we return to a sub-fragment,
- * the correct navigation item is selected so that it does not
- * highlight the wrong one in UI.
- */
- when (destination.id) {
- in listOf(R.id.navigation_downloads, R.id.navigation_download_child) -> {
- navRailView.menu.findItem(R.id.navigation_downloads).isChecked = true
- navView.menu.findItem(R.id.navigation_downloads).isChecked = true
- }
- in listOf(
- R.id.navigation_settings,
- R.id.navigation_subtitles,
- R.id.navigation_chrome_subtitles,
- R.id.navigation_settings_player,
- R.id.navigation_settings_updates,
- R.id.navigation_settings_ui,
- R.id.navigation_settings_account,
- R.id.navigation_settings_providers,
- R.id.navigation_settings_general,
- R.id.navigation_settings_extensions,
- R.id.navigation_settings_plugins,
- R.id.navigation_test_providers
- ) -> {
- navRailView.menu.findItem(R.id.navigation_settings).isChecked = true
- navView.menu.findItem(R.id.navigation_settings).isChecked = true
- }
- }
+ // Hide library on TV since it is not supported yet :(
+ val isTrueTv = isTrueTvSettings()
+ navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
+ navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
}
}
//private var mCastSession: CastSession? = null
- var mSessionManager: SessionManager? = null
+ lateinit var mSessionManager: SessionManager
private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() }
private inner class SessionManagerListenerImpl : SessionManagerListener {
@@ -647,10 +578,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
override fun onResume() {
super.onResume()
afterPluginsLoadedEvent += ::onAllPluginsLoaded
- setActivityInstance(this)
try {
if (isCastApiAvailable()) {
- mSessionManager?.addSessionManagerListener(mSessionManagerListener)
+ //mCastSession = mSessionManager.currentCastSession
+ mSessionManager.addSessionManagerListener(mSessionManagerListener)
}
} catch (e: Exception) {
logError(e)
@@ -666,7 +597,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
try {
if (isCastApiAvailable()) {
- mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
+ mSessionManager.removeSessionManagerListener(mSessionManagerListener)
//mCastSession = null
}
} catch (e: Exception) {
@@ -674,7 +605,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
val response = CommonActivity.dispatchKeyEvent(this, event)
if (response != null)
return response
@@ -704,16 +635,35 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
builder.show().setDefaultFocus()
}
- override fun onDestroy() {
- filesToDelete.forEach { path ->
- val result = File(path).deleteRecursively()
- if (result) {
- Log.d(TAG, "Deleted temporary file: $path")
- } else {
- Log.d(TAG, "Failed to delete temporary file: $path")
- }
+ private fun backPressed() {
+ this.window?.navigationBarColor =
+ this.colorFromAttribute(R.attr.primaryGrayBackground)
+ this.updateLocale()
+ this.updateLocale()
+
+ val navHostFragment =
+ supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
+ val navController = navHostFragment?.navController
+ val isAtHome =
+ navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
+
+ if (isAtHome && isTrueTvSettings()) {
+ showConfirmExitDialog()
+ } else {
+ super.onBackPressed()
}
- filesToDelete = setOf()
+ }
+
+ override fun onBackPressed() {
+ ((supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment?)?.childFragmentManager?.primaryNavigationFragment as? IOnBackPressed)?.onBackPressed()
+ ?.let { runNormal ->
+ if (runNormal) backPressed()
+ } ?: run {
+ backPressed()
+ }
+ }
+
+ override fun onDestroy() {
val broadcastIntent = Intent()
broadcastIntent.action = "restart_service"
broadcastIntent.setClass(this, VideoDownloadRestartReceiver::class.java)
@@ -770,7 +720,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
- allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
+ allProviders.add(it.javaClass.newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
@@ -792,15 +742,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
lateinit var viewModel: ResultViewModel2
- lateinit var syncViewModel: SyncViewModel
- private var libraryViewModel: LibraryViewModel? = null
- /** kinda dirty, however it signals that we should use the watch status as sync or not*/
- var isLocalList: Boolean = false
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
-
- viewModel = ViewModelProvider(this)[ResultViewModel2::class.java]
- syncViewModel = ViewModelProvider(this)[SyncViewModel::class.java]
+ viewModel =
+ ViewModelProvider(this)[ResultViewModel2::class.java]
return super.onCreateView(name, context, attrs)
}
@@ -887,13 +832,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
focusOutline.get()?.isVisible = false
}
}
- /*private val scrollListener = object : RecyclerView.OnScrollListener() {
- override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
- super.onScrolled(recyclerView, dx, dy)
- current = current.copy(x = current.x + dx, y = current.y + dy)
- setTargetPosition(current)
- }
- }*/
private fun setTargetPosition(target: FocusTarget) {
focusOutline.get()?.apply {
@@ -936,10 +874,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (!exactlyTheSame) {
lastView?.removeOnLayoutChangeListener(layoutListener)
lastView?.removeOnAttachStateChangeListener(attachListener)
- (lastView?.parent as? RecyclerView)?.apply {
- removeOnLayoutChangeListener(layoutListener)
- //removeOnScrollListener(scrollListener)
- }
+ (lastView?.parent as? RecyclerView)?.removeOnLayoutChangeListener(layoutListener)
}
val wasGone = focusOutline.isGone
@@ -1017,10 +952,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
focusOutline.isVisible = false
}
if (!exactlyTheSame) {
- (newFocus.parent as? RecyclerView)?.apply {
- addOnLayoutChangeListener(layoutListener)
- //addOnScrollListener(scrollListener)
- }
+ (newFocus.parent as? RecyclerView)?.addOnLayoutChangeListener(layoutListener)
newFocus.addOnLayoutChangeListener(layoutListener)
newFocus.addOnAttachStateChangeListener(attachListener)
}
@@ -1038,9 +970,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
)
// if they are the same within then snap, aka scrolling
- val deltaMinX = min(end.width / 2, 60.toPx)
- val deltaMinY = min(end.height / 2, 60.toPx)
- if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMinX && (start.y - end.y).absoluteValue < deltaMinY) {
+ val deltaMin = 50.toPx
+ if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) {
animator?.cancel()
last = start
current = end
@@ -1069,7 +1000,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
// animate between a and b
animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
startDelay = 0
- duration = 200
+ duration = 100
addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
@@ -1111,22 +1042,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- private fun centerView(view: View?) {
- if (view == null) return
- try {
- Log.v(TAG, "centerView: $view")
- val r = Rect(0, 0, 0, 0)
- view.getDrawingRect(r)
- val x = r.centerX()
- val y = r.centerY()
- val dx = r.width() / 2 //screenWidth / 2
- val dy = screenHeight / 2
- val r2 = Rect(x - dx, y - dy, x + dx, y + dy)
- view.requestRectangleOnScreen(r2, false)
- // TvFocus.current =TvFocus.current.copy(y=y.toFloat())
- } catch (_: Throwable) {
- }
- }
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
@@ -1151,7 +1066,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
super.onCreate(savedInstanceState)
try {
if (isCastApiAvailable()) {
- CastContext.getSharedInstance(this) {it.run()}.addOnSuccessListener { mSessionManager = it.sessionManager }
+ mSessionManager = CastContext.getSharedInstance(this).sessionManager
}
} catch (t: Throwable) {
logError(t)
@@ -1161,61 +1076,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
updateTv()
// backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
- normalSafeApiCall {
+ try {
val appVer = BuildConfig.VERSION_NAME
val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
- normalSafeApiCall {
- backup(this)
- }
- normalSafeApiCall {
- // Recompile oat on new version
- PluginManager.deleteAllOatFiles(this)
- }
+ backup()
}
+ } catch (t: Throwable) {
+ logError(t)
}
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
binding = try {
- if (isLayout(TV or EMULATOR)) {
+ if (isTvSettings()) {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root)
-
- if (isLayout(TV) && ANIMATED_OUTLINE) {
- TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
- newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
- TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
- }
- newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
- TvFocus.updateFocusView(newFocus)
- }
- } else {
- newLocalBinding.focusOutline.isVisible = false
+ TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
+ newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
+ // println("refocus $oldFocus -> $newFocus")
+ TvFocus.updateFocusView(newFocus)
}
-
- if (isLayout(TV)) {
- // Put here any button you don't want focusing it to center the view
- val exceptionButtons = listOf(
- R.id.home_preview_play_btt,
- R.id.home_preview_info_btt,
- R.id.home_preview_hidden_next_focus,
- R.id.home_preview_hidden_prev_focus,
- R.id.result_play_movie_button,
- R.id.result_play_series_button,
- R.id.result_resume_series_button,
- R.id.result_play_trailer_button,
- R.id.result_bookmark_Button,
- R.id.result_favorite_Button,
- R.id.result_subscribe_Button,
- R.id.result_search_Button,
- R.id.result_episodes_show_button,
- )
-
- newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
- if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
- centerView(newFocus)
- }
+ newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
+ TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
}
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
@@ -1229,26 +1112,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
null
}
- changeStatusBarState(isLayout(EMULATOR))
-
- /** Biometric stuff for users without accounts **/
- val noAccounts = settingsManager.getBoolean(
- getString(R.string.skip_startup_account_select_key),
- false
- ) || accounts.count() <= 1
-
- if (isLayout(PHONE) && isAuthEnabled(this) && noAccounts) {
- if (deviceHasPasswordPinLock(this)) {
- startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
-
- promptInfo?.let { prompt ->
- biometricPrompt?.authenticate(prompt)
- }
-
- // hide background while authenticating, Sorry moms & dads 🙏
- binding?.navHostFragment?.isInvisible = true
- }
- }
+ changeStatusBarState(isEmulatorSettings())
// Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
@@ -1257,12 +1121,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
} else {
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
- showSnackbar(
- this@MainActivity,
- R.string.jsdelivr_enabled,
- Snackbar.LENGTH_LONG,
- R.string.revert
- ) { setKey(getString(R.string.jsdelivr_proxy_key), false) }
+ val parentView: View = findViewById(android.R.id.content)
+ Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG)
+ .let { snackbar ->
+ snackbar.setAction(R.string.revert) {
+ setKey(getString(R.string.jsdelivr_proxy_key), false)
+ }
+ snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
+ snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
+ snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
+ snackbar.show()
+ }
}
}
}
@@ -1275,7 +1144,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
} else if (lastError == null) {
ioSafe {
- DataStoreHelper.currentHomePage?.let { homeApi ->
+ getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
} ?: run {
mainPluginsLoadedEvent.invoke(false)
@@ -1328,77 +1197,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
builder.show().setDefaultFocus()
}
-
- fun setUserData(status: Resource?) {
- if (isLocalList) return
- bottomPreviewBinding?.apply {
- when (status) {
- is Resource.Success -> {
- resultviewPreviewBookmark.isEnabled = true
- resultviewPreviewBookmark.setText(status.value.status.stringRes)
- resultviewPreviewBookmark.setIconResource(status.value.status.iconRes)
- }
-
- is Resource.Failure -> {
- resultviewPreviewBookmark.isEnabled = false
- resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24)
- resultviewPreviewBookmark.text = status.errorString
- }
-
- else -> {
- resultviewPreviewBookmark.isEnabled = false
- resultviewPreviewBookmark.setIconResource(R.drawable.ic_baseline_bookmark_border_24)
- resultviewPreviewBookmark.setText(R.string.loading)
- }
- }
- }
- }
-
- fun setWatchStatus(state: WatchType?) {
- if (!isLocalList || state == null) return
-
- bottomPreviewBinding?.resultviewPreviewBookmark?.apply {
- setIconResource(state.iconRes)
- setText(state.stringRes)
- }
- }
-
- fun setSubscribeStatus(state: Boolean?) {
- bottomPreviewBinding?.resultviewPreviewSubscribe?.apply {
- if (state != null) {
- val drawable = if (state) {
- R.drawable.ic_baseline_notifications_active_24
- } else {
- R.drawable.baseline_notifications_none_24
- }
- setImageResource(drawable)
- }
- isVisible = state != null
-
- setOnClickListener {
- viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? ->
- if (newStatus == null) return@toggleSubscriptionStatus
-
- val message = if (newStatus) {
- // Kinda icky to have this here, but it works.
- SubscriptionWorkManager.enqueuePeriodicWork(context)
- R.string.subscription_new
- } else {
- R.string.subscription_deleted
- }
-
- val name = (viewModel.page.value as? Resource.Success)?.value?.title
- ?: txt(R.string.no_data).asStringNull(context) ?: ""
- showToast(txt(message, name), Toast.LENGTH_SHORT)
- }
- }
- }
- }
-
- observe(viewModel.watchStatus, ::setWatchStatus)
- observe(syncViewModel.userData, ::setUserData)
- observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus)
-
observeNullable(viewModel.page) { resource ->
if (resource == null) {
hidePreviewPopupDialog()
@@ -1433,78 +1231,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
resultviewPreviewMetaDuration.setText(d.durationText)
resultviewPreviewMetaRating.setText(d.ratingText)
- resultviewPreviewDescription.setTextHtml(d.plotText)
+ resultviewPreviewDescription.setText(d.plotText)
resultviewPreviewPoster.setImage(
d.posterImage ?: d.posterBackgroundImage
)
- setUserData(syncViewModel.userData.value)
- setWatchStatus(viewModel.watchStatus.value)
- setSubscribeStatus(viewModel.subscribeStatus.value)
-
- resultviewPreviewBookmark.setOnClickListener {
+ resultviewPreviewPoster.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
- if (isLocalList) {
- val value = viewModel.watchStatus.value ?: WatchType.NONE
+ val value = viewModel.watchStatus.value ?: WatchType.NONE
- this@MainActivity.showBottomDialog(
- WatchType.entries.map { getString(it.stringRes) }.toList(),
- value.ordinal,
- this@MainActivity.getString(R.string.action_add_to_bookmarks),
- showApply = false,
- {}) {
- viewModel.updateWatchStatus(
- WatchType.entries[it],
- this@MainActivity
- )
- }
- } else {
- val value =
- (syncViewModel.userData.value as? Resource.Success)?.value?.status
- ?: SyncWatchType.NONE
-
- this@MainActivity.showBottomDialog(
- SyncWatchType.entries.map { getString(it.stringRes) }.toList(),
- value.ordinal,
- this@MainActivity.getString(R.string.action_add_to_bookmarks),
- showApply = false,
- {}) {
- syncViewModel.setStatus(SyncWatchType.entries[it].internalId)
- syncViewModel.publishUserData()
- }
+ this@MainActivity.showBottomDialog(
+ WatchType.values().map { getString(it.stringRes) }.toList(),
+ value.ordinal,
+ this@MainActivity.getString(R.string.action_add_to_bookmarks),
+ showApply = false,
+ {}) {
+ viewModel.updateWatchStatus(WatchType.values()[it])
}
}
- observeNullable(viewModel.favoriteStatus) observeFavoriteStatus@{ isFavorite ->
- resultviewPreviewFavorite.isVisible = isFavorite != null
- if (isFavorite == null) return@observeFavoriteStatus
-
- val drawable = if (isFavorite) {
- R.drawable.ic_baseline_favorite_24
- } else {
- R.drawable.ic_baseline_favorite_border_24
- }
-
- resultviewPreviewFavorite.setImageResource(drawable)
- }
-
- resultviewPreviewFavorite.setOnClickListener {
- viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? ->
- if (newStatus == null) return@toggleFavoriteStatus
-
- val message = if (newStatus) {
- R.string.favorite_added
- } else {
- R.string.favorite_removed
- }
-
- val name = (viewModel.page.value as? Resource.Success)?.value?.title
- ?: txt(R.string.no_data).asStringNull(this@MainActivity) ?: ""
- showToast(txt(message, name), Toast.LENGTH_SHORT)
- }
- }
-
- if (isLayout(PHONE)) // dont want this clickable on tv layout
+ if (!isTvSettings()) // dont want this clickable on tv layout
resultviewPreviewDescription.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
@@ -1550,26 +1296,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
logError(e)
}
}
-
- // we need to run this after we init all apis, otherwise currentSyncApi will fuck itself
- this@MainActivity.runOnUiThread {
- // Change library icon with logo of current api in sync
- libraryViewModel = ViewModelProvider(this@MainActivity)[LibraryViewModel::class.java]
- libraryViewModel?.currentApiName?.observe(this@MainActivity) {
- val syncAPI = libraryViewModel?.currentSyncApi
- Log.i("SYNC_API", "${syncAPI?.name}, ${syncAPI?.idPrefix}")
- val icon = if (syncAPI?.idPrefix == localListApi.idPrefix) {
- R.drawable.library_icon
- } else {
- syncAPI?.icon ?: R.drawable.library_icon
- }
-
- binding?.apply {
- navRailView.menu.findItem(R.id.navigation_library)?.setIcon(icon)
- navView.menu.findItem(R.id.navigation_library)?.setIcon(icon)
- }
- }
- }
}
SearchResultBuilder.updateCache(this)
@@ -1598,17 +1324,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
}
}
-
- if (isLayout(TV or EMULATOR)) {
- if (navDestination.matchDestination(R.id.navigation_home)) {
- attachBackPressedCallback {
- showConfirmExitDialog()
- window?.navigationBarColor =
- colorFromAttribute(R.attr.primaryGrayBackground)
- updateLocale()
- }
- } else detachBackPressedCallback()
- }
}
//val navController = findNavController(R.id.nav_host_fragment)
@@ -1640,7 +1355,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
itemRippleColor = rippleColor
itemActiveIndicatorColor = rippleColor
setupWithNavController(navController)
- if (isLayout(TV or EMULATOR)) {
+ if (isTvSettings()) {
background?.alpha = 200
} else {
background?.alpha = 255
@@ -1774,15 +1489,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
runAutoUpdate()
}
- FcastManager().init(this, false)
-
APIRepository.dubStatusActive = getApiDubstatusSettings()
try {
// this ensures that no unnecessary space is taken
loadCache()
File(filesDir, "exoplayer").deleteRecursively() // old cache
- deleteFileOnExit(File(cacheDir, "exoplayer")) // current cache
+ File(cacheDir, "exoplayer").deleteOnExit() // current cache
} catch (e: Exception) {
logError(e)
}
@@ -1792,11 +1505,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
migrateResumeWatching()
}
- getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage ->
- DataStoreHelper.currentHomePage = homepage
- removeKey(USER_SELECTED_HOMEPAGE_API)
- }
-
try {
if (getKey(HAS_DONE_SETUP_KEY, false) != true) {
navController.navigate(R.id.navigation_setup_language)
@@ -1812,6 +1520,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
} catch (e: Exception) {
logError(e)
+ } finally {
+ setKey(HAS_DONE_SETUP_KEY, true)
}
// Used to check current focus for TV
@@ -1823,32 +1533,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
// }
// }
- onBackPressedDispatcher.addCallback(
- this,
- object : OnBackPressedCallback(true) {
- override fun handleOnBackPressed() {
- window?.navigationBarColor = colorFromAttribute(R.attr.primaryGrayBackground)
- updateLocale()
-
- // If we don't disable we end up in a loop with default behavior calling
- // this callback as well, so we disable it, run default behavior,
- // then re-enable this callback so it can be used for next back press.
- isEnabled = false
- onBackPressedDispatcher.onBackPressed()
- isEnabled = true
- }
- }
- )
- }
-
- /** Biometric stuff **/
- override fun onAuthenticationSuccess() {
- // make background (nav host fragment) visible again
- binding?.navHostFragment?.isInvisible = false
- }
-
- override fun onAuthenticationError() {
- finish()
}
suspend fun checkGithubConnectivity(): Boolean {
@@ -1861,4 +1545,4 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
false
}
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt
new file mode 100644
index 00000000..7be90440
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/NativeCrashHandler.kt
@@ -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()
+ }*/
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt b/app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/ParCollections.kt
rename to app/src/main/java/com/lagradost/cloudstream3/ParCollections.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt
index 23f8dcf4..b0051ba7 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AStreamHub.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AStreamHub.kt
@@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors
-import com.lagradost.api.Log
+import android.util.Log
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
new file mode 100644
index 00000000..c782b29d
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Acefile.kt
@@ -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 {
+ val sources = mutableListOf()
+ 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
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Blogger.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Blogger.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/BullStream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/BullStream.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
new file mode 100644
index 00000000..b4f3d897
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt
@@ -0,0 +1,140 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.AppUtils
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import javax.crypto.Cipher
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.PBEKeySpec
+import javax.crypto.spec.SecretKeySpec
+
+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 = "11x&W5UBrcqn\$9Yl"
+ }
+
+ 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 encData = AppUtils.tryParseJson(base64Decode(master ?: return))
+ val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
+
+ 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>("[$tracks]")
+ ?.filter { it.kind == "captions" }?.map { track ->
+ subtitleCallback.invoke(
+ SubtitleFile(
+ track.label ?: "",
+ track.file ?: return@map null
+ )
+ )
+ }
+ }
+
+ private fun cryptoAESHandler(
+ data: AESData,
+ pass: String,
+ encrypt: Boolean = true
+ ): String {
+ val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
+ val spec = PBEKeySpec(
+ pass.toCharArray(),
+ data.salt?.hexToByteArray(),
+ data.iterations?.toIntOrNull() ?: 1,
+ 256
+ )
+ val key = factory.generateSecret(spec)
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+ return if (!encrypt) {
+ cipher.init(
+ Cipher.DECRYPT_MODE,
+ SecretKeySpec(key.encoded, "AES"),
+ IvParameterSpec(data.iv?.hexToByteArray())
+ )
+ String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
+ } else {
+ cipher.init(
+ Cipher.ENCRYPT_MODE,
+ SecretKeySpec(key.encoded, "AES"),
+ IvParameterSpec(data.iv?.hexToByteArray())
+ )
+ base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
+ }
+ }
+
+ private fun String.hexToByteArray(): ByteArray {
+ check(length % 2 == 0) { "Must have an even length" }
+ return chunked(2)
+ .map { it.toInt(16).toByte() }
+
+ .toByteArray()
+ }
+
+ data class AESData(
+ @JsonProperty("ciphertext") val ciphertext: String? = null,
+ @JsonProperty("iv") val iv: String? = null,
+ @JsonProperty("salt") val salt: String? = null,
+ @JsonProperty("iterations") val iterations: String? = null,
+ )
+
+ data class Tracks(
+ @JsonProperty("file") val file: String? = null,
+ @JsonProperty("label") val label: String? = null,
+ @JsonProperty("kind") val kind: String? = null,
+ )
+}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
similarity index 73%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
index 2343a92e..4b7cb19f 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
@@ -7,18 +7,13 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
+import com.lagradost.cloudstream3.utils.Qualities
import java.net.URL
-class Geodailymotion : Dailymotion() {
- override val name = "GeoDailymotion"
- override val mainUrl = "https://geo.dailymotion.com"
-}
-
open class Dailymotion : ExtractorApi() {
override val mainUrl = "https://www.dailymotion.com"
override val name = "Dailymotion"
override val requiresReferer = false
- private val baseUrl = "https://www.dailymotion.com"
@Suppress("RegExpSimplifiable")
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
@@ -32,16 +27,21 @@ open class Dailymotion : ExtractorApi() {
callback: (ExtractorLink) -> Unit
) {
val embedUrl = getEmbedUrl(url) ?: return
- val req = app.get(embedUrl)
+ val doc = app.get(embedUrl).document
val prefix = "window.__PLAYER_CONFIG__ = "
- val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return
- val config = tryParseJson(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return
+ val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
+ val config = tryParseJson(configStr.substringAfter(prefix)) ?: return
val id = getVideoId(embedUrl) ?: return
val dmV1st = config.dmInternalData.v1st
val dmTs = config.dmInternalData.ts
- val embedder = config.context.embedder
- val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
- val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
+ val metaDataUrl =
+ "$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
+ val cookies = mapOf(
+ "v1st" to dmV1st,
+ "dmvk" to config.context.dmvk,
+ "ts" to dmTs.toString()
+ )
+ val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
.parsedSafe() ?: return
metaData.qualities.forEach { (_, video) ->
video.forEach {
@@ -51,19 +51,16 @@ open class Dailymotion : ExtractorApi() {
}
private fun getEmbedUrl(url: String): String? {
- if (url.contains("/embed/") || url.contains("/video/")) {
- return url
+ if (url.contains("/embed/")) {
+ 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? {
val path = URL(url).path
- val id = path.substringAfter("/video/")
+ val id = path.substringAfter("video/")
if (id.matches(videoIdRegex)) {
return id
}
@@ -87,13 +84,13 @@ open class Dailymotion : ExtractorApi() {
)
data class InternalData(
- val ts: Long,
+ val ts: Int,
val v1st: String
)
data class Context(
@JsonProperty("access_token") val accessToken: String?,
- val embedder: String?,
+ val dmvk: String,
)
data class MetaData(
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
similarity index 73%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
index 370dcaca..8dcfb859 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
@@ -7,18 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay
-class D0000d : DoodLaExtractor() {
- override var mainUrl = "https://d0000d.com"
-}
-
-class D000dCom : DoodLaExtractor() {
- override var mainUrl = "https://d000d.com"
-}
-
-class DoodstreamCom : DoodLaExtractor() {
- override var mainUrl = "https://doodstream.com"
-}
-
class Dooood : DoodLaExtractor() {
override var mainUrl = "https://dooood.com"
}
@@ -68,10 +56,9 @@ open class DoodLaExtractor : ExtractorApi() {
}
override suspend fun getUrl(url: String, referer: String?): List? {
- val newUrl= url.replace(mainUrl, "https://d0000d.com")
- val response0 = app.get(newUrl).text // html of DoodStream page to look for /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 = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
+ val response0 = app.get(url).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 trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("").substringBefore(""))?.groupValues?.get(0)
return listOf(
ExtractorLink(
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Embedgram.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Embedgram.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Fastream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Fastream.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Filesim.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GMPlayer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GMPlayer.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
similarity index 58%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
index 8d1a4d07..df9c74a4 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gdriveplayer.kt
@@ -2,10 +2,14 @@ package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
-import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import org.jsoup.nodes.Element
+import java.security.DigestException
+import java.security.MessageDigest
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
class DatabaseGdrive2 : Gdriveplayer() {
override var mainUrl = "https://databasegdriveplayer.co"
@@ -61,6 +65,78 @@ open class Gdriveplayer : ExtractorApi() {
?.data()?.let { getAndUnpack(it) }
}
+ private fun String.decodeHex(): ByteArray {
+ check(length % 2 == 0) { "Must have an even length" }
+ return chunked(2)
+ .map { it.toInt(16).toByte() }
+ .toByteArray()
+ }
+
+ // https://stackoverflow.com/a/41434590/8166854
+ private fun GenerateKeyAndIv(
+ password: ByteArray,
+ salt: ByteArray,
+ hashAlgorithm: String = "MD5",
+ keyLength: Int = 32,
+ ivLength: Int = 16,
+ iterations: Int = 1
+ ): List? {
+
+ val md = MessageDigest.getInstance(hashAlgorithm)
+ val digestLength = md.digestLength
+ val targetKeySize = keyLength + ivLength
+ val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
+ val generatedData = ByteArray(requiredLength)
+ var generatedLength = 0
+
+ try {
+ md.reset()
+
+ while (generatedLength < targetKeySize) {
+ if (generatedLength > 0)
+ md.update(
+ generatedData,
+ generatedLength - digestLength,
+ digestLength
+ )
+
+ md.update(password)
+ md.update(salt, 0, 8)
+ md.digest(generatedData, generatedLength, digestLength)
+
+ for (i in 1 until iterations) {
+ md.update(generatedData, generatedLength, digestLength)
+ md.digest(generatedData, generatedLength, digestLength)
+ }
+
+ generatedLength += digestLength
+ }
+ return listOf(
+ generatedData.copyOfRange(0, keyLength),
+ generatedData.copyOfRange(keyLength, targetKeySize)
+ )
+ } catch (e: DigestException) {
+ return null
+ }
+ }
+
+ private fun cryptoAESHandler(
+ data: AesData,
+ pass: ByteArray,
+ encrypt: Boolean = true
+ ): String? {
+ val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
+ val cipher = Cipher.getInstance("AES/CBC/NoPadding")
+ return if (!encrypt) {
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
+ String(cipher.doFinal(base64DecodeArray(data.ct)))
+ } else {
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
+ base64Encode(cipher.doFinal(data.ct.toByteArray()))
+
+ }
+ }
+
private fun Regex.first(str: String): String? {
return find(str)?.groupValues?.getOrNull(1)
}
@@ -78,14 +154,14 @@ open class Gdriveplayer : ExtractorApi() {
val document = app.get(url).document
val eval = unpackJs(document)?.replace("\\", "") ?: return
- val data = Regex("data='(\\S+?)'").first(eval) ?: return
+ val data = tryParseJson(Regex("data='(\\S+?)'").first(eval)) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+"))
?.joinToString("") {
Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password")
- val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
+ val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
@@ -118,6 +194,12 @@ open class Gdriveplayer : ExtractorApi() {
}
+ data class AesData(
+ @JsonProperty("ct") val ct: String,
+ @JsonProperty("iv") val iv: String,
+ @JsonProperty("s") val s: String
+ )
+
data class Tracks(
@JsonProperty("file") val file: String,
@JsonProperty("kind") val kind: String,
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GenericM3U8.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GenericM3U8.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt
similarity index 91%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt
index 8d78c1a4..d76b0e11 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Gofile.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt
@@ -19,12 +19,12 @@ open class Gofile : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
- 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()?.data?.get("token")
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()?.data?.contents?.forEach {
callback.invoke(
ExtractorLink(
@@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() {
@JsonProperty("data") val data: Data? = null,
)
-}
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/JWPlayer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/JWPlayer.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jawcloud.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Jawcloud.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Jeniusplay.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Krakenfiles.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Krakenfiles.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
similarity index 83%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
index 04a9a6ac..6a4945bb 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Linkbox.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
@@ -18,8 +18,7 @@ open class Linkbox : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
- val token = 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()?.data?.itemId
+ val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
.parsedSafe()?.data?.itemInfo?.resolutionList?.map { link ->
callback.invoke(
@@ -45,7 +44,6 @@ open class Linkbox : ExtractorApi() {
data class Data(
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
- @JsonProperty("itemId") val itemId: String? = null,
)
data class Responses(
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/M3u8Manifest.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Maxstream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Maxstream.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MixDrop.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MixDrop.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Mvidoo.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Mvidoo.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt
new file mode 100644
index 00000000..70e87fbf
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/OkRuExtractor.kt
@@ -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 = 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? {
+ val doc = app.get(url).document
+ val sources = ArrayList()
+ val datajson = doc.select("div[data-options]").attr("data-options")
+ if (datajson.isNotBlank()) {
+ val main = parseJson(datajson)
+ val metadatajson = parseJson(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
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Okrulink.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Okrulink.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
index 4163cd94..45ec4c2f 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Pelisplus.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pelisplus.kt
@@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor
@@ -67,7 +66,7 @@ open class Pelisplus(val mainUrl: String) {
href,
page.url,
getQualityFromName(qual),
- type = INFER_TYPE
+ element.attr("href").contains(".m3u8")
)
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt
new file mode 100644
index 00000000..9b481240
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Pixeldrain.kt
@@ -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,
+ )
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
similarity index 99%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
index a4dc694e..2b286abb 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayLtXyz.kt
@@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors
-import com.lagradost.api.Log
+import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/PlayerVoxzer.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt
similarity index 68%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt
index 2df81bc6..0154b4e8 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Rabbitstream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Rabbitstream.kt
@@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64DecodeArray
-import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
@@ -17,52 +16,13 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
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() {
override val name = "Megacloud"
override val mainUrl = "https://megacloud.tv"
override val embed = "embed-2/ajax/e-1"
- private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js"
-
- override suspend fun extractRealKey(sources: String): Pair {
- 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> {
- 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
- }
-
+ override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt"
}
class Dokicloud : Rabbitstream() {
@@ -70,14 +30,12 @@ class Dokicloud : Rabbitstream() {
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() {
override val name = "Rabbitstream"
override val mainUrl = "https://rabbitstream.net"
override val requiresReferer = false
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(
url: String,
@@ -98,7 +56,7 @@ open class Rabbitstream : ExtractorApi() {
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
response.parsedSafe()
} else {
- val (key, encData) = extractRealKey(sources)
+ val (key, encData) = extractRealKey(sources, getRawKey())
val decrypted = decryptMapped>(encData, key)
SourcesResponses(
sources = decrypted,
@@ -117,8 +75,8 @@ open class Rabbitstream : ExtractorApi() {
decryptedSources?.tracks?.map { track ->
subtitleCallback.invoke(
SubtitleFile(
- track?.label ?: return@map,
- track.file ?: return@map
+ track?.label ?: "",
+ track?.file ?: return@map
)
)
}
@@ -126,10 +84,23 @@ open class Rabbitstream : ExtractorApi() {
}
- open suspend fun extractRealKey(sources: String): Pair {
- val rawKeys = parseJson>(app.get(key).text)
- val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
- return extractedKey to sources
+ private suspend fun getRawKey(): String = app.get(key).text
+
+ private fun extractRealKey(originalString: String?, stops: String): Pair {
+ val table = parseJson>>(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 decryptMapped(input: String, key: String): T? {
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/SBPlay.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/SBPlay.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Sendvid.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Sendvid.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Solidfiles.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Solidfiles.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
similarity index 81%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
index 702501a1..3f6fff2f 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Minoplres.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt
@@ -7,12 +7,14 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-open class Minoplres : ExtractorApi() {
+class SpeedoStream1 : SpeedoStream() {
+ override val mainUrl = "https://speedostream.pm"
+}
- override val name = "Minoplres" // formerly SpeedoStream
+open class SpeedoStream : ExtractorApi() {
+ override val name = "SpeedoStream"
+ override val mainUrl = "https://speedostream.mom"
override val requiresReferer = true
- override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond
- private val hostUrl = "https://minoplres.xyz"
override suspend fun getUrl(url: String, referer: String?): List {
val sources = mutableListOf()
@@ -24,7 +26,7 @@ open class Minoplres : ExtractorApi() {
M3u8Helper.generateM3u8(
name,
it.file,
- "$hostUrl/",
+ "$mainUrl/",
).forEach { m3uData -> sources.add(m3uData) }
}
}
@@ -35,4 +37,6 @@ open class Minoplres : ExtractorApi() {
private data class File(
@JsonProperty("file") val file: String,
)
+
+
}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
similarity index 93%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
index 2ee98c65..ece8dc4b 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamTape.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamTape.kt
@@ -9,10 +9,6 @@ class StreamTapeNet : StreamTape() {
override var mainUrl = "https://streamtape.net"
}
-class StreamTapeXyz : StreamTape() {
- override var mainUrl = "https://streamtape.xyz"
-}
-
class ShaveTape : StreamTape(){
override var mainUrl = "https://shavetape.cash"
}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamhub.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamhub.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamlare.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamlare.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamoUpload.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamoUpload.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Streamplay.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Streamplay.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
index e70cae6b..dd49d994 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Supervideo.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Supervideo.kt
@@ -13,7 +13,7 @@ data class Files(
open class Supervideo : ExtractorApi() {
override var name = "Supervideo"
- override var mainUrl = "https://supervideo.cc"
+ override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List? {
val extractedLinksList: MutableList = mutableListOf()
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Tomatomatela.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/UpstreamExtractor.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userload.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Userload.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Userscloud.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Userscloud.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uservideo.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Uservideo.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vicloud.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vicloud.kt
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
new file mode 100644
index 00000000..a27bf188
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VidSrcExtractor.kt
@@ -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)
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/VideoVard.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/VideoVard.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
similarity index 89%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
index 979fd8c5..615cfd74 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidmoly.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidmoly.kt
@@ -25,13 +25,9 @@ open class Vidmoly : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> 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(
url,
- headers = headers,
referer = referer,
).document.select("script")
.find { it.data().contains("sources:") }?.data()
@@ -70,4 +66,4 @@ open class Vidmoly : ExtractorApi() {
@JsonProperty("kind") val kind: String? = null,
)
-}
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vido.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vido.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt
index c6493dbe..7eb7fbac 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Vidstream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Vidstream.kt
@@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.argamap
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor
@@ -71,7 +70,7 @@ class Vidstream(val mainUrl: String) {
href,
page.url,
getQualityFromName(qual),
- type = INFER_TYPE
+ element.attr("href").contains(".m3u8")
)
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
new file mode 100644
index 00000000..2c6998de
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
@@ -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)
+
+ }
+}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WatchSB.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/WatchSB.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt
similarity index 96%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt
index 659d7804..6cc486cd 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/WcoStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/WcoStream.kt
@@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.Qualities
class Vidstreamz : WcoStream() {
@@ -127,7 +126,8 @@ open class WcoStream : ExtractorApi() {
if (!response.text.startsWith("{")) throw ErrorLoadingException("Seems like 9Anime kiddies changed stuff again, Go touch some grass for bout an hour Or use a different Server")
return response.parsed().data.media.sources.map {
- ExtractorLink(name, it.file, it.file, host, Qualities.Unknown.value, type = INFER_TYPE)
+ ExtractorLink(name, it.file,it.file,host,Qualities.Unknown.value,it.file.contains(".m3u8"))
}
+
}
}
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt
similarity index 92%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt
index c69f0938..ae1e872a 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Wibufile.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Wibufile.kt
@@ -4,8 +4,8 @@ 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.INFER_TYPE
import com.lagradost.cloudstream3.utils.Qualities
+import java.net.URI
open class Wibufile : ExtractorApi() {
override val name: String = "Wibufile"
@@ -28,8 +28,10 @@ open class Wibufile : ExtractorApi() {
video ?: return,
"$mainUrl/",
Qualities.Unknown.value,
- type = INFER_TYPE
+ URI(url).path.endsWith(".m3u8")
)
)
+
}
+
}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YourUpload.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/YourUpload.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt
similarity index 94%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt
index 4e854630..23704e90 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt
@@ -70,18 +70,19 @@ open class YoutubeExtractor : ExtractorApi() {
}
}
ytVideos[url]?.mapNotNull {
- if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null
+ if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
ExtractorLink(
this.name,
this.name,
- it.content ?: return@mapNotNull null,
+ it.url ?: return@mapNotNull null,
"",
it.height
)
}?.forEach(callback)
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)
}
+
}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zorofile.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Zorofile.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Zplayer.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Zplayer.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt
similarity index 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt
index bd42424f..0b401c06 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/AsianEmbedHelper.kt
@@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors.helper
-import com.lagradost.api.Log
+import android.util.Log
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/GogoHelper.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/NineAnimeHelper.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt
similarity index 100%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/VstreamhubHelper.kt
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt
similarity index 76%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt
index 35aec2b1..768fa1f6 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/helper/WcoHelper.kt
@@ -1,6 +1,8 @@
package com.lagradost.cloudstream3.extractors.helper
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
class WcoHelper {
@@ -28,7 +30,9 @@ class WcoHelper {
private suspend fun getKeys() {
keys = keys
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
- .parsedSafe()
+ .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
+ BACKUP_KEY_DATA
+ )
}
suspend fun getWcoKey(): ExternalKeys? {
@@ -39,7 +43,9 @@ class WcoHelper {
private suspend fun getNewKeys() {
newKeys = newKeys
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
- .parsedSafe()
+ .parsedSafe()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
+ BACKUP_KEY_DATA
+ )
}
suspend fun getNewWcoKey(): NewExternalKeys? {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt
new file mode 100644
index 00000000..8cfe1e9a
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt
@@ -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? {
+ 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
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
index bc646a8d..75e96bec 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/SyncRedirector.kt
@@ -2,13 +2,15 @@ package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
+import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
+ val syncApis = SyncApis
private val syncIds =
listOf(
- SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
- SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
+ SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
+ SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
)
suspend fun redirect(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
index c5b4d453..314177af 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TmdbProvider.kt
@@ -105,7 +105,6 @@ open class TmdbProvider : MainAPI() {
this.id,
episode.episode_number,
episode.season_number,
- this.name ?: this.original_name,
).toJson(),
episode.name,
episode.season_number,
@@ -123,7 +122,6 @@ open class TmdbProvider : MainAPI() {
this.id,
episodeNum,
season.season_number,
- this.name ?: this.original_name,
).toJson(),
season = season.season_number
)
@@ -153,8 +151,6 @@ open class TmdbProvider : MainAPI() {
recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
addActors(credits?.cast?.toList().toActors())
-
- contentRating = fetchContentRating(id, "US")
}
}
@@ -197,8 +193,6 @@ open class TmdbProvider : MainAPI() {
recommendations = (this@toLoadResponse.recommendations
?: this@toLoadResponse.similar)?.results?.map { it.toSearchResponse() }
addActors(credits?.cast?.toList().toActors())
-
- contentRating = fetchContentRating(id, "US")
}
}
@@ -270,26 +264,6 @@ open class TmdbProvider : MainAPI() {
return null
}
- open suspend fun fetchContentRating(id: Int?, country: String): String? {
- id ?: return null
-
- val contentRatings = tmdb.tvService().content_ratings(id).awaitResponse().body()?.results
- return if (!contentRatings.isNullOrEmpty()) {
- contentRatings.firstOrNull { it: ContentRating ->
- it.iso_3166_1 == country
- }?.rating
- } else {
- val releaseDates = tmdb.moviesService().releaseDates(id).awaitResponse().body()?.results
- val certification = releaseDates?.firstOrNull { it: ReleaseDatesResult ->
- it.iso_3166_1 == country
- }?.release_dates?.firstOrNull { it: ReleaseDate ->
- !it.certification.isNullOrBlank()
- }?.certification
-
- certification
- }
- }
-
// Possible to add recommendations and such here.
override suspend fun load(url: String): LoadResponse? {
// https://www.themoviedb.org/movie/7445-brothers
diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
deleted file mode 100644
index addee9a0..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/TraktProvider.kt
+++ /dev/null
@@ -1,471 +0,0 @@
-package com.lagradost.cloudstream3.metaproviders
-
-import android.net.Uri
-import com.fasterxml.jackson.annotation.JsonAlias
-import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.APIHolder
-import com.lagradost.cloudstream3.APIHolder.unixTimeMS
-import com.lagradost.cloudstream3.Actor
-import com.lagradost.cloudstream3.ActorData
-import com.lagradost.cloudstream3.Episode
-import com.lagradost.cloudstream3.HomePageResponse
-import com.lagradost.cloudstream3.LoadResponse
-import com.lagradost.cloudstream3.LoadResponse.Companion.addImdbId
-import com.lagradost.cloudstream3.LoadResponse.Companion.addTMDbId
-import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
-import com.lagradost.cloudstream3.MainAPI
-import com.lagradost.cloudstream3.MainPageRequest
-import com.lagradost.cloudstream3.NextAiring
-import com.lagradost.cloudstream3.ProviderType
-import com.lagradost.cloudstream3.SearchResponse
-import com.lagradost.cloudstream3.ShowStatus
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.addDate
-import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.base64Decode
-import com.lagradost.cloudstream3.mainPageOf
-import com.lagradost.cloudstream3.mvvm.logError
-import com.lagradost.cloudstream3.newHomePageResponse
-import com.lagradost.cloudstream3.newMovieLoadResponse
-import com.lagradost.cloudstream3.newMovieSearchResponse
-import com.lagradost.cloudstream3.newTvSeriesLoadResponse
-import com.lagradost.cloudstream3.newTvSeriesSearchResponse
-import com.lagradost.cloudstream3.utils.AppUtils.parseJson
-import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import java.text.SimpleDateFormat
-import java.util.Locale
-import kotlin.math.roundToInt
-
-open class TraktProvider : MainAPI() {
- override var name = "Trakt"
- override val hasMainPage = true
- override val providerType = ProviderType.MetaProvider
- override val supportedTypes = setOf(
- TvType.Movie,
- TvType.TvSeries,
- TvType.Anime,
- )
-
- private val traktClientId =
- base64Decode("N2YzODYwYWQzNGI4ZTZmOTdmN2I5MTA0ZWQzMzEwOGI0MmQ3MTdlMTM0MmM2NGMxMTg5NGE1MjUyYTQ3NjE3Zg==")
- private val traktApiUrl = base64Decode("aHR0cHM6Ly9hcGl6LnRyYWt0LnR2")
-
- override val mainPage = mainPageOf(
- "$traktApiUrl/movies/trending" to "Trending Movies", //Most watched movies right now
- "$traktApiUrl/movies/popular" to "Popular Movies", //The most popular movies for all time
- "$traktApiUrl/shows/trending" to "Trending Shows", //Most watched Shows right now
- "$traktApiUrl/shows/popular" to "Popular Shows", //The most popular Shows for all time
- )
-
- override suspend fun getMainPage(page: Int, request: MainPageRequest): HomePageResponse {
-
- val apiResponse = getApi("${request.data}?extended=cloud9,full&page=$page")
-
- val results = parseJson>(apiResponse).map { element ->
- element.toSearchResponse()
- }
- return newHomePageResponse(request.name, results)
- }
-
- private fun MediaDetails.toSearchResponse(): SearchResponse {
-
- val media = this.media ?: this
- val mediaType = if (media.ids?.tvdb == null) TvType.Movie else TvType.TvSeries
- val poster = media.images?.poster?.firstOrNull()
-
- if (mediaType == TvType.Movie) {
- return newMovieSearchResponse(
- name = media.title!!,
- url = Data(
- type = mediaType,
- mediaDetails = media,
- ).toJson(),
- type = TvType.Movie,
- ) {
- posterUrl = fixPath(poster)
- }
- } else {
- return newTvSeriesSearchResponse(
- name = media.title!!,
- url = Data(
- type = mediaType,
- mediaDetails = media,
- ).toJson(),
- type = TvType.TvSeries,
- ) {
- this.posterUrl = fixPath(poster)
- }
- }
- }
-
- override suspend fun search(query: String): List? {
- val apiResponse =
- getApi("$traktApiUrl/search/movie,show?extended=cloud9,full&limit=20&page=1&query=$query")
-
- val results = parseJson>(apiResponse).map { element ->
- element.toSearchResponse()
- }
-
- return results
- }
-
- override suspend fun load(url: String): LoadResponse {
-
- val data = parseJson(url)
- val mediaDetails = data.mediaDetails
- val moviesOrShows = if (data.type == TvType.Movie) "movies" else "shows"
-
- val posterUrl = mediaDetails?.images?.poster?.firstOrNull()
- val backDropUrl = mediaDetails?.images?.fanart?.firstOrNull()
-
- val resActor =
- getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/people?extended=cloud9,full")
-
- val actors = parseJson(resActor).cast?.map {
- ActorData(
- Actor(
- name = it.person?.name!!,
- image = getWidthImageUrl(it.person.images?.headshot?.firstOrNull(), "w500")
- ),
- roleString = it.character
- )
- }
-
- val resRelated =
- getApi("$traktApiUrl/$moviesOrShows/${mediaDetails?.ids?.trakt}/related?extended=cloud9,full&limit=20")
-
- val relatedMedia = parseJson>(resRelated).map { it.toSearchResponse() }
-
- val isCartoon =
- mediaDetails?.genres?.contains("animation") == true || mediaDetails?.genres?.contains("anime") == true
- val isAnime =
- isCartoon && (mediaDetails?.language == "zh" || mediaDetails?.language == "ja")
- val isAsian = !isAnime && (mediaDetails?.language == "zh" || mediaDetails?.language == "ko")
- val isBollywood = mediaDetails?.country == "in"
-
- if (data.type == TvType.Movie) {
-
- val linkData = LinkData(
- id = mediaDetails?.ids?.tmdb,
- traktId = mediaDetails?.ids?.trakt,
- traktSlug = mediaDetails?.ids?.slug,
- tmdbId = mediaDetails?.ids?.tmdb,
- imdbId = mediaDetails?.ids?.imdb.toString(),
- tvdbId = mediaDetails?.ids?.tvdb,
- tvrageId = mediaDetails?.ids?.tvrage,
- type = data.type.toString(),
- title = mediaDetails?.title,
- year = mediaDetails?.year,
- orgTitle = mediaDetails?.title,
- isAnime = isAnime,
- //jpTitle = later if needed as it requires another network request,
- airedDate = mediaDetails?.released
- ?: mediaDetails?.firstAired,
- isAsian = isAsian,
- isBollywood = isBollywood,
- ).toJson()
-
- return newMovieLoadResponse(
- name = mediaDetails?.title!!,
- url = data.toJson(),
- dataUrl = linkData.toJson(),
- type = if (isAnime) TvType.AnimeMovie else TvType.Movie,
- ) {
- this.name = mediaDetails.title
- this.type = if (isAnime) TvType.AnimeMovie else TvType.Movie
- this.posterUrl = getOriginalWidthImageUrl(posterUrl)
- this.year = mediaDetails.year
- this.plot = mediaDetails.overview
- this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
- this.tags = mediaDetails.genres
- this.duration = mediaDetails.runtime
- this.recommendations = relatedMedia
- this.actors = actors
- this.comingSoon = isUpcoming(mediaDetails.released)
- //posterHeaders
- this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
- this.contentRating = mediaDetails.certification
- addTrailer(mediaDetails.trailer)
- addImdbId(mediaDetails.ids?.imdb)
- addTMDbId(mediaDetails.ids?.tmdb.toString())
- }
- } else {
-
- val resSeasons =
- getApi("$traktApiUrl/shows/${mediaDetails?.ids?.trakt.toString()}/seasons?extended=cloud9,full,episodes")
- val episodes = mutableListOf()
- val seasons = parseJson>(resSeasons)
- var nextAir: NextAiring? = null
-
- seasons.forEach { season ->
-
- season.episodes?.map { episode ->
-
- val linkData = LinkData(
- id = mediaDetails?.ids?.tmdb,
- traktId = mediaDetails?.ids?.trakt,
- traktSlug = mediaDetails?.ids?.slug,
- tmdbId = mediaDetails?.ids?.tmdb,
- imdbId = mediaDetails?.ids?.imdb.toString(),
- tvdbId = mediaDetails?.ids?.tvdb,
- tvrageId = mediaDetails?.ids?.tvrage,
- type = data.type.toString(),
- season = episode.season,
- episode = episode.number,
- title = mediaDetails?.title,
- year = mediaDetails?.year,
- orgTitle = mediaDetails?.title,
- isAnime = isAnime,
- airedYear = mediaDetails?.year,
- lastSeason = seasons.size,
- epsTitle = episode.title,
- //jpTitle = later if needed as it requires another network request,
- date = episode.firstAired,
- airedDate = episode.firstAired,
- isAsian = isAsian,
- isBollywood = isBollywood,
- isCartoon = isCartoon
- ).toJson()
-
- episodes.add(
- Episode(
- data = linkData.toJson(),
- name = episode.title,
- season = episode.season,
- episode = episode.number,
- posterUrl = fixPath(episode.images?.screenshot?.firstOrNull()),
- rating = episode.rating?.times(10)?.roundToInt(),
- description = episode.overview,
- runTime = episode.runtime
- ).apply {
- this.addDate(episode.firstAired, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
- if (nextAir == null && this.date != null && this.date!! > unixTimeMS && this.season != 0) {
- nextAir = NextAiring(
- episode = this.episode!!,
- unixTime = this.date!!.div(1000L),
- season = if (this.season == 1) null else this.season,
- )
- }
- }
- )
- }
- }
-
- return newTvSeriesLoadResponse(
- name = mediaDetails?.title!!,
- url = data.toJson(),
- type = if (isAnime) TvType.Anime else TvType.TvSeries,
- episodes = episodes
- ) {
- this.name = mediaDetails.title
- this.type = if (isAnime) TvType.Anime else TvType.TvSeries
- this.episodes = episodes
- this.posterUrl = getOriginalWidthImageUrl(posterUrl)
- this.year = mediaDetails.year
- this.plot = mediaDetails.overview
- this.showStatus = getStatus(mediaDetails.status)
- this.rating = mediaDetails.rating?.times(1000)?.roundToInt()
- this.tags = mediaDetails.genres
- this.duration = mediaDetails.runtime
- this.recommendations = relatedMedia
- this.actors = actors
- this.comingSoon = isUpcoming(mediaDetails.released)
- //posterHeaders
- this.nextAiring = nextAir
- this.backgroundPosterUrl = getOriginalWidthImageUrl(backDropUrl)
- this.contentRating = mediaDetails.certification
- addTrailer(mediaDetails.trailer)
- addImdbId(mediaDetails.ids?.imdb)
- addTMDbId(mediaDetails.ids?.tmdb.toString())
- }
- }
- }
-
- private suspend fun getApi(url: String): String {
- return app.get(
- url = url,
- headers = mapOf(
- "Content-Type" to "application/json",
- "trakt-api-version" to "2",
- "trakt-api-key" to traktClientId,
- )
- ).toString()
- }
-
- private fun isUpcoming(dateString: String?): Boolean {
- return try {
- val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
- val dateTime = dateString?.let { format.parse(it)?.time } ?: return false
- unixTimeMS < dateTime
- } catch (t: Throwable) {
- logError(t)
- false
- }
- }
-
- private fun getStatus(t: String?): ShowStatus {
- return when (t) {
- "returning series" -> ShowStatus.Ongoing
- "continuing" -> ShowStatus.Ongoing
- else -> ShowStatus.Completed
- }
- }
-
- private fun fixPath(url: String?): String? {
- url ?: return null
- return "https://$url"
- }
-
- private fun getWidthImageUrl(path: String?, width: String): String? {
- if (path == null) return null
- if (!path.contains("image.tmdb.org")) return fixPath(path)
- val fileName = Uri.parse(path).lastPathSegment ?: return null
- return "https://image.tmdb.org/t/p/${width}/${fileName}"
- }
-
- private fun getOriginalWidthImageUrl(path: String?): String? {
- if (path == null) return null
- if (!path.contains("image.tmdb.org")) return fixPath(path)
- return getWidthImageUrl(path, "original")
- }
-
- data class Data(
- val type: TvType? = null,
- val mediaDetails: MediaDetails? = null,
- )
-
- data class MediaDetails(
- @JsonProperty("title") val title: String? = null,
- @JsonProperty("year") val year: Int? = null,
- @JsonProperty("ids") val ids: Ids? = null,
- @JsonProperty("tagline") val tagline: String? = null,
- @JsonProperty("overview") val overview: String? = null,
- @JsonProperty("released") val released: String? = null,
- @JsonProperty("runtime") val runtime: Int? = null,
- @JsonProperty("country") val country: String? = null,
- @JsonProperty("updatedAt") val updatedAt: String? = null,
- @JsonProperty("trailer") val trailer: String? = null,
- @JsonProperty("homepage") val homepage: String? = null,
- @JsonProperty("status") val status: String? = null,
- @JsonProperty("rating") val rating: Double? = null,
- @JsonProperty("votes") val votes: Long? = null,
- @JsonProperty("comment_count") val commentCount: Long? = null,
- @JsonProperty("language") val language: String? = null,
- @JsonProperty("languages") val languages: List? = null,
- @JsonProperty("available_translations") val availableTranslations: List? = null,
- @JsonProperty("genres") val genres: List? = null,
- @JsonProperty("certification") val certification: String? = null,
- @JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
- @JsonProperty("first_aired") val firstAired: String? = null,
- @JsonProperty("airs") val airs: Airs? = null,
- @JsonProperty("network") val network: String? = null,
- @JsonProperty("images") val images: Images? = null,
- @JsonProperty("movie") @JsonAlias("show") val media: MediaDetails? = null
- )
-
- data class Airs(
- @JsonProperty("day") val day: String? = null,
- @JsonProperty("time") val time: String? = null,
- @JsonProperty("timezone") val timezone: String? = null,
- )
-
- data class Ids(
- @JsonProperty("trakt") val trakt: Int? = null,
- @JsonProperty("slug") val slug: String? = null,
- @JsonProperty("tvdb") val tvdb: Int? = null,
- @JsonProperty("imdb") val imdb: String? = null,
- @JsonProperty("tmdb") val tmdb: Int? = null,
- @JsonProperty("tvrage") val tvrage: String? = null,
- )
-
- data class Images(
- @JsonProperty("fanart") val fanart: List? = null,
- @JsonProperty("poster") val poster: List? = null,
- @JsonProperty("logo") val logo: List? = null,
- @JsonProperty("clearart") val clearart: List? = null,
- @JsonProperty("banner") val banner: List? = null,
- @JsonProperty("thumb") val thumb: List? = null,
- @JsonProperty("screenshot") val screenshot: List? = null,
- @JsonProperty("headshot") val headshot: List? = null,
- )
-
- data class People(
- @JsonProperty("cast") val cast: List? = null,
- )
-
- data class Cast(
- @JsonProperty("character") val character: String? = null,
- @JsonProperty("characters") val characters: List? = null,
- @JsonProperty("episode_count") val episodeCount: Long? = null,
- @JsonProperty("person") val person: Person? = null,
- @JsonProperty("images") val images: Images? = null,
- )
-
- data class Person(
- @JsonProperty("name") val name: String? = null,
- @JsonProperty("ids") val ids: Ids? = null,
- @JsonProperty("images") val images: Images? = null,
- )
-
- data class Seasons(
- @JsonProperty("aired_episodes") val airedEpisodes: Int? = null,
- @JsonProperty("episode_count") val episodeCount: Int? = null,
- @JsonProperty("episodes") val episodes: List? = null,
- @JsonProperty("first_aired") val firstAired: String? = null,
- @JsonProperty("ids") val ids: Ids? = null,
- @JsonProperty("images") val images: Images? = null,
- @JsonProperty("network") val network: String? = null,
- @JsonProperty("number") val number: Int? = null,
- @JsonProperty("overview") val overview: String? = null,
- @JsonProperty("rating") val rating: Double? = null,
- @JsonProperty("title") val title: String? = null,
- @JsonProperty("updated_at") val updatedAt: String? = null,
- @JsonProperty("votes") val votes: Int? = null,
- )
-
- data class TraktEpisode(
- @JsonProperty("available_translations") val availableTranslations: List? = null,
- @JsonProperty("comment_count") val commentCount: Int? = null,
- @JsonProperty("episode_type") val episodeType: String? = null,
- @JsonProperty("first_aired") val firstAired: String? = null,
- @JsonProperty("ids") val ids: Ids? = null,
- @JsonProperty("images") val images: Images? = null,
- @JsonProperty("number") val number: Int? = null,
- @JsonProperty("number_abs") val numberAbs: Int? = null,
- @JsonProperty("overview") val overview: String? = null,
- @JsonProperty("rating") val rating: Double? = null,
- @JsonProperty("runtime") val runtime: Int? = null,
- @JsonProperty("season") val season: Int? = null,
- @JsonProperty("title") val title: String? = null,
- @JsonProperty("updated_at") val updatedAt: String? = null,
- @JsonProperty("votes") val votes: Int? = null,
- )
-
- data class LinkData(
- val id: Int? = null,
- val traktId: Int? = null,
- val traktSlug: String? = null,
- val tmdbId: Int? = null,
- val imdbId: String? = null,
- val tvdbId: Int? = null,
- val tvrageId: String? = null,
- val type: String? = null,
- val season: Int? = null,
- val episode: Int? = null,
- val aniId: String? = null,
- val animeId: String? = null,
- val title: String? = null,
- val year: Int? = null,
- val orgTitle: String? = null,
- val isAnime: Boolean = false,
- val airedYear: Int? = null,
- val lastSeason: Int? = null,
- val epsTitle: String? = null,
- val jpTitle: String? = null,
- val date: String? = null,
- val airedDate: String? = null,
- val isAsian: Boolean = false,
- val isBollywood: Boolean = false,
- val isCartoon: Boolean = false,
- )
-}
\ No newline at end of file
diff --git a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
similarity index 89%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
rename to app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
index d3b4999a..eb575775 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/mvvm/ArchComponentExt.kt
@@ -1,7 +1,10 @@
package com.lagradost.cloudstream3.mvvm
-import com.lagradost.api.BuildConfig
-import com.lagradost.api.Log
+import android.util.Log
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import com.bumptech.glide.load.HttpException
+import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.ErrorLoadingException
import kotlinx.coroutines.*
import java.io.InterruptedIOException
@@ -46,6 +49,14 @@ inline fun debugWarning(assert: () -> Boolean, message: () -> String) {
}
}
+fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) {
+ liveData.observe(this) { it?.let { t -> action(t) } }
+}
+
+fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
+ liveData.observe(this) { action(it) }
+}
+
sealed class Resource {
data class Success(val value: T) : Resource()
data class Failure(
@@ -143,14 +154,14 @@ fun throwAbleToResource(
"Connection Timeout\nPlease try again later."
)
}
-// is HttpException -> {
-// Resource.Failure(
-// false,
-// throwable.statusCode,
-// null,
-// throwable.message ?: "HttpException"
-// )
-// }
+ is HttpException -> {
+ Resource.Failure(
+ false,
+ throwable.statusCode,
+ null,
+ throwable.message ?: "HttpException"
+ )
+ }
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt b/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
deleted file mode 100644
index 3df5197c..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/mvvm/Lifecycle.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.lagradost.cloudstream3.mvvm
-
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LiveData
-
-/** NOTE: Only one observer at a time per value */
-fun LifecycleOwner.observe(liveData: LiveData, action: (t: T) -> Unit) {
- liveData.removeObservers(this)
- liveData.observe(this) { it?.let { t -> action(t) } }
-}
-
-/** NOTE: Only one observer at a time per value */
-fun LifecycleOwner.observeNullable(liveData: LiveData, action: (t: T) -> Unit) {
- liveData.removeObservers(this)
- liveData.observe(this) { action(it) }
-}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
index 85a9db5d..6950d961 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/CloudflareKiller.kt
@@ -9,10 +9,7 @@ import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.nicehttp.Requests.Companion.await
import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking
-import okhttp3.Headers
-import okhttp3.Interceptor
-import okhttp3.Request
-import okhttp3.Response
+import okhttp3.*
import java.net.URI
@@ -20,8 +17,6 @@ import java.net.URI
class CloudflareKiller : Interceptor {
companion object {
const val TAG = "CloudflareKiller"
- private val ERROR_CODES = listOf(403, 503)
- private val CLOUDFLARE_SERVERS = listOf("cloudflare-nginx", "cloudflare")
fun parseCookieMap(cookie: String): Map {
return cookie.split(";").associate {
val split = it.split("=")
@@ -53,23 +48,15 @@ class CloudflareKiller : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response = runBlocking {
val request = chain.request()
+ val cookies = savedCookies[request.url.host]
- when (val cookies = savedCookies[request.url.host]) {
- null -> {
- val response = chain.proceed(request)
- if(!(response.header("Server") in CLOUDFLARE_SERVERS && response.code in ERROR_CODES)) {
- return@runBlocking response
- } else {
- response.close()
- bypassCloudflare(request)?.let {
- Log.d(TAG, "Succeeded bypassing cloudflare: ${request.url}")
- return@runBlocking it
- }
- }
- }
- else -> {
- return@runBlocking proceed(request, cookies)
+ if (cookies == null) {
+ bypassCloudflare(request)?.let {
+ Log.d(TAG, "Succeeded bypassing cloudflare: ${request.url}")
+ return@runBlocking it
}
+ } else {
+ return@runBlocking proceed(request, cookies)
}
debugWarning({ true }) { "Failed cloudflare at: ${request.url}" }
diff --git a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt b/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt
similarity index 91%
rename from library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt
rename to app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt
index 0fbc5749..9171aed9 100644
--- a/library/src/androidMain/kotlin/com/lagradost/cloudstream3/network/WebViewResolver.android.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/network/WebViewResolver.kt
@@ -1,12 +1,11 @@
package com.lagradost.cloudstream3.network
import android.annotation.SuppressLint
-import android.content.Context
import android.net.http.SslError
-import android.os.Handler
-import android.os.Looper
import android.webkit.*
-import com.lagradost.api.getContext
+import com.lagradost.cloudstream3.AcraApplication
+import com.lagradost.cloudstream3.AcraApplication.Companion.context
+import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.logError
@@ -28,28 +27,21 @@ import java.net.URI
* @param additionalUrls this will make resolveUsingWebView also return all other requests matching the list of Regex.
* @param userAgent if null then will use the default user agent
* @param useOkhttp will try to use the okhttp client as much as possible, but this might cause some requests to fail. Disable for cloudflare.
- * @param script pass custom js to execute
- * @param scriptCallback will be called with the result from custom js
- * @param timeout close webview after timeout
* */
-actual class WebViewResolver actual constructor(
+class WebViewResolver(
val interceptUrl: Regex,
- val additionalUrls: List,
- val userAgent: String?,
- val useOkhttp: Boolean,
- val script: String?,
- val scriptCallback: ((String) -> Unit)?,
- val timeout: Long
+ val additionalUrls: List = emptyList(),
+ val userAgent: String? = USER_AGENT,
+ val useOkhttp: Boolean = true
) :
Interceptor {
- actual companion object {
+ companion object {
var webViewUserAgent: String? = null
- actual val DEFAULT_TIMEOUT = 60_000L
@JvmName("getWebViewUserAgent1")
fun getWebViewUserAgent(): String? {
- return webViewUserAgent ?: (getContext() as? Context)?.let { ctx ->
+ return webViewUserAgent ?: context?.let { ctx ->
runBlocking {
mainWork {
WebView(ctx).settings.userAgentString.also { userAgent ->
@@ -120,7 +112,7 @@ actual class WebViewResolver actual constructor(
WebView.setWebContentsDebuggingEnabled(true)
try {
webView = WebView(
- (getContext() as? Context)
+ AcraApplication.context
?: throw RuntimeException("No base context in WebViewResolver")
).apply {
// Bare minimum to bypass captcha
@@ -144,14 +136,6 @@ actual class WebViewResolver actual constructor(
val webViewUrl = request.url.toString()
println("Loading WebView URL: $webViewUrl")
- if (script != null) {
- val handler = Handler(Looper.getMainLooper())
- handler.post {
- view.evaluateJavascript("$script")
- { scriptCallback?.invoke(it) }
- }
- }
-
if (interceptUrl.containsMatchIn(webViewUrl)) {
fixedRequest = request.toRequest()?.also {
requestCallBack(it)
@@ -257,7 +241,7 @@ actual class WebViewResolver actual constructor(
var loop = 0
// Timeouts after this amount, 60s
- val totalTime = timeout
+ val totalTime = 60000L
val delayTime = 100L
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
index ddf5b286..e89ccfeb 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/CloudstreamPlugin.kt
@@ -2,4 +2,5 @@ package com.lagradost.cloudstream3.plugins
@Suppress("unused")
@Target(AnnotationTarget.CLASS)
-annotation class CloudstreamPlugin
\ No newline at end of file
+annotation class CloudstreamPlugin(
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
index fc836587..6b7dc90b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt
@@ -34,7 +34,7 @@ abstract class Plugin {
*/
fun registerMainAPI(element: MainAPI) {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
- element.sourcePlugin = this.filename
+ element.sourcePlugin = this.__filename
// Race condition causing which would case duplicates if not for distinctBy
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.add(element)
@@ -48,7 +48,7 @@ abstract class Plugin {
*/
fun registerExtractorAPI(element: ExtractorApi) {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) ExtractorApi")
- element.sourcePlugin = this.filename
+ element.sourcePlugin = this.__filename
extractorApis.add(element)
}
@@ -67,12 +67,7 @@ abstract class Plugin {
* This will contain your resources if you specified requiresResources in gradle
*/
var resources: Resources? = null
- /** Full file path to the plugin. */
- @Deprecated("Renamed to `filename` to follow conventions", replaceWith = ReplaceWith("filename"))
- var __filename: String?
- get() = filename
- set(value) {filename = value}
- var filename: String? = null
+ var __filename: String? = null
/**
* This will add a button in the settings allowing you to add custom settings
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
index bc2a1780..87b0ba3b 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt
@@ -1,25 +1,24 @@
package com.lagradost.cloudstream3.plugins
-import android.Manifest
import android.app.*
import android.content.Context
-import android.content.pm.PackageManager
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Build
import android.os.Environment
import android.util.Log
import android.widget.Toast
-import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.Gson
import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
+import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
@@ -35,7 +34,6 @@ import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
-import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@@ -139,20 +137,6 @@ object PluginManager {
}
}
- /**
- * Deletes all generated oat files which will force Android to recompile the dex extensions.
- * This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update.
- */
- fun deleteAllOatFiles(context: Context) {
- File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo ->
- repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file ->
- val success = file.deleteRecursively()
- Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success")
- }
- }
- }
-
-
fun getPluginsOnline(): Array {
return getKey(PLUGINS_KEY) ?: emptyArray()
}
@@ -166,7 +150,7 @@ object PluginManager {
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
- var currentlyLoading: String? = null
+ public var currentlyLoading: String? = null
// Maps filepath to plugin
val plugins: MutableMap =
@@ -342,7 +326,7 @@ object PluginManager {
//Omit non-NSFW if mode is set to NSFW only
if (mode == AutoDownloadMode.NsfwOnly) {
- if (!tvtypes.contains(TvType.NSFW.name)) {
+ if (tvtypes.contains(TvType.NSFW.name) == false) {
return@mapNotNull null
}
}
@@ -431,6 +415,7 @@ object PluginManager {
**/
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
val dir = File(LOCAL_PLUGINS_PATH)
+ removeKey(PLUGINS_KEY_LOCAL)
if (!dir.exists()) {
val res = dir.mkdirs()
@@ -478,14 +463,6 @@ object PluginManager {
Log.i(TAG, "Loading plugin: $data")
return try {
- // in case of android 14 then
- try {
- File(filePath).setReadOnly()
- } catch (t: Throwable) {
- Log.e(TAG, "Failed to set dex as readonly")
- logError(t)
- }
-
val loader = PathClassLoader(filePath, context.classLoader)
var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream ->
@@ -507,12 +484,10 @@ object PluginManager {
val version: Int = manifest.version ?: PLUGIN_VERSION_NOT_SET.also {
Log.d(TAG, "No manifest version for ${data.internalName}")
}
-
- @Suppress("UNCHECKED_CAST")
val pluginClass: Class<*> =
loader.loadClass(manifest.pluginClassName) as Class
val pluginInstance: Plugin =
- pluginClass.getDeclaredConstructor().newInstance() as Plugin
+ pluginClass.newInstance() as Plugin
// Sets with the proper version
setPluginData(data.copy(version = version))
@@ -522,16 +497,14 @@ object PluginManager {
return true
}
- pluginInstance.filename = file.absolutePath
+ pluginInstance.__filename = fileName
if (manifest.requiresResources) {
Log.d(TAG, "Loading resources for ${data.internalName}")
// based on https://stackoverflow.com/questions/7483568/dynamic-resource-loading-from-other-apk
- val assets = AssetManager::class.java.getDeclaredConstructor().newInstance()
+ val assets = AssetManager::class.java.newInstance()
val addAssetPath =
AssetManager::class.java.getMethod("addAssetPath", String::class.java)
addAssetPath.invoke(assets, file.absolutePath)
-
- @Suppress("DEPRECATION")
pluginInstance.resources = Resources(
assets,
context.resources.displayMetrics,
@@ -573,14 +546,14 @@ object PluginManager {
// remove all registered apis
synchronized(APIHolder.apis) {
- APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
+ APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
removePluginMapping(it)
}
}
synchronized(APIHolder.allProviders) {
- APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
+ APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
}
- extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
+ extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
classLoaders.values.removeIf { v -> v == plugin }
@@ -727,14 +700,9 @@ object PluginManager {
}
val notification = builder.build()
- // notificationId is a unique int for each notification that you must define
- if (ActivityCompat.checkSelfPermission(
- context,
- Manifest.permission.POST_NOTIFICATIONS
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- NotificationManagerCompat.from(context)
- .notify((System.currentTimeMillis() / 1000).toInt(), notification)
+ with(NotificationManagerCompat.from(context)) {
+ // notificationId is a unique int for each notification that you must define
+ notify((System.currentTimeMillis() / 1000).toInt(), notification)
}
return notification
} catch (e: Exception) {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
index c6ec9df7..b80a590e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt
@@ -73,7 +73,7 @@ object RepositoryManager {
val PREBUILT_REPOSITORIES: Array by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
}
- private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
+ val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String {
diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
index d1b702f4..a45ab5f0 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/VotingApi.kt
@@ -15,7 +15,7 @@ import kotlinx.coroutines.sync.withLock
object VotingApi { // please do not cheat the votes lol
private const val LOGKEY = "VotingApi"
- private const val API_DOMAIN = "https://counterapi.com/api"
+ private const val apiDomain = "https://counterapi.com/api"
private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest
@@ -49,13 +49,13 @@ object VotingApi { // please do not cheat the votes lol
.joinToString("-")
private suspend fun readVote(pluginUrl: String): Int {
- val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
+ var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true"
Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe()?.value ?: 0
}
private suspend fun writeVote(pluginUrl: String): Boolean {
- val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
+ var url = "${apiDomain}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}"
Log.d(LOGKEY, "Requesting: $url")
return app.get(url).parsedSafe()?.value != null
}
@@ -69,7 +69,8 @@ object VotingApi { // please do not cheat the votes lol
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false
fun canVote(pluginUrl: String): Boolean {
- return PluginManager.urlPlugins.contains(pluginUrl)
+ if (!PluginManager.urlPlugins.contains(pluginUrl)) return false
+ return true
}
private val voteLock = Mutex()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt
deleted file mode 100644
index 4ef841f5..00000000
--- a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt
+++ /dev/null
@@ -1,96 +0,0 @@
-package com.lagradost.cloudstream3.services
-
-import android.content.Context
-import androidx.core.app.NotificationCompat
-import androidx.work.Constraints
-import androidx.work.CoroutineWorker
-import androidx.work.ExistingPeriodicWorkPolicy
-import androidx.work.ForegroundInfo
-import androidx.work.PeriodicWorkRequest
-import androidx.work.WorkManager
-import androidx.work.WorkerParameters
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
-import com.lagradost.cloudstream3.utils.BackupUtils
-import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import java.util.concurrent.TimeUnit
-
-const val BACKUP_CHANNEL_ID = "cloudstream3.backups"
-const val BACKUP_WORK_NAME = "work_backup"
-const val BACKUP_CHANNEL_NAME = "Backups"
-const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups"
-const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique
-
-class BackupWorkManager(val context: Context, workerParams: WorkerParameters) :
- CoroutineWorker(context, workerParams) {
- companion object {
- fun enqueuePeriodicWork(context: Context?, intervalHours: Long) {
- if (context == null) return
-
- if (intervalHours == 0L) {
- WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME)
- return
- }
-
- val constraints = Constraints.Builder()
- .setRequiresStorageNotLow(true)
- .build()
-
- val periodicSyncDataWork =
- PeriodicWorkRequest.Builder(
- BackupWorkManager::class.java,
- intervalHours,
- TimeUnit.HOURS
- )
- .addTag(BACKUP_WORK_NAME)
- .setConstraints(constraints)
- .build()
-
- WorkManager.getInstance(context).enqueueUniquePeriodicWork(
- BACKUP_WORK_NAME,
- ExistingPeriodicWorkPolicy.UPDATE,
- periodicSyncDataWork
- )
-
- // Uncomment below for testing
-
-// val oneTimeBackupWork =
-// OneTimeWorkRequest.Builder(BackupWorkManager::class.java)
-// .addTag(BACKUP_WORK_NAME)
-// .setConstraints(constraints)
-// .build()
-//
-// WorkManager.getInstance(context).enqueue(oneTimeBackupWork)
- }
- }
-
- private val backupNotificationBuilder =
- NotificationCompat.Builder(context, BACKUP_CHANNEL_ID)
- .setColorized(true)
- .setOnlyAlertOnce(true)
- .setSilent(true)
- .setAutoCancel(true)
- .setContentTitle(context.getString(R.string.pref_category_backup))
- .setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setColor(context.colorFromAttribute(R.attr.colorPrimary))
- .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
-
- override suspend fun doWork(): Result {
- context.createNotificationChannel(
- BACKUP_CHANNEL_ID,
- BACKUP_CHANNEL_NAME,
- BACKUP_CHANNEL_DESCRIPTION
- )
-
- setForeground(
- ForegroundInfo(
- BACKUP_NOTIFICATION_ID,
- backupNotificationBuilder.build()
- )
- )
-
- BackupUtils.backup(context)
-
- return Result.success()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
index 00c74dff..adf5abfa 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
@@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.services
-import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
@@ -10,13 +9,13 @@ import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.work.*
import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.result.txt
-import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
-import com.lagradost.cloudstream3.utils.AppContextUtils.getApiDubstatusSettings
+import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
@@ -98,138 +97,128 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
)
}
- @SuppressLint("UnspecifiedImmutableFlag")
override suspend fun doWork(): Result {
- try {
// println("Update subscriptions!")
- context.createNotificationChannel(
- SUBSCRIPTION_CHANNEL_ID,
- SUBSCRIPTION_CHANNEL_NAME,
- SUBSCRIPTION_CHANNEL_DESCRIPTION
+ context.createNotificationChannel(
+ SUBSCRIPTION_CHANNEL_ID,
+ SUBSCRIPTION_CHANNEL_NAME,
+ SUBSCRIPTION_CHANNEL_DESCRIPTION
+ )
+
+ setForeground(
+ ForegroundInfo(
+ SUBSCRIPTION_NOTIFICATION_ID,
+ progressNotificationBuilder.build()
)
+ )
- setForeground(
- ForegroundInfo(
- SUBSCRIPTION_NOTIFICATION_ID,
- progressNotificationBuilder.build()
- )
- )
+ val subscriptions = getAllSubscriptions()
- val subscriptions = getAllSubscriptions()
-
- if (subscriptions.isEmpty()) {
- WorkManager.getInstance(context).cancelWorkById(this.id)
- return Result.success()
- }
-
- val max = subscriptions.size
- var progress = 0
-
- updateProgress(max, progress, true)
-
- // We need all plugins loaded.
- PluginManager.loadAllOnlinePlugins(context)
- PluginManager.loadAllLocalPlugins(context, false)
-
- subscriptions.apmap { savedData ->
- try {
- val id = savedData.id ?: return@apmap null
- val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
-
- // Reasonable timeout to prevent having this worker run forever.
- val response = withTimeoutOrNull(60_000) {
- api.load(savedData.url) as? EpisodeResponse
- } ?: return@apmap null
-
- val dubPreference =
- getDub(id) ?: if (
- context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
- ) {
- DubStatus.Dubbed
- } else {
- DubStatus.Subbed
- }
-
- val latestEpisodes = response.getLatestEpisodes()
- val latestPreferredEpisode = latestEpisodes[dubPreference]
-
- val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
- val latestSeenEpisode =
- savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
- val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
- shouldUpdate to latestPreferredEpisode
- } else {
- val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
- val latestSeenEpisode =
- savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
- val shouldUpdate = latestEpisode > latestSeenEpisode
- shouldUpdate to latestEpisode
- }
-
- DataStoreHelper.updateSubscribedData(
- id,
- savedData,
- response
- )
-
- if (shouldUpdate) {
- val updateHeader = savedData.name
- val updateDescription = txt(
- R.string.subscription_episode_released,
- latestEpisode,
- savedData.name
- ).asString(context)
-
- val intent = Intent(context, MainActivity::class.java).apply {
- data = savedData.url.toUri()
- flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
- }
-
- val pendingIntent =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- PendingIntent.getActivity(
- context,
- 0,
- intent,
- PendingIntent.FLAG_IMMUTABLE
- )
- } else {
- PendingIntent.getActivity(context, 0, intent, 0)
- }
-
- val poster = ioWork {
- savedData.posterUrl?.let { url ->
- context.getImageBitmapFromUrl(
- url,
- savedData.posterHeaders
- )
- }
- }
-
- val updateNotification =
- updateNotificationBuilder.setContentTitle(updateHeader)
- .setContentText(updateDescription)
- .setContentIntent(pendingIntent)
- .setLargeIcon(poster)
- .build()
-
- notificationManager.notify(id, updateNotification)
- }
-
- // You can probably get some issues here since this is async but it does not matter much.
- updateProgress(max, ++progress, false)
- } catch (t: Throwable) {
- logError(t)
- }
- }
-
- return Result.success()
- } catch (t: Throwable) {
- logError(t)
- // ye, while this is not correct, but because gods know why android just crashes
- // and this causes major battery usage as it retries it inf times. This is better, just
- // in case android decides to be android and fuck us
+ if (subscriptions.isEmpty()) {
+ WorkManager.getInstance(context).cancelWorkById(this.id)
return Result.success()
}
+
+ val max = subscriptions.size
+ var progress = 0
+
+ updateProgress(max, progress, true)
+
+ // We need all plugins loaded.
+ PluginManager.loadAllOnlinePlugins(context)
+ PluginManager.loadAllLocalPlugins(context, false)
+
+ subscriptions.apmap { savedData ->
+ try {
+ val id = savedData.id ?: return@apmap null
+ val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
+
+ // Reasonable timeout to prevent having this worker run forever.
+ val response = withTimeoutOrNull(60_000) {
+ api.load(savedData.url) as? EpisodeResponse
+ } ?: return@apmap null
+
+ val dubPreference =
+ getDub(id) ?: if (
+ context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
+ ) {
+ DubStatus.Dubbed
+ } else {
+ DubStatus.Subbed
+ }
+
+ val latestEpisodes = response.getLatestEpisodes()
+ val latestPreferredEpisode = latestEpisodes[dubPreference]
+
+ val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
+ val latestSeenEpisode =
+ savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
+ val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
+ shouldUpdate to latestPreferredEpisode
+ } else {
+ val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
+ val latestSeenEpisode =
+ savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
+ val shouldUpdate = latestEpisode > latestSeenEpisode
+ shouldUpdate to latestEpisode
+ }
+
+ DataStoreHelper.updateSubscribedData(
+ id,
+ savedData,
+ response
+ )
+
+ if (shouldUpdate) {
+ val updateHeader = savedData.name
+ val updateDescription = txt(
+ R.string.subscription_episode_released,
+ latestEpisode,
+ savedData.name
+ ).asString(context)
+
+ val intent = Intent(context, MainActivity::class.java).apply {
+ data = savedData.url.toUri()
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+
+ val pendingIntent =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ } else {
+ PendingIntent.getActivity(context, 0, intent, 0)
+ }
+
+ val poster = ioWork {
+ savedData.posterUrl?.let { url ->
+ context.getImageBitmapFromUrl(
+ url,
+ savedData.posterHeaders
+ )
+ }
+ }
+
+ val updateNotification =
+ updateNotificationBuilder.setContentTitle(updateHeader)
+ .setContentText(updateDescription)
+ .setContentIntent(pendingIntent)
+ .setLargeIcon(poster)
+ .build()
+
+ notificationManager.notify(id, updateNotification)
+ }
+
+ // You can probably get some issues here since this is async but it does not matter much.
+ updateProgress(max, ++progress, false)
+ } catch (_: Throwable) {
+ }
+ }
+
+ return Result.success()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
index df64caab..77a1b0b5 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubProvider.kt
@@ -1,23 +1,11 @@
package com.lagradost.cloudstream3.subtitles
import androidx.annotation.WorkerThread
-import androidx.core.net.toUri
-import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
-import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.syncproviders.AuthAPI
-import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
-import okio.BufferedSource
-import okio.buffer
-import okio.sink
-import okio.source
-import java.io.File
-import java.util.zip.ZipInputStream
interface AbstractSubProvider {
- val idPrefix: String
-
@WorkerThread
suspend fun search(query: SubtitleSearch): List? {
throw NotImplementedError()
@@ -27,98 +15,6 @@ interface AbstractSubProvider {
suspend fun load(data: SubtitleEntity): String? {
throw NotImplementedError()
}
-
- @WorkerThread
- suspend fun SubtitleResource.getResources(data: SubtitleEntity) {
- this.addUrl(load(data))
- }
-
- @WorkerThread
- suspend fun getResource(data: SubtitleEntity): SubtitleResource {
- return SubtitleResource().apply {
- this.getResources(data)
- }
- }
-}
-
-/**
- * A builder for subtitle files.
- * @see addUrl
- * @see addFile
- */
-class SubtitleResource {
- fun downloadFile(source: BufferedSource): File {
- val file = File.createTempFile("temp-subtitle", ".tmp").apply {
- deleteFileOnExit(this)
- }
- val sink = file.sink().buffer()
- sink.writeAll(source)
- sink.close()
- source.close()
-
- return file
- }
-
- private fun unzip(file: File): List> {
- val entries = mutableListOf>()
-
- ZipInputStream(file.inputStream()).use { zipInputStream ->
- var zipEntry = zipInputStream.nextEntry
-
- while (zipEntry != null) {
- val tempFile = File.createTempFile("unzipped-subtitle", ".tmp").apply {
- deleteFileOnExit(this)
- }
- entries.add(zipEntry.name to tempFile)
-
- tempFile.sink().buffer().use { buffer ->
- buffer.writeAll(zipInputStream.source())
- }
-
- zipEntry = zipInputStream.nextEntry
- }
- }
- return entries
- }
-
- data class SingleSubtitleResource(
- val name: String?,
- val url: String,
- val origin: SubtitleOrigin
- )
-
- private var resources: MutableList = mutableListOf()
-
- fun getSubtitles(): List {
- return resources.toList()
- }
-
- fun addUrl(url: String?, name: String? = null) {
- if (url == null) return
- this.resources.add(
- SingleSubtitleResource(name, url, SubtitleOrigin.URL)
- )
- }
-
- fun addFile(file: File, name: String? = null) {
- this.resources.add(
- SingleSubtitleResource(name, file.toUri().toString(), SubtitleOrigin.DOWNLOADED_FILE)
- )
- deleteFileOnExit(file)
- }
-
- suspend fun addZipUrl(
- url: String,
- nameGenerator: (String, File) -> String? = { _, _ -> null }
- ) {
- val source = app.get(url).okhttpResponse.body.source()
- val zip = downloadFile(source)
- val realFiles = unzip(zip)
- zip.deleteRecursively()
- realFiles.forEach { (name, subtitleFile) ->
- addFile(subtitleFile, nameGenerator(name, subtitleFile))
- }
- }
}
interface AbstractSubApi : AbstractSubProvider, AuthAPI
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
index 685b499b..f6424c4c 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/subtitles/AbstractSubtitleEntities.kt
@@ -19,11 +19,8 @@ class AbstractSubtitleEntities {
data class SubtitleSearch(
var query: String = "",
+ var imdb: Long? = null,
var lang: String? = null,
- var imdbId: String? = null,
- var tmdbId: Int? = null,
- var malId: Int? = null,
- var aniListId: Int? = null,
var epNumber: Int? = null,
var seasonNumber: Int? = null,
var year: Int? = null
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
index 2e14c3c4..8bf8dffa 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/AccountManager.kt
@@ -3,26 +3,18 @@ package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
-import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.syncproviders.providers.*
import java.util.concurrent.TimeUnit
abstract class AccountManager(private val defIndex: Int) : AuthAPI {
companion object {
- val malApi = MALApi(0).also { api ->
- LoadResponse.Companion.malIdPrefix = api.idPrefix
- }
- val aniListApi = AniListApi(0).also { api ->
- LoadResponse.Companion.aniListIdPrefix = api.idPrefix
- }
- val simklApi = SimklApi(0).also { api ->
- LoadResponse.Companion.simklIdPrefix = api.idPrefix
- }
+ val malApi = MALApi(0)
+ val aniListApi = AniListApi(0)
val openSubtitlesApi = OpenSubtitlesApi(0)
+ val simklApi = SimklApi(0)
+ val indexSubtitlesApi = IndexSubtitleApi()
val addic7ed = Addic7ed()
- val subDlApi = SubDlApi(0)
val localListApi = LocalList()
- val subSourceApi = SubSourceApi()
// used to login via app intent
val OAuth2Apis
@@ -33,7 +25,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// this needs init with context and can be accessed in settings
val accountManagers
get() = listOf(
- malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi
+ malApi, aniListApi, openSubtitlesApi, simklApi //nginxApi
)
// used for active syncing
@@ -43,35 +35,31 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
)
val inAppAuths
- get() = listOf(
- openSubtitlesApi,
- subDlApi
- )//, nginxApi)
+ get() = listOf(openSubtitlesApi)//, nginxApi)
val subtitleProviders
get() = listOf(
openSubtitlesApi,
- addic7ed,
- subDlApi,
- subSourceApi
+ indexSubtitlesApi, // they got anti scraping measures in place :(
+ addic7ed
)
- const val APP_STRING = "cloudstreamapp"
- const val APP_STRING_REPO = "cloudstreamrepo"
- const val APP_STRING_PLAYER = "cloudstreamplayer"
+ const val appString = "cloudstreamapp"
+ const val appStringRepo = "cloudstreamrepo"
+ const val appStringPlayer = "cloudstreamplayer"
// Instantly start the search given a query
- const val APP_STRING_SEARCH = "cloudstreamsearch"
+ const val appStringSearch = "cloudstreamsearch"
// Instantly resume watching a show
- const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
+ const val appStringResumeWatching = "cloudstreamcontinuewatching"
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
get() = System.currentTimeMillis()
- const val MAX_STALE = 60 * 10
+ const val maxStale = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong()
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt
index 3d0bb940..ef74edfc 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/OAuth2API.kt
@@ -5,23 +5,7 @@ import androidx.fragment.app.FragmentActivity
interface OAuth2API : AuthAPI {
val key: String
val redirectUrl: String
- val supportDeviceAuth: Boolean
suspend fun handleRedirect(url: String) : Boolean
fun authenticate(activity: FragmentActivity?)
- suspend fun getDevicePin() : PinAuthData? {
- return null
- }
-
- suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean {
- return false
- }
-
- data class PinAuthData(
- val deviceCode: String,
- val userCode: String,
- val verificationUrl: String,
- val expiresIn: Int,
- val interval: Int,
- )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt
similarity index 91%
rename from app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
rename to app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt
index dcb8bbea..ed496326 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/SyncAPI.kt
@@ -1,11 +1,18 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.*
-import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
-import java.util.Date
+
+enum class SyncIdName {
+ Anilist,
+ MyAnimeList,
+ Trakt,
+ Imdb,
+ Simkl,
+ LocalList,
+}
interface SyncAPI : OAuth2API {
/**
@@ -54,7 +61,7 @@ interface SyncAPI : OAuth2API {
) : SearchResponse
abstract class AbstractSyncStatus {
- abstract var status: SyncWatchType
+ abstract var status: Int
/** 1-10 */
abstract var score: Int?
@@ -63,9 +70,8 @@ interface SyncAPI : OAuth2API {
abstract var maxEpisodes: Int?
}
-
data class SyncStatus(
- override var status: SyncWatchType,
+ override var status: Int,
/** 1-10 */
override var score: Int?,
override var watchedEpisodes: Int?,
@@ -125,8 +131,6 @@ interface SyncAPI : OAuth2API {
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
- ListSorting.ReleaseDateNew -> items.sortedByDescending { it.releaseDate }
- ListSorting.ReleaseDateOld -> items.sortedBy { it.releaseDate }
else -> items
}
}
@@ -161,10 +165,6 @@ interface SyncAPI : OAuth2API {
override var posterUrl: String?,
override var posterHeaders: Map?,
override var quality: SearchQuality?,
- val releaseDate: Date?,
override var id: Int? = null,
- val plot : String? = null,
- val rating: Int? = null,
- val tags: List? = null
) : SearchResponse
}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
index db467639..507c5e2a 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/Addic7ed.kt
@@ -18,13 +18,13 @@ class Addic7ed : AbstractSubApi {
override fun logOut() {}
companion object {
- const val HOST = "https://www.addic7ed.com"
+ const val host = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED"
}
private fun fixUrl(url: String): String {
- return if (url.startsWith("/")) HOST + url
- else if (!url.startsWith("http")) "$HOST/$url"
+ return if (url.startsWith("/")) host + url
+ else if (!url.startsWith("http")) "$host/$url"
else url
}
@@ -62,7 +62,7 @@ class Addic7ed : AbstractSubApi {
}
val title = queryText.substringBefore("(").trim()
- val url = "$HOST/search.php?search=${title}&Submit=Search"
+ val url = "$host/search.php?search=${title}&Submit=Search"
val hostDocument = app.get(url).document
var searchResult = ""
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
@@ -74,8 +74,8 @@ class Addic7ed : AbstractSubApi {
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
?.substringBefore(",")
val doc = app.get(
- "$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
- referer = "$HOST/"
+ "$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
+ referer = "$host/"
).document
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
if (node.selectFirst("td")?.text()
@@ -97,7 +97,7 @@ class Addic7ed : AbstractSubApi {
val link = fixUrl(node.select("a.buttonDownload").attr("href"))
val isHearingImpaired =
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
- cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired)
+ cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
}
return results
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
index 6112c7db..d0c88901 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/AniListApi.kt
@@ -13,19 +13,17 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
-import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
-import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
-import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
import java.net.URL
import java.net.URLEncoder
-import java.util.Locale
+import java.util.*
class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override var name = "AniList"
@@ -33,7 +31,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val redirectUrl = "anilistlogin"
override val idPrefix = "anilist"
override var requireLibraryRefresh = true
- override val supportDeviceAuth = false
override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon
override val requiresLogin = false
@@ -64,7 +61,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleRedirect(url: String): Boolean {
val sanitizer =
- splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
+ splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val token = sanitizer["access_token"]!!
val expiresIn = sanitizer["expires_in"]!!
@@ -88,7 +85,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun search(name: String): List? {
val data = searchShows(name) ?: return null
- return data.data?.page?.media?.map {
+ return data.data?.Page?.media?.map {
SyncAPI.SyncSearchResult(
it.title.romaji ?: return null,
this.name,
@@ -102,7 +99,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getResult(id: String): SyncAPI.SyncResult {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
- val season = getSeason(internalId).data.media
+ val season = getSeason(internalId).data.Media
return SyncAPI.SyncResult(
season.id.toString(),
@@ -168,7 +165,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return SyncAPI.SyncStatus(
score = data.score,
watchedEpisodes = data.progress,
- status = SyncWatchType.fromInternalId(data.type?.value ?: return null),
+ status = data.type?.value ?: return null,
isFavorite = data.isFavourite,
maxEpisodes = data.episodes,
)
@@ -177,7 +174,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
return postDataAboutId(
id.toIntOrNull() ?: return false,
- fromIntToAnimeStatus(status.status.internalId),
+ fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
).also {
@@ -302,12 +299,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
//println("NAME $name NEW NAME ${name.replace(blackListRegex, "")}")
val shows = searchShows(name.replace(blackListRegex, ""))
- shows?.data?.page?.media?.find {
+ shows?.data?.Page?.media?.find {
(malId ?: "NONE") == it.idMal.toString()
}?.let { return it }
val filtered =
- shows?.data?.page?.media?.filter {
+ shows?.data?.Page?.media?.filter {
(((it.startDate.year ?: year.toString()) == year.toString()
|| year == null))
}
@@ -497,7 +494,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q, true)
val d = parseJson(data ?: return null)
- val main = d.data?.media
+ val main = d.data?.Media
if (main?.mediaListEntry != null) {
return AniListTitleHolder(
title = main.title,
@@ -537,7 +534,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
headers = mapOf(
"Authorization" to "Bearer " + (getAuth()
?: return@suspendSafeApiCall null),
- if (cache) "Cache-Control" to "max-stale=$MAX_STALE" else "Cache-Control" to "no-cache"
+ if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
),
cacheTime = 0,
data = mapOf(
@@ -598,7 +595,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
//@JsonProperty("source") val source: String,
@JsonProperty("episodes") val episodes: Int,
@JsonProperty("title") val title: Title,
- @JsonProperty("description") val description: String?,
+ //@JsonProperty("description") val description: String,
@JsonProperty("coverImage") val coverImage: CoverImage,
@JsonProperty("synonyms") val synonyms: List,
@JsonProperty("nextAiringEpisode") val nextAiringEpisode: SeasonNextAiringEpisode?,
@@ -632,9 +629,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
?: this.media.coverImage.medium,
null,
null,
- this.media.seasonYear.toYear(),
- null,
- plot = this.media.description,
+ null
)
}
}
@@ -649,7 +644,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class Data(
- @JsonProperty("MediaListCollection") val mediaListCollection: MediaListCollection
+ @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
)
private fun getAniListListCached(): Array? {
@@ -661,7 +656,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
if (checkToken()) return null
return if (requireLibraryRefresh) {
- val list = getFullAniListList()?.data?.mediaListCollection?.lists?.toTypedArray()
+ val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
if (list != null) {
setKey(ANILIST_CACHED_LIST, list)
}
@@ -680,7 +675,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
// To fill empty lists when AniList does not return them
val baseMap =
- AniListStatusType.entries.filter { it.value >= 0 }.associate {
+ AniListStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList()
}
@@ -691,8 +686,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
- ListSorting.ReleaseDateNew,
- ListSorting.ReleaseDateOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
@@ -768,7 +761,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
/** Used to query a saved MediaItem on the list to get the id for removal */
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
- data class MediaListItem(@JsonProperty("MediaList") val mediaList: MediaListId? = null)
+ data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId(
@@ -791,7 +784,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
"""
val response = postApi(idQuery)
val listId =
- tryParseJson(response)?.data?.mediaList?.id ?: return false
+ tryParseJson(response)?.data?.MediaList?.id ?: return false
"""
mutation(${'$'}id: Int = $listId) {
DeleteMediaListEntry(id: ${'$'}id) {
@@ -840,7 +833,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val data = postApi(q)
if (data.isNullOrBlank()) return null
val userData = parseJson(data)
- val u = userData.data?.viewer
+ val u = userData.data?.Viewer
val user = AniListUser(
u?.id,
u?.name,
@@ -862,8 +855,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
suspend fun getSeasonRecursive(id: Int) {
val season = getSeason(id)
seasons.add(season)
- if (season.data.media.format?.startsWith("TV") == true) {
- season.data.media.relations?.edges?.forEach {
+ if (season.data.Media.format?.startsWith("TV") == true) {
+ season.data.Media.relations?.edges?.forEach {
if (it.node?.format != null) {
if (it.relationType == "SEQUEL" && it.node.format.startsWith("TV")) {
getSeasonRecursive(it.node.id)
@@ -882,7 +875,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class SeasonData(
- @JsonProperty("Media") val media: SeasonMedia,
+ @JsonProperty("Media") val Media: SeasonMedia,
)
data class SeasonMedia(
@@ -1054,7 +1047,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class AniListData(
- @JsonProperty("Viewer") val viewer: AniListViewer?,
+ @JsonProperty("Viewer") val Viewer: AniListViewer?,
)
data class AniListRoot(
@@ -1094,7 +1087,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class LikeData(
- @JsonProperty("Viewer") val viewer: LikeViewer?,
+ @JsonProperty("Viewer") val Viewer: LikeViewer?,
)
data class LikeRoot(
@@ -1134,7 +1127,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class GetDataData(
- @JsonProperty("Media") val media: GetDataMedia?,
+ @JsonProperty("Media") val Media: GetDataMedia?,
)
data class GetDataRoot(
@@ -1167,7 +1160,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
)
data class GetSearchPage(
- @JsonProperty("Page") val page: GetSearchData?,
+ @JsonProperty("Page") val Page: GetSearchData?,
)
data class GetSearchData(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt
index 94537ea3..7ec168da 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/DropboxApi.kt
@@ -11,7 +11,6 @@ class Dropbox : OAuth2API {
override val key = "zlqsamadlwydvb2"
override val redirectUrl = "dropboxlogin"
override val requiresLogin = true
- override val supportDeviceAuth = false
override val createAccountUrl: String? = null
override val icon: Int
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt
new file mode 100644
index 00000000..668d10bd
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/IndexSubtitleApi.kt
@@ -0,0 +1,265 @@
+package com.lagradost.cloudstream3.syncproviders.providers
+
+import android.util.Log
+import com.lagradost.cloudstream3.TvType
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.imdbUrlToIdNullable
+import com.lagradost.cloudstream3.subtitles.AbstractSubApi
+import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
+import com.lagradost.cloudstream3.utils.SubtitleHelper
+
+class IndexSubtitleApi : AbstractSubApi {
+ override val name = "IndexSubtitle"
+ override val idPrefix = "indexsubtitle"
+ override val requiresLogin = false
+ override val icon: Nothing? = null
+ override val createAccountUrl: Nothing? = null
+
+ override fun loginInfo(): Nothing? = null
+
+ override fun logOut() {}
+
+
+ companion object {
+ const val host = "https://indexsubtitle.com"
+ const val TAG = "INDEXSUBS"
+ }
+
+ private fun fixUrl(url: String): String {
+ if (url.startsWith("http")) {
+ return url
+ }
+ if (url.isEmpty()) {
+ return ""
+ }
+
+ val startsWithNoHttp = url.startsWith("//")
+ if (startsWithNoHttp) {
+ return "https:$url"
+ } else {
+ if (url.startsWith('/')) {
+ return host + url
+ }
+ return "$host/$url"
+ }
+ }
+
+ private fun getOrdinal(num: Int?): String? {
+ return when (num) {
+ 1 -> "First"
+ 2 -> "Second"
+ 3 -> "Third"
+ 4 -> "Fourth"
+ 5 -> "Fifth"
+ 6 -> "Sixth"
+ 7 -> "Seventh"
+ 8 -> "Eighth"
+ 9 -> "Ninth"
+ 10 -> "Tenth"
+ 11 -> "Eleventh"
+ 12 -> "Twelfth"
+ 13 -> "Thirteenth"
+ 14 -> "Fourteenth"
+ 15 -> "Fifteenth"
+ 16 -> "Sixteenth"
+ 17 -> "Seventeenth"
+ 18 -> "Eighteenth"
+ 19 -> "Nineteenth"
+ 20 -> "Twentieth"
+ 21 -> "Twenty-First"
+ 22 -> "Twenty-Second"
+ 23 -> "Twenty-Third"
+ 24 -> "Twenty-Fourth"
+ 25 -> "Twenty-Fifth"
+ 26 -> "Twenty-Sixth"
+ 27 -> "Twenty-Seventh"
+ 28 -> "Twenty-Eighth"
+ 29 -> "Twenty-Ninth"
+ 30 -> "Thirtieth"
+ 31 -> "Thirty-First"
+ 32 -> "Thirty-Second"
+ 33 -> "Thirty-Third"
+ 34 -> "Thirty-Fourth"
+ 35 -> "Thirty-Fifth"
+ else -> null
+ }
+ }
+
+ private fun isRightEps(text: String, seasonNum: Int?, epNum: Int?): Boolean {
+ val FILTER_EPS_REGEX =
+ Regex("(?i)((Chapter\\s?0?${epNum})|((Season)?\\s?0?${seasonNum}?\\s?(Episode)\\s?0?${epNum}[^0-9]))|(?i)((S?0?${seasonNum}?E0?${epNum}[^0-9])|(0?${seasonNum}[a-z]0?${epNum}[^0-9]))")
+ return text.contains(FILTER_EPS_REGEX)
+ }
+
+ private fun haveEps(text: String): Boolean {
+ val HAVE_EPS_REGEX =
+ Regex("(?i)((Chapter\\s?0?\\d)|((Season)?\\s?0?\\d?\\s?(Episode)\\s?0?\\d))|(?i)((S?0?\\d?E0?\\d)|(0?\\d[a-z]0?\\d))")
+ return text.contains(HAVE_EPS_REGEX)
+ }
+
+ override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List {
+ val imdbId = query.imdb ?: 0
+ val lang = query.lang
+ val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
+ val queryText = query.query
+ val epNum = query.epNumber ?: 0
+ val seasonNum = query.seasonNumber ?: 0
+ val yearNum = query.year ?: 0
+
+ val urlItems = ArrayList()
+
+ fun cleanResources(
+ results: MutableList,
+ name: String,
+ link: String
+ ) {
+ results.add(
+ AbstractSubtitleEntities.SubtitleEntity(
+ idPrefix = idPrefix,
+ name = name,
+ lang = queryLang.toString(),
+ data = link,
+ source = this.name,
+ type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
+ epNumber = epNum,
+ seasonNumber = seasonNum,
+ year = yearNum,
+ )
+ )
+ }
+
+ val document = app.get("$host/?search=$queryText").document
+
+ document.select("div.my-3.p-3 div.media").map { block ->
+ if (seasonNum > 0) {
+ val name = block.select("strong.text-primary, strong.text-info").text().trim()
+ val season = getOrdinal(seasonNum)
+ if ((block.selectFirst("a")?.attr("href")
+ ?.contains(
+ "$season",
+ ignoreCase = true
+ )!! || name.contains(
+ "$season",
+ ignoreCase = true
+ )) && name.contains(queryText, ignoreCase = true)
+ ) {
+ block.select("div.media").mapNotNull {
+ urlItems.add(
+ fixUrl(
+ it.selectFirst("a")!!.attr("href")
+ )
+ )
+ }
+ }
+ } else {
+ if (block.selectFirst("strong")!!.text().trim()
+ .matches(Regex("(?i)^$queryText\$"))
+ ) {
+ if (block.select("span[title=Release]").isNullOrEmpty()) {
+ block.select("div.media").mapNotNull {
+ val urlItem = fixUrl(
+ it.selectFirst("a")!!.attr("href")
+ )
+ val itemDoc = app.get(urlItem).document
+ val id = imdbUrlToIdNullable(
+ itemDoc.selectFirst("div.d-flex span.badge.badge-primary")?.parent()
+ ?.attr("href")
+ )?.toLongOrNull()
+ val year = itemDoc.selectFirst("div.d-flex span.badge.badge-success")
+ ?.ownText()
+ ?.trim().toString()
+ Log.i(TAG, "id => $id \nyear => $year||$yearNum")
+ if (imdbId > 0) {
+ if (id == imdbId) {
+ urlItems.add(urlItem)
+ }
+ } else {
+ if (year.contains("$yearNum")) {
+ urlItems.add(urlItem)
+ }
+ }
+ }
+ } else {
+ if (block.select("span[title=Release]").text().trim()
+ .contains("$yearNum")
+ ) {
+ block.select("div.media").mapNotNull {
+ urlItems.add(
+ fixUrl(
+ it.selectFirst("a")!!.attr("href")
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ Log.i(TAG, "urlItems => $urlItems")
+ val results = mutableListOf()
+
+ urlItems.forEach { url ->
+ val request = app.get(url)
+ if (request.isSuccessful) {
+ request.document.select("div.my-3.p-3 div.media").map { block ->
+ if (block.select("span.d-block span[data-original-title=Language]").text()
+ .trim()
+ .contains("$queryLang")
+ ) {
+ var name = block.select("strong.text-primary, strong.text-info").text().trim()
+ val link = fixUrl(block.selectFirst("a")!!.attr("href"))
+ if (seasonNum > 0) {
+ when {
+ isRightEps(name, seasonNum, epNum) -> {
+ cleanResources(results, name, link)
+ }
+ !(haveEps(name)) -> {
+ name = "$name (S${seasonNum}:E${epNum})"
+ cleanResources(results, name, link)
+ }
+ }
+ } else {
+ cleanResources(results, name, link)
+ }
+ }
+ }
+ }
+ }
+ return results
+ }
+
+ override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? {
+ val seasonNum = data.seasonNumber
+ val epNum = data.epNumber
+
+ val req = app.get(data.data)
+
+ if (req.isSuccessful) {
+ val document = req.document
+ val link = if (document.select("div.my-3.p-3 div.media").size == 1) {
+ fixUrl(
+ document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
+ )
+ } else {
+ document.select("div.my-3.p-3 div.media").firstNotNullOf { block ->
+ val name =
+ block.selectFirst("strong.d-block")?.text()?.trim().toString()
+ if (seasonNum!! > 0) {
+ if (isRightEps(name, seasonNum, epNum)) {
+ fixUrl(block.selectFirst("a")!!.attr("href"))
+ } else {
+ null
+ }
+ } else {
+ fixUrl(block.selectFirst("a")!!.attr("href"))
+ }
+ }
+ }
+ return link
+ }
+
+ return null
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
index 0d9a4d13..e6ca9711 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt
@@ -8,10 +8,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
-import com.lagradost.cloudstream3.ui.settings.Globals.TV
-import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
-import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
@@ -21,7 +18,6 @@ class LocalList : SyncAPI {
override val name = "Local"
override val icon: Int = R.drawable.ic_baseline_storage_24
override val requiresLogin = false
- override val supportDeviceAuth = false
override val createAccountUrl: Nothing? = null
override val idPrefix = "local"
override var requireLibraryRefresh = true
@@ -73,57 +69,31 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null
val list = ioWork {
- val isTrueTv = isLayout(TV)
-
- val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate {
- // None is not something to display
- it.stringRes to emptyList()
- } + mapOf(
- R.string.favorites_list_name to emptyList()
- ) + if (!isTrueTv) {
- mapOf(
- R.string.subscription_list_name to emptyList()
- )
- } else {
- emptyMap()
- }
-
- val watchStatusMap = watchStatusIds.groupBy { it.second.stringRes }.mapValues { group ->
+ watchStatusIds.groupBy {
+ it.second.stringRes
+ }.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
- }
-
- val favoritesMap = mapOf(R.string.favorites_list_name to getAllFavorites().mapNotNull {
+ } + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem()
})
-
- // Don't show subscriptions on TV
- val result = if (isTrueTv) {
- baseMap + watchStatusMap + favoritesMap
- } else {
- val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
- it.toLibraryItem()
- })
-
- baseMap + watchStatusMap + subscriptionsMap + favoritesMap
- }
-
- result
}
+ val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
+ // None is not something to display
+ it.stringRes to emptyList()
+ } + mapOf(R.string.subscription_list_name to emptyList())
+
return SyncAPI.LibraryMetadata(
- list.map { SyncAPI.LibraryList(txt(it.key), it.value) },
+ (baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
- ListSorting.UpdatedNew,
- ListSorting.UpdatedOld,
- ListSorting.ReleaseDateNew,
- ListSorting.ReleaseDateOld,
+// ListSorting.UpdatedNew,
+// ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
-
)
)
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt
index 08c18653..02826401 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/MALApi.kt
@@ -16,22 +16,16 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
-import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
-import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import java.net.URL
import java.security.SecureRandom
import java.text.ParseException
import java.text.SimpleDateFormat
-import java.time.Instant
-import java.time.format.DateTimeFormatter
-import java.util.Calendar
-import java.util.Date
-import java.util.Locale
-import java.util.TimeZone
+import java.util.*
/** max 100 via https://myanimelist.net/apiconfig/references/api/v2#tag/anime */
const val MAL_MAX_SEARCH_LIMIT = 25
@@ -45,7 +39,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private val apiUrl = "https://api.myanimelist.net"
override val icon = R.drawable.mal_logo
override val requiresLogin = false
- override val supportDeviceAuth = false
override val syncIdName = SyncIdName.MyAnimeList
override var requireLibraryRefresh = true
override val createAccountUrl = "$mainUrl/register.php"
@@ -56,6 +49,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
override fun loginInfo(): AuthAPI.LoginInfo? {
+ //getMalUser(true)?
getKey(accountId, MAL_USER_KEY)?.let { user ->
return AuthAPI.LoginInfo(
profilePicture = user.picture,
@@ -88,7 +82,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
this.name,
node.id.toString(),
"$mainUrl/anime/${node.id}/",
- node.mainPicture?.large ?: node.mainPicture?.medium
+ node.main_picture?.large ?: node.main_picture?.medium
)
}
}
@@ -100,7 +94,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
return setScoreRequest(
id.toIntOrNull() ?: return false,
- fromIntToAnimeStatus(status.status.internalId),
+ fromIntToAnimeStatus(status.status),
status.score,
status.watchedEpisodes
).also {
@@ -182,7 +176,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun parseDate(string: String?): Long? {
return try {
- SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(string ?: return null)?.time
+ SimpleDateFormat("yyyy-MM-dd")?.parse(string ?: return null)?.time
} catch (e: Exception) {
null
}
@@ -194,7 +188,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
apiName = this.name,
syncId = node.id.toString(),
url = "$mainUrl/anime/${node.id}",
- posterUrl = node.mainPicture?.large
+ posterUrl = node.main_picture?.large
)
}
@@ -248,12 +242,12 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val internalId = id.toIntOrNull() ?: return null
val data =
- getDataAboutMalId(internalId)?.myListStatus //?: throw ErrorLoadingException("No my_list_status")
+ getDataAboutMalId(internalId)?.my_list_status //?: throw ErrorLoadingException("No my_list_status")
return SyncAPI.SyncStatus(
score = data?.score,
- status = SyncWatchType.fromInternalId(malStatusAsString.indexOf(data?.status)),
+ status = malStatusAsString.indexOf(data?.status),
isFavorite = null,
- watchedEpisodes = data?.numEpisodesWatched,
+ watchedEpisodes = data?.num_episodes_watched,
)
}
@@ -295,7 +289,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
private fun parseDateLong(string: String?): Long? {
return try {
- SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(
+ SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {
@@ -306,7 +300,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun handleRedirect(url: String): Boolean {
val sanitizer =
- splitQuery(URL(url.replace(APP_STRING, "https").replace("/#", "?"))) // FIX ERROR
+ splitQuery(URL(url.replace(appString, "https").replace("/#", "?"))) // FIX ERROR
val state = sanitizer["state"]!!
if (state == "RequestID$requestId") {
val currentCode = sanitizer["code"]!!
@@ -355,9 +349,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
try {
if (response != "") {
val token = parseJson(response)
- setKey(accountId, MAL_UNIXTIME_KEY, (token.expiresIn + unixTime))
- setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refreshToken)
- setKey(accountId, MAL_TOKEN_KEY, token.accessToken)
+ setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
+ setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
+ setKey(accountId, MAL_TOKEN_KEY, token.access_token)
requireLibraryRefresh = true
}
} catch (e: Exception) {
@@ -399,62 +393,55 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class Node(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String,
- @JsonProperty("main_picture") val mainPicture: MainPicture?,
- @JsonProperty("alternative_titles") val alternativeTitles: AlternativeTitles?,
- @JsonProperty("media_type") val mediaType: String?,
- @JsonProperty("num_episodes") val numEpisodes: Int?,
+ @JsonProperty("main_picture") val main_picture: MainPicture?,
+ @JsonProperty("alternative_titles") val alternative_titles: AlternativeTitles?,
+ @JsonProperty("media_type") val media_type: String?,
+ @JsonProperty("num_episodes") val num_episodes: Int?,
@JsonProperty("status") val status: String?,
- @JsonProperty("start_date") val startDate: String?,
- @JsonProperty("end_date") val endDate: String?,
- @JsonProperty("average_episode_duration") val averageEpisodeDuration: Int?,
+ @JsonProperty("start_date") val start_date: String?,
+ @JsonProperty("end_date") val end_date: String?,
+ @JsonProperty("average_episode_duration") val average_episode_duration: Int?,
@JsonProperty("synopsis") val synopsis: String?,
@JsonProperty("mean") val mean: Double?,
@JsonProperty("genres") val genres: List?,
@JsonProperty("rank") val rank: Int?,
@JsonProperty("popularity") val popularity: Int?,
- @JsonProperty("num_list_users") val numListUsers: Int?,
- @JsonProperty("num_favorites") val numFavorites: Int?,
- @JsonProperty("num_scoring_users") val numScoringUsers: Int?,
- @JsonProperty("start_season") val startSeason: StartSeason?,
+ @JsonProperty("num_list_users") val num_list_users: Int?,
+ @JsonProperty("num_favorites") val num_favorites: Int?,
+ @JsonProperty("num_scoring_users") val num_scoring_users: Int?,
+ @JsonProperty("start_season") val start_season: StartSeason?,
@JsonProperty("broadcast") val broadcast: Broadcast?,
@JsonProperty("nsfw") val nsfw: String?,
- @JsonProperty("created_at") val createdAt: String?,
- @JsonProperty("updated_at") val updatedAt: String?
+ @JsonProperty("created_at") val created_at: String?,
+ @JsonProperty("updated_at") val updated_at: String?
)
data class ListStatus(
@JsonProperty("status") val status: String?,
@JsonProperty("score") val score: Int,
- @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
- @JsonProperty("is_rewatching") val isRewatching: Boolean,
- @JsonProperty("updated_at") val updatedAt: String,
+ @JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
+ @JsonProperty("is_rewatching") val is_rewatching: Boolean,
+ @JsonProperty("updated_at") val updated_at: String,
)
data class Data(
@JsonProperty("node") val node: Node,
- @JsonProperty("list_status") val listStatus: ListStatus?,
+ @JsonProperty("list_status") val list_status: ListStatus?,
) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
this.node.title,
"https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
- this.listStatus?.numEpisodesWatched,
- this.node.numEpisodes,
- this.listStatus?.score?.times(10),
- parseDateLong(this.listStatus?.updatedAt),
+ this.list_status?.num_episodes_watched,
+ this.node.num_episodes,
+ this.list_status?.score?.times(10),
+ parseDateLong(this.list_status?.updated_at),
"MAL",
TvType.Anime,
- this.node.mainPicture?.large ?: this.node.mainPicture?.medium,
+ this.node.main_picture?.large ?: this.node.main_picture?.medium,
null,
null,
- plot = this.node.synopsis,
- releaseDate = if (this.node.startDate == null) null else try {Date.from(
- Instant.from(
- DateTimeFormatter.ofPattern(if (this.node.startDate.length == 4) "yyyy" else if (this.node.startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
- .parse(this.node.startDate)
- )
- )} catch (_: RuntimeException) {null}
)
}
}
@@ -480,8 +467,8 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
)
data class Broadcast(
- @JsonProperty("day_of_the_week") val dayOfTheWeek: String?,
- @JsonProperty("start_time") val startTime: String?
+ @JsonProperty("day_of_the_week") val day_of_the_week: String?,
+ @JsonProperty("start_time") val start_time: String?
)
private fun getMalAnimeListCached(): Array? {
@@ -501,14 +488,14 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getMalAnimeListSmart()?.groupBy {
- convertToStatus(it.listStatus?.status ?: "").stringRes
+ convertToStatus(it.list_status?.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.toLibraryItem() }
} ?: emptyMap()
// To fill empty lists when MAL does not return them
val baseMap =
- MalStatusType.entries.filter { it.value >= 0 }.associate {
+ MalStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList()
}
@@ -519,8 +506,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
- ListSorting.ReleaseDateNew,
- ListSorting.ReleaseDateOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
@@ -585,7 +570,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text
val values = parseJson(res)
val titles =
- values.data.map { MalTitleHolder(it.listStatus, it.node.id, it.node.title) }
+ values.data.map { MalTitleHolder(it.list_status, it.node.id, it.node.title) }
for (t in titles) {
allTitles[t.id] = t
}
@@ -594,13 +579,11 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
- private fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
+ fun convertJapanTimeToTimeRemaining(date: String, endDate: String? = null): String? {
// No time remaining if the show has already ended
try {
endDate?.let {
- if (SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(it)
- ?.before(Date.from(Instant.now())) != false
- ) return@convertJapanTimeToTimeRemaining null
+ if (SimpleDateFormat("yyyy-MM-dd").parse(it).time < System.currentTimeMillis()) return@convertJapanTimeToTimeRemaining null
}
} catch (e: ParseException) {
logError(e)
@@ -617,7 +600,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
val currentWeek = currentDate.get(Calendar.WEEK_OF_MONTH)
val currentYear = currentDate.get(Calendar.YEAR)
- val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm", Locale.getDefault())
+ val dateFormat = SimpleDateFormat("yyyy MM W EEEE HH:mm")
dateFormat.timeZone = TimeZone.getTimeZone("Japan")
val parsedDate =
dateFormat.parse("$currentYear $currentMonth $currentWeek $date") ?: return null
@@ -661,13 +644,13 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
id: Int,
status: MalStatusType? = null,
score: Int? = null,
- numWatchedEpisodes: Int? = null,
+ num_watched_episodes: Int? = null,
): Boolean {
val res = setScoreRequest(
id,
if (status == null) null else malStatusAsString[maxOf(0, status.value)],
score,
- numWatchedEpisodes
+ num_watched_episodes
)
return if (res.isNullOrBlank()) {
@@ -684,18 +667,17 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
}
}
- @Suppress("UNCHECKED_CAST")
private suspend fun setScoreRequest(
id: Int,
status: String? = null,
score: Int? = null,
- numWatchedEpisodes: Int? = null,
+ num_watched_episodes: Int? = null,
): String? {
val data = mapOf(
"status" to status,
"score" to score?.toString(),
- "num_watched_episodes" to numWatchedEpisodes?.toString()
- ).filterValues { it != null } as Map
+ "num_watched_episodes" to num_watched_episodes?.toString()
+ ).filter { it.value != null } as Map
return app.put(
"$apiUrl/v2/anime/$id/my_list_status",
@@ -708,10 +690,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class ResponseToken(
- @JsonProperty("token_type") val tokenType: String,
- @JsonProperty("expires_in") val expiresIn: Int,
- @JsonProperty("access_token") val accessToken: String,
- @JsonProperty("refresh_token") val refreshToken: String,
+ @JsonProperty("token_type") val token_type: String,
+ @JsonProperty("expires_in") val expires_in: Int,
+ @JsonProperty("access_token") val access_token: String,
+ @JsonProperty("refresh_token") val refresh_token: String,
)
data class MalRoot(
@@ -720,7 +702,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class MalDatum(
@JsonProperty("node") val node: MalNode,
- @JsonProperty("list_status") val listStatus: MalStatus,
+ @JsonProperty("list_status") val list_status: MalStatus,
)
data class MalNode(
@@ -737,16 +719,16 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class MalStatus(
@JsonProperty("status") val status: String,
@JsonProperty("score") val score: Int,
- @JsonProperty("num_episodes_watched") val numEpisodesWatched: Int,
- @JsonProperty("is_rewatching") val isRewatching: Boolean,
- @JsonProperty("updated_at") val updatedAt: String,
+ @JsonProperty("num_episodes_watched") val num_episodes_watched: Int,
+ @JsonProperty("is_rewatching") val is_rewatching: Boolean,
+ @JsonProperty("updated_at") val updated_at: String,
)
data class MalUser(
@JsonProperty("id") val id: Int,
@JsonProperty("name") val name: String,
@JsonProperty("location") val location: String,
- @JsonProperty("joined_at") val joinedAt: String,
+ @JsonProperty("joined_at") val joined_at: String,
@JsonProperty("picture") val picture: String?,
)
@@ -759,9 +741,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class SmallMalAnime(
@JsonProperty("id") val id: Int,
@JsonProperty("title") val title: String?,
- @JsonProperty("num_episodes") val numEpisodes: Int,
- @JsonProperty("my_list_status") val myListStatus: MalStatus?,
- @JsonProperty("main_picture") val mainPicture: MalMainPicture?,
+ @JsonProperty("num_episodes") val num_episodes: Int,
+ @JsonProperty("my_list_status") val my_list_status: MalStatus?,
+ @JsonProperty("main_picture") val main_picture: MalMainPicture?,
)
data class MalSearchNode(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
index 37b95614..3e372c2d 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/OpenSubtitlesApi.kt
@@ -2,13 +2,12 @@ package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
+import com.google.common.collect.BiMap
+import com.google.common.collect.HashBiMap
+import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
-import com.lagradost.cloudstream3.ErrorLoadingException
-import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.TvType
-import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
@@ -16,8 +15,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils
-import okhttp3.Interceptor
-import okhttp3.Response
+import java.net.URLEncoder
+import java.nio.charset.StandardCharsets
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "opensubtitles"
@@ -29,31 +28,14 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
companion object {
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
- const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
- const val HOST = "https://api.opensubtitles.com/api/v1"
+ const val apiKey = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
+ const val host = "https://api.opensubtitles.com/api/v1"
const val TAG = "OPENSUBS"
- const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms
+ const val coolDownDuration: Long = 1000L * 30L // CoolDown if 429 error code in ms
var currentCoolDown: Long = 0L
var currentSession: SubtitleOAuthEntity? = null
}
- private val headerInterceptor = OpenSubtitleInterceptor()
-
- /** Automatically adds required api headers */
- private class OpenSubtitleInterceptor : Interceptor {
- /** Required user agent! */
- private val userAgent = "Cloudstream3 v0.1"
- override fun intercept(chain: Interceptor.Chain): Response {
- return chain.proceed(
- chain.request().newBuilder()
- .removeHeader("user-agent")
- .addHeader("user-agent", userAgent)
- .addHeader("Api-Key", API_KEY)
- .build()
- )
- }
- }
-
private fun canDoRequest(): Boolean {
return unixTimeMs > currentCoolDown
}
@@ -65,7 +47,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
}
private fun throwGotTooManyRequests() {
- currentCoolDown = unixTimeMs + COOLDOWN_DURATION
+ currentCoolDown = unixTimeMs + coolDownDuration
throw ErrorLoadingException("Too many requests")
}
@@ -114,15 +96,15 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
private suspend fun initLogin(username: String, password: String): Boolean {
//Log.i(TAG, "DATA = [$username] [$password]")
val response = app.post(
- url = "$HOST/login",
+ url = "$host/login",
headers = mapOf(
- "Content-Type" to "application/json",
+ "Api-Key" to apiKey,
+ "Content-Type" to "application/json"
),
data = mapOf(
"username" to username,
"password" to password
- ),
- interceptor = headerInterceptor
+ )
)
//Log.i(TAG, "Responsecode = ${response.code}")
//Log.i(TAG, "Result => ${response.text}")
@@ -133,7 +115,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
SubtitleOAuthEntity(
user = username,
pass = password,
- accessToken = token.token ?: run {
+ access_token = token.token ?: run {
return false
})
)
@@ -167,13 +149,11 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
// "pt" to "pt-PT",
// "pt" to "pt-BR"
)
-
- private fun fixLanguage(language: String?): String? {
+ private fun fixLanguage(language: String?) : String? {
return languageExceptions[language] ?: language
}
-
// O(n) but good enough, BiMap did not want to work properly
- private fun fixLanguageReverse(language: String?): String? {
+ private fun fixLanguageReverse(language: String?) : String? {
return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language
}
@@ -185,7 +165,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
throwIfCantDoRequest()
val fixedLang = fixLanguage(query.lang)
- val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0
+ val imdbId = query.imdb ?: 0
val queryText = query.query
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
@@ -196,16 +176,16 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val searchQueryUrl = when (imdbId > 0) {
//Use imdb_id to search if its valid
- true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
- false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
+ true -> "$host/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
+ false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
}
val req = app.get(
url = searchQueryUrl,
headers = mapOf(
+ Pair("Api-Key", apiKey),
Pair("Content-Type", "application/json")
- ),
- interceptor = headerInterceptor
+ )
)
Log.i(TAG, "Search Req => ${req.text}")
if (!req.isSuccessful) {
@@ -227,12 +207,12 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
//Use any valid name/title in hierarchy
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: query.query
- val lang = fixLanguageReverse(attr.language) ?: ""
+ val lang = fixLanguageReverse(attr.language)?: ""
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
val year = featureDetails?.year ?: query.year
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
- val isHearingImpaired = attr.hearingImpaired ?: false
+ val isHearingImpaired = attr.hearing_impaired ?: false
//Log.i(TAG, "Result id/name => ${item.id} / $name")
item.attributes?.files?.forEach { file ->
val resultData = file.fileId?.toString() ?: ""
@@ -265,19 +245,19 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
throwIfCantDoRequest()
val req = app.post(
- url = "$HOST/download",
+ url = "$host/download",
headers = mapOf(
Pair(
"Authorization",
- "Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}"
+ "Bearer ${currentSession?.access_token ?: throw ErrorLoadingException("No access token active in current session")}"
),
+ Pair("Api-Key", apiKey),
Pair("Content-Type", "application/json"),
Pair("Accept", "*/*")
),
data = mapOf(
Pair("file_id", data.data)
- ),
- interceptor = headerInterceptor
+ )
)
Log.i(TAG, "Request result => (${req.code}) ${req.text}")
//Log.i(TAG, "Request headers => ${req.headers}")
@@ -298,7 +278,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
data class SubtitleOAuthEntity(
var user: String,
var pass: String,
- var accessToken: String,
+ var access_token: String,
)
data class OAuthToken(
@@ -323,7 +303,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
@JsonProperty("url") var url: String? = null,
@JsonProperty("files") var files: List? = listOf(),
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
- @JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null,
+ @JsonProperty("hearing_impaired") var hearing_impaired: Boolean? = null,
)
data class ResultFiles(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
index 50517f9d..b4a9d789 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/SimklApi.kt
@@ -5,33 +5,26 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
-import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
-import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.BuildConfig
-import com.lagradost.cloudstream3.LoadResponse.Companion.readIdFromString
import com.lagradost.cloudstream3.R
-import com.lagradost.cloudstream3.SimklSyncServices
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI
-import com.lagradost.cloudstream3.syncproviders.OAuth2API
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
-import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.toJson
-import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import okhttp3.Interceptor
import okhttp3.Response
import java.math.BigInteger
@@ -39,17 +32,12 @@ import java.security.SecureRandom
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.Date
-import java.util.Locale
import java.util.TimeZone
-import kotlin.time.Duration
-import kotlin.time.DurationUnit
-import kotlin.time.toDuration
class SimklApi(index: Int) : AccountManager(index), SyncAPI {
override var name = "Simkl"
override val key = "simkl-key"
override val redirectUrl = "simkl"
- override val supportDeviceAuth = true
override val idPrefix = "simkl"
override var requireLibraryRefresh = true
override var mainUrl = "https://api.simkl.com"
@@ -71,83 +59,9 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
*/
private var lastScoreTime = -1L
- private object SimklCache {
- private const val SIMKL_CACHE_KEY = "SIMKL_API_CACHE"
-
- enum class CacheTimes(val value: String) {
- OneMonth("30d"),
- ThirtyMinutes("30m")
- }
-
- private class SimklCacheWrapper(
- @JsonProperty("obj") val obj: T?,
- @JsonProperty("validUntil") val validUntil: Long,
- @JsonProperty("cacheTime") val cacheTime: Long = unixTime,
- ) {
- /** Returns true if cache is newer than cacheDays */
- fun isFresh(): Boolean {
- return validUntil > unixTime
- }
-
- fun remainingTime(): Duration {
- val unixTime = unixTime
- return if (validUntil > unixTime) {
- (validUntil - unixTime).toDuration(DurationUnit.SECONDS)
- } else {
- Duration.ZERO
- }
- }
- }
-
- fun cleanOldCache() {
- getKeys(SIMKL_CACHE_KEY)?.forEach {
- val isOld = AcraApplication.getKey>(it)?.isFresh() == false
- if (isOld) {
- removeKey(it)
- }
- }
- }
-
- fun setKey(path: String, value: T, cacheTime: Duration) {
- debugPrint { "Set cache: $SIMKL_CACHE_KEY/$path for ${cacheTime.inWholeDays} days or ${cacheTime.inWholeSeconds} seconds." }
- setKey(
- SIMKL_CACHE_KEY,
- path,
- // Storing as plain sting is required to make generics work.
- SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson()
- )
- }
-
- /**
- * Gets cached object, if object is not fresh returns null and removes it from cache
- */
- inline fun getKey(path: String): T? {
- // Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject"
- val type = mapper.typeFactory.constructParametricType(
- SimklCacheWrapper::class.java,
- T::class.java
- )
- val cache = getKey(SIMKL_CACHE_KEY, path)?.let {
- mapper.readValue>(it, type)
- }
-
- return if (cache?.isFresh() == true) {
- debugPrint {
- "Cache hit at: $SIMKL_CACHE_KEY/$path. " +
- "Remains fresh for ${cache.remainingTime().inWholeDays} days or ${cache.remainingTime().inWholeSeconds} seconds."
- }
- cache.obj
- } else {
- debugPrint { "Cache miss at: $SIMKL_CACHE_KEY/$path" }
- removeKey(SIMKL_CACHE_KEY, path)
- null
- }
- }
- }
-
companion object {
- private const val CLIENT_ID: String = BuildConfig.SIMKL_CLIENT_ID
- private const val CLIENT_SECRET: String = BuildConfig.SIMKL_CLIENT_SECRET
+ private const val clientId: String = BuildConfig.SIMKL_CLIENT_ID
+ private const val clientSecret: String = BuildConfig.SIMKL_CLIENT_SECRET
private var lastLoginState = ""
const val SIMKL_TOKEN_KEY: String = "simkl_token"
@@ -156,10 +70,10 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
const val SIMKL_CACHED_LIST_TIME: String = "simkl_cached_time"
/** 2014-09-01T09:10:11Z -> 1409562611 */
- private const val SIMKL_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'"
+ private const val simklDateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
fun getUnixTime(string: String?): Long? {
return try {
- SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
+ SimpleDateFormat(simklDateFormat).apply {
this.timeZone = TimeZone.getTimeZone("UTC")
}.parse(
string ?: return null
@@ -173,7 +87,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
/** 1409562611 -> 2014-09-01T09:10:11Z */
fun getDateTime(unixTime: Long?): String? {
return try {
- SimpleDateFormat(SIMKL_DATE_FORMAT, Locale.getDefault()).apply {
+ SimpleDateFormat(simklDateFormat).apply {
this.timeZone = TimeZone.getTimeZone("UTC")
}.format(
Date.from(
@@ -187,6 +101,32 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
}
+ /**
+ * 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 SyncServices(val originalName: String) {
+ Simkl("simkl"),
+ Imdb("imdb"),
+ Tmdb("tmdb"),
+ AniList("anilist"),
+ Mal("mal"),
+ }
+
+ /**
+ * 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: SyncServices, 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 */
+ private fun readIdFromString(idString: String?): Map {
+ return tryParseJson(idString) ?: return emptyMap()
+ }
+
fun getPosterUrl(poster: String): String {
return "https://wsrv.nl/?url=https://simkl.in/posters/${poster}_m.webp"
}
@@ -210,7 +150,7 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
companion object {
fun fromString(string: String): SimklListStatusType? {
- return SimklListStatusType.entries.firstOrNull {
+ return SimklListStatusType.values().firstOrNull {
it.originalName == string
}
}
@@ -221,17 +161,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonInclude(JsonInclude.Include.NON_EMPTY)
data class TokenRequest(
@JsonProperty("code") val code: String,
- @JsonProperty("client_id") val clientId: String = CLIENT_ID,
- @JsonProperty("client_secret") val clientSecret: String = CLIENT_SECRET,
- @JsonProperty("redirect_uri") val redirectUri: String = "$APP_STRING://simkl",
- @JsonProperty("grant_type") val grantType: String = "authorization_code"
+ @JsonProperty("client_id") val client_id: String = clientId,
+ @JsonProperty("client_secret") val client_secret: String = clientSecret,
+ @JsonProperty("redirect_uri") val redirect_uri: String = "$appString://simkl",
+ @JsonProperty("grant_type") val grant_type: String = "authorization_code"
)
data class TokenResponse(
/** No expiration date */
- @JsonProperty("access_token") val accessToken: String,
- @JsonProperty("token_type") val tokenType: String,
- @JsonProperty("scope") val scope: String
+ val access_token: String,
+ val token_type: String,
+ val scope: String
)
// -------------------
@@ -246,32 +186,17 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
)
}
- data class PinAuthResponse(
- @JsonProperty("result") val result: String,
- @JsonProperty("device_code") val deviceCode: String,
- @JsonProperty("user_code") val userCode: String,
- @JsonProperty("verification_url") val verificationUrl: String,
- @JsonProperty("expires_in") val expiresIn: Int,
- @JsonProperty("interval") val interval: Int,
- )
-
- data class PinExchangeResponse(
- @JsonProperty("result") val result: String,
- @JsonProperty("message") val message: String? = null,
- @JsonProperty("access_token") val accessToken: String? = null,
- )
-
// -------------------
data class ActivitiesResponse(
- @JsonProperty("all") val all: String?,
- @JsonProperty("tv_shows") val tvShows: UpdatedAt,
- @JsonProperty("anime") val anime: UpdatedAt,
- @JsonProperty("movies") val movies: UpdatedAt,
+ val all: String?,
+ val tv_shows: UpdatedAt,
+ val anime: UpdatedAt,
+ val movies: UpdatedAt,
) {
data class UpdatedAt(
- @JsonProperty("all") val all: String?,
- @JsonProperty("removed_from_list") val removedFromList: String?,
- @JsonProperty("rated_at") val ratedAt: String?,
+ val all: String?,
+ val removed_from_list: String?,
+ val rated_at: String?,
)
}
@@ -285,18 +210,18 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("img") val img: String?
) {
companion object {
- fun convertToEpisodes(list: List?): List? {
+ fun convertToEpisodes(list: List?): List {
return list?.map {
MediaObject.Season.Episode(it.episode)
- }
+ } ?: emptyList()
}
- fun convertToSeasons(list: List?): List? {
+ fun convertToSeasons(list: List?): List {
return list?.filter { it.season != null }?.groupBy {
it.season
- }?.mapNotNull { (season, episodes) ->
- convertToEpisodes(episodes)?.let { MediaObject.Season(season!!, it) }
- }?.ifEmpty { null }
+ }?.map { (season, episodes) ->
+ MediaObject.Season(season!!, convertToEpisodes(episodes))
+ } ?: emptyList()
}
}
}
@@ -310,17 +235,11 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("title") val title: String?,
@JsonProperty("year") val year: Int?,
@JsonProperty("ids") val ids: Ids?,
- @JsonProperty("total_episodes") val totalEpisodes: Int? = null,
- @JsonProperty("status") val status: String? = null,
@JsonProperty("poster") val poster: String? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("seasons") val seasons: List? = null,
@JsonProperty("episodes") val episodes: List? = null
) {
- fun hasEnded(): Boolean {
- return status == "released" || status == "ended"
- }
-
@JsonInclude(JsonInclude.Include.NON_EMPTY)
data class Season(
@JsonProperty("number") val number: Int,
@@ -338,13 +257,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("anilist") val anilist: String? = null,
) {
companion object {
- fun fromMap(map: Map): Ids {
+ fun fromMap(map: Map): Ids {
return Ids(
- simkl = map[SimklSyncServices.Simkl]?.toIntOrNull(),
- imdb = map[SimklSyncServices.Imdb],
- tmdb = map[SimklSyncServices.Tmdb],
- mal = map[SimklSyncServices.Mal],
- anilist = map[SimklSyncServices.AniList]
+ simkl = map[SyncServices.Simkl]?.toIntOrNull(),
+ imdb = map[SyncServices.Imdb],
+ tmdb = map[SyncServices.Tmdb],
+ mal = map[SyncServices.Mal],
+ anilist = map[SyncServices.AniList]
)
}
}
@@ -362,214 +281,13 @@ class SimklApi(index: Int) : AccountManager(index), SyncAPI {
}
}
- class SimklScoreBuilder private constructor() {
- data class Builder(
- private var url: String? = null,
- private var interceptor: Interceptor? = null,
- private var ids: MediaObject.Ids? = null,
- private var score: Int? = null,
- private var status: Int? = null,
- private var addEpisodes: Pair?, List?>? = null,
- private var removeEpisodes: Pair?, List?>? = null,
- // Required for knowing if the status should be overwritten
- private var onList: Boolean = false
- ) {
- fun interceptor(interceptor: Interceptor) = apply { this.interceptor = interceptor }
- fun apiUrl(url: String) = apply { this.url = url }
- fun ids(ids: MediaObject.Ids) = apply { this.ids = ids }
- fun score(score: Int?, oldScore: Int?) = apply {
- if (score != oldScore) {
- this.score = score
- }
- }
-
- fun status(newStatus: Int?, oldStatus: Int?) = apply {
- onList = oldStatus != null
- // Only set status if its new
- if (newStatus != oldStatus) {
- this.status = newStatus
- } else {
- this.status = null
- }
- }
-
- fun episodes(
- allEpisodes: List