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/downloads.jpg b/.github/downloads.jpg
new file mode 100644
index 00000000..ca14a664
Binary files /dev/null and b/.github/downloads.jpg differ
diff --git a/.github/home.jpg b/.github/home.jpg
new file mode 100644
index 00000000..72370d3c
Binary files /dev/null and b/.github/home.jpg differ
diff --git a/.github/locales.py b/.github/locales.py
index a74d7258..1c79c093 100644
--- a/.github/locales.py
+++ b/.github/locales.py
@@ -1,8 +1,6 @@
import re
import glob
import requests
-import os
-import lxml.etree as ET # builtin library doesn't preserve comments
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
@@ -47,23 +45,4 @@ open(SETTINGS_PATH, "w+",encoding='utf-8').write(
"\n" +
END_MARKER +
after_src
-)
-
-# Go through each values.xml file and fix escaped \@string
-for file in glob.glob(f"{XML_NAME}*/strings.xml"):
- try:
- tree = ET.parse(file)
- for child in tree.getroot():
- if 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}")
+)
\ No newline at end of file
diff --git a/.github/player.jpg b/.github/player.jpg
new file mode 100644
index 00000000..f6959cf3
Binary files /dev/null and b/.github/player.jpg differ
diff --git a/.github/results.jpg b/.github/results.jpg
new file mode 100644
index 00000000..4dbc9b8d
Binary files /dev/null and b/.github/results.jpg differ
diff --git a/.github/search.jpg b/.github/search.jpg
new file mode 100644
index 00000000..784bec89
Binary files /dev/null and b/.github/search.jpg differ
diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml
index e84bb08b..83430766 100644
--- a/.github/workflows/build_to_archive.yml
+++ b/.github/workflows/build_to_archive.yml
@@ -19,23 +19,23 @@ 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
- - name: Set up JDK 17
- uses: actions/setup-java@v4
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
with:
- java-version: '17'
+ java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
@@ -56,9 +56,7 @@ jobs:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
- SIMKL_CLIENT_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..3c5caad7 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 }}
@@ -42,14 +42,13 @@ jobs:
cd $GITHUB_WORKSPACE/dokka/
rm -rf "./-cloudstream"
- - name: Setup JDK 17
- uses: actions/setup-java@v4
+ - name: Setup JDK 11
+ uses: actions/setup-java@v1
with:
- java-version: 17
- distribution: 'adopt'
+ java-version: 11
- 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..4ce7dba1 100644
--- a/.github/workflows/prerelease.yml
+++ b/.github/workflows/prerelease.yml
@@ -18,16 +18,16 @@ 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
- - name: Set up JDK 17
- uses: actions/setup-java@v4
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
with:
- java-version: '17'
+ java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
@@ -43,14 +43,11 @@ 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 }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
- SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
- name: Create pre-release
uses: "marvinpinto/action-automatic-releases@latest"
with:
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 7f6dd412..36199cd6 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -6,18 +6,18 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - name: Set up JDK 17
- uses: actions/setup-java@v4
+ - uses: actions/checkout@v2
+ - name: Set up JDK 11
+ uses: actions/setup-java@v2
with:
- java-version: '17'
+ java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- 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..93cdca44 100644
--- a/.github/workflows/update_locales.yml
+++ b/.github/workflows/update_locales.yml
@@ -1,4 +1,4 @@
-name: Fix locale issues
+name: Update locale lists
on:
workflow_dispatch:
@@ -9,7 +9,7 @@ on:
- master
concurrency:
- group: "locale"
+ group: "locale-list"
cancel-in-progress: true
jobs:
@@ -18,17 +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/cloudstream"
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v2
with:
token: ${{ steps.generate_token.outputs.token }}
- - name: Install dependencies
- run: |
- pip3 install lxml
- name: Edit files
run: |
python3 .github/locales.py
@@ -38,5 +35,5 @@ jobs:
git config --local user.name "recloudstream[bot]"
git add .
# "echo" returns true so the build succeeds, even if no changed files
- git commit -m 'chore(locales): fix locale issues' || echo
+ git commit -m 'update list of locales' || echo
git push
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b589d56e..5421743a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
-
+
-
\ No newline at end of file
+
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index d7c08c9c..10c26704 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,16 +4,17 @@
diff --git a/README.md b/README.md
index 8949304e..3430d626 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,15 @@
+ **AdFree**, No ads whatsoever
+ No tracking/analytics
+ Bookmarks
-+ Phone and TV support
++ Download and stream movies, tv-shows and anime
+ Chromecast
-+ Extension system for personal customization
+
+### Screenshots:
+
+


+
### Supported languages:
-
+
\ No newline at end of file
diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
deleted file mode 100644
index 7f7fd14c..00000000
--- a/app/CMakeLists.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-# Set this to the minimum version your project supports.
-cmake_minimum_required(VERSION 3.18)
-project(CrashHandler)
-find_library(log-lib log)
-add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
-target_link_libraries(native-lib ${log-lib})
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d0c86bab..3c855d28 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,14 +1,13 @@
-import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
+import com.android.build.gradle.api.BaseVariantOutput
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("kotlin-android-extensions")
id("org.jetbrains.dokka")
}
@@ -20,7 +19,7 @@ fun String.execute() = ByteArrayOutputStream().use { baot ->
workingDir = projectDir
commandLine = this@execute.split(Regex("\\s"))
standardOutput = baot
- }.exitValue == 0)
+ }.exitValue == 0)
String(baot.toByteArray()).trim()
else null
}
@@ -29,21 +28,9 @@ android {
testOptions {
unitTests.isReturnDefaultValues = true
}
-
- viewBinding {
- enable = true
- }
-
- /* 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,44 +39,33 @@ 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 = 33
+
+ versionCode = 57
+ versionName = "4.0.0"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
+
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
+
resValue("bool", "is_prerelease", "false")
- // Reads local.properties
- val localProperties = gradleLocalProperties(rootDir)
+ buildConfigField(
+ "String",
+ "BUILDDATE",
+ "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
+ )
- buildConfigField(
- "long",
- "BUILD_DATE",
- "${System.currentTimeMillis()}"
- )
- buildConfigField(
- "String",
- "SIMKL_CLIENT_ID",
- "\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\""
- )
- buildConfigField(
- "String",
- "SIMKL_CLIENT_SECRET",
- "\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
- )
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- ksp {
- arg("room.schemaLocation", "$projectDir/schemas")
- arg("exportSchema", "true")
+ kapt {
+ includeCompileClasspath = true
}
}
@@ -98,21 +74,14 @@ android {
isDebuggable = false
isMinifyEnabled = false
isShrinkResources = false
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
isDebuggable = true
applicationIdSuffix = ".debug"
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"),
- "proguard-rules.pro"
- )
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
-
flavorDimensions.add("state")
productFlavors {
create("stable") {
@@ -124,31 +93,25 @@ 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()
}
}
-
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 +120,122 @@ 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.3")
+ 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.8.0")
+ implementation("androidx.appcompat:appcompat:1.4.2") // 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.5.1")
+ implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
+ implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+ testImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test.ext:junit:1.1.3")
+ androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+
+ //implementation("io.karn:khttp-android:0.1.2") //okhttp instead
+// implementation("org.jsoup:jsoup:1.13.1")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
+
+ implementation("androidx.preference:preference-ktx:1.2.0")
+
+ implementation("com.github.bumptech.glide:glide:4.13.1")
+ kapt("com.github.bumptech.glide:compiler:4.13.1")
+ implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
+
+ implementation("jp.wasabeef:glide-transformations:4.3.0")
+
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
- // 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")
+ // Exoplayer
+ implementation("com.google.android.exoplayer:exoplayer:2.18.2")
+ implementation("com.google.android.exoplayer:extension-cast:2.18.2")
+ implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
+ implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
- // Media 3 (ExoPlayer)
- implementation("androidx.media3:media3-ui:1.1.1")
- implementation("androidx.media3:media3-cast:1.1.1")
- implementation("androidx.media3:media3-common:1.1.1")
- implementation("androidx.media3:media3-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-exoplayer-hls:1.1.1")
- implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
- implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
+ //implementation("com.google.android.exoplayer:extension-leanback:2.14.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
+ // Bug reports
+ implementation("ch.acra:acra-core:5.8.4")
+ implementation("ch.acra:acra-toast:5.8.4")
- // Crash Reports (AcraApplication.kt)
- implementation("ch.acra:acra-core:5.11.3")
- implementation("ch.acra:acra-toast:5.11.3")
+ compileOnly("com.google.auto.service:auto-service-annotations:1.0")
+ //either for java sources:
+ annotationProcessor("com.google.auto.service:auto-service:1.0")
+ //or for kotlin sources (requires kapt gradle plugin):
+ kapt("com.google.auto.service:auto-service:1.0")
+
+ // subtitle color picker
+ implementation("com.jaredrummler:colorpicker:1.1.0")
+
+ //run JS
+ // do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
+ // available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
+ implementation("org.mozilla:rhino:1.7.13")
+
+ // TorrentStream
+ //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
+
+ // Downloading
+ implementation("androidx.work:work-runtime:2.7.1")
+ implementation("androidx.work:work-runtime-ktx:2.7.1")
+
+ // Networking
+// implementation("com.squareup.okhttp3:okhttp:4.9.2")
+// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
+ implementation("com.github.Blatzar:NiceHttp:0.4.2")
+ // To fix SSL fuckery on android 9
+ implementation("org.conscrypt:conscrypt-android:2.2.1")
+ // Util to skip the URI file fuckery 🙏
+ implementation("com.github.tachiyomiorg:unifile:17bec43")
+
+ // API because cba maintaining it myself
+ implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
+
+ implementation("com.github.discord:OverlappingPanels:0.1.3")
+ // debugImplementation because LeakCanary should only run in debug builds.
+ // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
+
+ // for shimmer when loading
+ implementation("com.facebook.shimmer:shimmer:0.5.0")
- // 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
+ // slow af yt
+ //implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
- 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)
- }
- }
+ // newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
+ implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
- this.extra.set("isDebug", isDebug)
- })
+ // Library/extensions searching with Levenshtein distance
+ implementation("me.xdrop:fuzzywuzzy:1.4.0")
+
+ // color pallette for images -> colors
+ implementation("androidx.palette:palette-ktx:1.0.0")
}
-tasks.register("androidSourcesJar") {
+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 +248,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..81753f6b 100644
--- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
+++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt
@@ -1,57 +1,155 @@
package com.lagradost.cloudstream3
-import android.app.Activity
-import android.os.Bundle
-import android.os.PersistableBundle
-import android.view.LayoutInflater
-import androidx.test.core.app.ActivityScenario
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
-import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
-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
-import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
-import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
-import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
-import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
-import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
+import com.lagradost.cloudstream3.mvvm.logError
+import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper
-import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
-
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
-class TestApplication : Activity() {
- override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
- super.onCreate(savedInstanceState, persistentState)
- }
-}
-
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
- private fun getAllProviders(): Array {
- println("Providers: ${APIHolder.allProviders.size}")
- return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
+ //@Test
+ //fun useAppContext() {
+ // // Context of the app under test.
+ // val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ // assertEquals("com.lagradost.cloudstream3", appContext.packageName)
+ //}
+
+ private fun getAllProviders(): List {
+ return APIHolder.allProviders //.filter { !it.usesWebView }
+ }
+
+ private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
+ Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
+ if (url == null) return true
+ var linksLoaded = 0
+ try {
+ val success = api.loadLinks(url, false, {}) { link ->
+ Assert.assertTrue(
+ "Api ${api.name} returns link with invalid Quality",
+ Qualities.values().map { it.value }.contains(link.quality)
+ )
+ Assert.assertTrue(
+ "Api ${api.name} returns link with invalid url ${link.url}",
+ link.url.length > 4
+ )
+ linksLoaded++
+ }
+ if (success) {
+ return linksLoaded > 0
+ }
+ Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider has not implemented .loadLinks")
+ }
+ logError(e)
+ }
+ return true
+ }
+
+ private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
+ val searchQueries = listOf("over", "iron", "guy")
+ var correctResponses = 0
+ var searchResult: List? = null
+ for (query in searchQueries) {
+ val response = try {
+ api.search(query)
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider has not implemented .search")
+ }
+ logError(e)
+ null
+ }
+ if (!response.isNullOrEmpty()) {
+ correctResponses++
+ if (searchResult == null) {
+ searchResult = response
+ }
+ }
+ }
+
+ if (correctResponses == 0 || searchResult == null) {
+ System.err.println("Api ${api.name} did not return any valid search responses")
+ return false
+ }
+
+ try {
+ var validResults = false
+ for (result in searchResult) {
+ Assert.assertEquals(
+ "Invalid apiName on response on ${api.name}",
+ result.apiName,
+ api.name
+ )
+ val load = api.load(result.url) ?: continue
+ Assert.assertEquals(
+ "Invalid apiName on load on ${api.name}",
+ load.apiName,
+ result.apiName
+ )
+ Assert.assertTrue(
+ "Api ${api.name} on load does not contain any of the supportedTypes",
+ api.supportedTypes.contains(load.type)
+ )
+ when (load) {
+ is AnimeLoadResponse -> {
+ val gotNoEpisodes =
+ load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
+
+ if (gotNoEpisodes) {
+ println("Api ${api.name} got no episodes on ${load.url}")
+ continue
+ }
+
+ val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
+ validResults = loadLinks(api, url)
+ if (!validResults) continue
+ }
+ is MovieLoadResponse -> {
+ val gotNoEpisodes = load.dataUrl.isBlank()
+ if (gotNoEpisodes) {
+ println("Api ${api.name} got no movie on ${load.url}")
+ continue
+ }
+
+ validResults = loadLinks(api, load.dataUrl)
+ if (!validResults) continue
+ }
+ is TvSeriesLoadResponse -> {
+ val gotNoEpisodes = load.episodes.isEmpty()
+ if (gotNoEpisodes) {
+ println("Api ${api.name} got no episodes on ${load.url}")
+ continue
+ }
+
+ validResults = loadLinks(api, load.episodes.first().data)
+ if (!validResults) continue
+ }
+ }
+ break
+ }
+ if (!validResults) {
+ System.err.println("Api ${api.name} did not load on any")
+ }
+
+ return validResults
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider has not implemented .load")
+ }
+ logError(e)
+ return false
+ }
}
@Test
@@ -60,78 +158,7 @@ class ExampleInstrumentedTest {
println("Done providersExist")
}
- @Throws
- private inline fun testAllLayouts(
- activity: Activity,
- vararg layouts: Int
- ) {
-
- val bind = T::class.java.methods.first { it.name == "bind" }
- val inflater = LayoutInflater.from(activity)
- for (layout in layouts) {
- val root = inflater.inflate(layout, null, false)
- bind.invoke(null, root)
- }
- }
-
@Test
- @Throws
- fun layoutTest() {
- ActivityScenario.launch(MainActivity::class.java).use { scenario ->
- scenario.onActivity { activity: MainActivity ->
- // FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same
- //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
- //testAllLayouts(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
-
- // main cant be tested
- // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv)
- // testAllLayouts(activity,R.layout.activity_main, R.layout.activity_main_tv)
- //testAllLayouts(activity, R.layout.activity_main_tv)
-
- testAllLayouts(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
- testAllLayouts(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
-
- // testAllLayouts(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
- // testAllLayouts(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
-
- testAllLayouts(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
- testAllLayouts(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
- testAllLayouts(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
-
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
-
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
- testAllLayouts(activity, R.layout.repository_item_tv, R.layout.repository_item)
-
- testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
- testAllLayouts(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
-
- testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
- testAllLayouts(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
-
- testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
- //testAllLayouts(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
-
- testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
- testAllLayouts(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
-
-
- // 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.fragment_library_tv, R.layout.fragment_library)
- testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
- }
- }
- }
-
- @Test
- @Throws(AssertionError::class)
fun providerCorrectData() {
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
@@ -153,20 +180,68 @@ class ExampleInstrumentedTest {
@Test
fun providerCorrectHomepage() {
runBlocking {
- getAllProviders().toList().amap { api ->
- TestingUtils.testHomepage(api, TestingUtils.Logger())
+ getAllProviders().amap { api ->
+ if (api.hasMainPage) {
+ try {
+ val f = api.mainPage.first()
+ val homepage =
+ api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
+ when {
+ homepage == null -> {
+ System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
+ }
+ homepage.items.isEmpty() -> {
+ System.err.println("Homepage provider ${api.name} does not contain any items!")
+ }
+ homepage.items.any { it.list.isEmpty() } -> {
+ System.err.println("Homepage provider ${api.name} does not have any items on result!")
+ }
+ }
+ } catch (e: Exception) {
+ if (e.cause is NotImplementedError) {
+ Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
+ }
+ logError(e)
+ }
+ }
}
}
println("Done providerCorrectHomepage")
}
+// @Test
+// fun testSingleProvider() {
+// testSingleProviderApi(ThenosProvider())
+// }
+
@Test
- fun testAllProvidersCorrect() {
+ fun providerCorrect() {
runBlocking {
- TestingUtils.getDeferredProviderTests(
- this,
- getAllProviders(),
- ) { _, _ -> }
+ val invalidProvider = ArrayList>()
+ val providers = getAllProviders()
+ providers.amap { api ->
+ try {
+ println("Trying $api")
+ if (testSingleProviderApi(api)) {
+ println("Success $api")
+ } else {
+ System.err.println("Error $api")
+ invalidProvider.add(Pair(api, null))
+ }
+ } catch (e: Exception) {
+ logError(e)
+ invalidProvider.add(Pair(api, e))
+ }
+ }
+ if (invalidProvider.isEmpty()) {
+ println("No Invalid providers! :D")
+ } else {
+ println("Invalid providers are: ")
+ for (provider in invalidProvider) {
+ println("${provider.first}")
+ }
+ }
}
+ println("Done providerCorrect")
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 888be999..871c4f69 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,20 +87,16 @@
-->
+
+
-
-
-
-
-
-
-
-
+
+
@@ -165,21 +151,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
@@ -187,14 +158,13 @@
-
+ android:exported="true">
+
@@ -204,7 +174,6 @@
android:exported="false" />
diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp
deleted file mode 100644
index f4cb531f..00000000
--- a/app/src/main/cpp/native-lib.cpp
+++ /dev/null
@@ -1,28 +0,0 @@
-#include
-#include
-#include
-
-#define TAG "CloudStream Crash Handler"
-volatile sig_atomic_t gSignalStatus = 0;
-void handleNativeCrash(int signal) {
- gSignalStatus = signal;
-}
-
-extern "C" JNIEXPORT void JNICALL
-Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) {
- #define REGISTER_SIGNAL(X) signal(X, handleNativeCrash);
- REGISTER_SIGNAL(SIGSEGV)
- #undef REGISTER_SIGNAL
-}
-
-//extern "C" JNIEXPORT void JNICALL
-//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) {
-// int *p = nullptr;
-// *p = 0;
-//}
-
-extern "C" JNIEXPORT int JNICALL
-Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) {
- //__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus);
- return gSignalStatus;
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt
index d6f978fe..0351b1ff 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,26 +32,27 @@ 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) {
println("Sending report")
val url =
- "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
+ "https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse"
val data = mapOf(
- "entry.1993829403" to errorContent.toJSON()
+ "entry.753293084" to errorContent.toJSON()
)
thread { // to not run it on main thread
runBlocking {
suspendSafeApiCall {
- app.post(url, data = data)
- //println("Report response: $post")
+ val post = app.post(url, data = data)
+ println("Report response: $post")
}
}
}
@@ -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) {
@@ -98,16 +104,12 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
}
class AcraApplication : Application() {
-
override fun onCreate() {
super.onCreate()
- ExceptionHandler(filesDir.resolve("last_error")) {
+ Thread.setDefaultUncaughtExceptionHandler(ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
- }.also {
- exceptionHandler = it
- Thread.setDefaultUncaughtExceptionHandler(it)
- }
+ })
}
override fun attachBaseContext(base: Context?) {
@@ -119,10 +121,10 @@ class AcraApplication : Application() {
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
- reportContent = listOf(
+ reportContent = arrayOf(
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
- ReportField.STACK_TRACE,
+ ReportField.STACK_TRACE
)
// removed this due to bug when starting the app, moved it to when it actually crashes
@@ -135,8 +137,6 @@ class AcraApplication : Application() {
}
companion object {
- var exceptionHandler: ExceptionHandler? = null
-
/** Use to get activity from Context */
tailrec fun Context.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
@@ -146,17 +146,8 @@ class AcraApplication : Application() {
get() = _context?.get()
private set(value) {
_context = WeakReference(value)
- setContext(WeakReference(value))
}
- fun getKeyClass(path: String, valueType: Class): T? {
- return context?.getKey(path, valueType)
- }
-
- fun setKeyClass(path: String, value: T) {
- context?.setKey(path, value)
- }
-
fun removeKeys(folder: String): Int? {
return context?.removeKeys(folder)
}
@@ -208,9 +199,10 @@ class AcraApplication : Application() {
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
- isLayout(TV or EMULATOR),
+ isTvSettings(),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}
+
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
index ee3a5d12..89f0ae51 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt
@@ -5,16 +5,11 @@ 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.View.NO_ID
-import android.view.ViewGroup
+import android.view.*
+import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@@ -23,21 +18,15 @@ import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
-import androidx.core.view.children
import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession
-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.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
@@ -45,50 +34,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
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
-
-enum class FocusDirection {
- Start,
- End,
- Up,
- Down,
-}
+import java.util.*
object CommonActivity {
-
- private var _activity: WeakReference? = null
- var activity
- get() = _activity?.get()
- private set(value) {
- _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,32 +53,9 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair) -> Boolean)? = null
- private var currentToast: Toast? = null
- fun showToast(@StringRes message: Int, duration: Int? = null) {
- val act = activity ?: return
- act.runOnUiThread {
- showToast(act, act.getString(message), duration)
- }
- }
+ var currentToast: Toast? = null
- fun showToast(message: String?, duration: Int? = null) {
- val act = activity ?: return
- act.runOnUiThread {
- showToast(act, message, duration)
- }
- }
-
- fun showToast(message: UiText?, duration: Int? = null) {
- val act = activity ?: return
- if (message == null) return
- act.runOnUiThread {
- showToast(act, message.asString(act), duration)
- }
- }
-
-
- @MainThread
fun showToast(act: Activity?, text: UiText, duration: Int) {
if (act == null) return
text.asStringNull(act)?.let {
@@ -156,19 +86,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 +138,22 @@ 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
//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 +170,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,57 +210,30 @@ 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
"AmoledLight" -> R.style.AmoledModeLight
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.MonetMode else R.style.AppTheme
-
else -> R.style.AppTheme
}
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,13 +242,10 @@ 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
-
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
-
else -> R.style.OverlayPrimaryColorNormal
}
act.theme.applyStyle(currentTheme, true)
@@ -354,179 +257,101 @@ object CommonActivity {
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
}
- /** because we want closes find, aka when multiple have the same id, we go to parent
- until the correct one is found */
- private fun localLook(from: View, id: Int): View? {
- if (id == NO_ID) return null
- var currentLook: View = from
- // limit to 15 look depth
- for (i in 0..15) {
- currentLook.findViewById(id)?.let { return it }
- currentLook = (currentLook.parent as? View) ?: break
- }
- return null
- }
- /*var currentLook: View = view
- while (true) {
- val tmpNext = currentLook.findViewById(nextId)
- if (tmpNext != null) {
- next = tmpNext
- break
- }
- 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?,
- view: View,
- direction: FocusDirection,
- nextId: Int,
- depth: Int = 0
- ): View? {
- if (nextId == NO_ID) return null
-
- // do an initial search for the view, in case the localLook is too deep we can use this as
- // an early break and backup view
- var next =
- when (root) {
- is Activity -> root.findViewById(nextId)
- is View -> root.rootView.findViewById(nextId)
- else -> null
- } ?: 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 not shown then continue because we will "skip" over views to get to a replacement
- if (!shown) {
- // 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)
- }
-
- (when (next) {
- is ChipGroup -> {
- next.children.firstOrNull { it.isFocusable && it.isShown }
- }
-
- is NavigationRailView -> {
- next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home)
- }
-
- else -> null
- })?.let {
- return it
- }
-
- // nothing wrong with the view found, return it
- return next
- }
-
- /** recursively looks for a next focus up to a depth of 10,
- * this is used to override the normal shit focus system
- * because this application has a lot of invisible views that messes with some tv devices*/
- fun getNextFocus(
- root: Any?,
+ private fun getNextFocus(
+ act: Activity?,
view: View?,
direction: FocusDirection,
depth: Int = 0
- ): View? {
- // if input is invalid let android decide + depth test to not crash if loop is found
- if (view == null || depth >= 10 || root == null) {
+ ): Int? {
+ if (view == null || depth >= 10 || act == null) {
return null
}
- var nextId = when (direction) {
- FocusDirection.Start -> {
- if (view.isRtl())
- view.nextFocusRightId
- else
- view.nextFocusLeftId
+ val nextId = when (direction) {
+ FocusDirection.Left -> {
+ view.nextFocusLeftId
}
-
FocusDirection.Up -> {
view.nextFocusUpId
}
-
- FocusDirection.End -> {
- if (view.isRtl())
- view.nextFocusLeftId
- else
- view.nextFocusRightId
+ FocusDirection.Right -> {
+ view.nextFocusRightId
}
-
FocusDirection.Down -> {
view.nextFocusDownId
}
}
- if (nextId == NO_ID) {
- // if not specified then use forward id
- nextId = view.nextFocusForwardId
- // if view is still not found to next focus then return and let android decide
- if (nextId == NO_ID)
- return null
+ return if (nextId != -1) {
+ val next = act.findViewById(nextId)
+ //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
+
+ if (next?.isShown == false) {
+ getNextFocus(act, next, direction, depth + 1)
+ } else {
+ if (depth == 0) {
+ null
+ } else {
+ nextId
+ }
+ }
+ } else {
+ null
}
- return continueGetNextFocus(root, view, direction, nextId, depth)
}
+ enum class FocusDirection {
+ Left,
+ Right,
+ Up,
+ Down,
+ }
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) {
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
PlayerEventType.SeekForward
}
-
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
PlayerEventType.SeekBack
}
-
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
PlayerEventType.NextEpisode
}
-
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
PlayerEventType.PrevEpisode
}
-
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
PlayerEventType.Pause
}
-
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
PlayerEventType.Play
}
-
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
PlayerEventType.Lock
}
-
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
PlayerEventType.ToggleHide
}
-
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
PlayerEventType.ToggleMute
}
-
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
PlayerEventType.ShowMirrors
}
@@ -534,27 +359,21 @@ object CommonActivity {
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
-
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
PlayerEventType.ShowSpeed
}
-
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
PlayerEventType.Resize
}
-
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp
}
-
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
PlayerEventType.SkipCurrentChapter
}
-
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}
-
else -> null
}?.let { playerEvent ->
playerEventListener?.invoke(playerEvent)
@@ -567,64 +386,64 @@ object CommonActivity {
//}
}
- /** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null
- val currentFocus = act.currentFocus
-
event?.keyCode?.let { keyCode ->
- if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
- val nextView = when (keyCode) {
- KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.Start
- )
+ when (event.action) {
+ KeyEvent.ACTION_DOWN -> {
+ if (act.currentFocus != null) {
+ val next = when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Left
+ )
+ KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Right
+ )
+ KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Up
+ )
+ KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
+ act,
+ act.currentFocus,
+ FocusDirection.Down
+ )
- KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.End
- )
+ else -> null
+ }
- KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.Up
- )
+ if (next != null && next != -1) {
+ val nextView = act.findViewById(next)
+ if (nextView != null) {
+ nextView.requestFocus()
+ keyEventListener?.invoke(Pair(event, true))
+ return true
+ }
+ }
- KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
- act,
- currentFocus,
- FocusDirection.Down
- )
-
- else -> null
+ when (keyCode) {
+ KeyEvent.KEYCODE_DPAD_CENTER -> {
+ if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
+ UIHelper.showInputMethod(act.currentFocus?.findFocus())
+ }
+ }
+ }
+ }
+ //println("Keycode: $keyCode")
+ //showToast(
+ // this,
+ // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
+ // Toast.LENGTH_LONG
+ //)
+ }
}
- // println("NEXT FOCUS : $nextView")
- if (nextView != null) {
- nextView.requestFocus()
- keyEventListener?.invoke(Pair(event, true))
- return true
- }
-
- if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
- (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
- ) {
- UIHelper.showInputMethod(act.currentFocus?.findFocus())
- }
-
- //println("Keycode: $keyCode")
- //showToast(
- // this,
- // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
- // Toast.LENGTH_LONG
- //)
-
}
- // if someone else want to override the focus then don't handle the event as it is already
- // consumed. used in video player
if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true
}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt
index 8da7ca38..379a91e4 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)
@@ -51,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
companion object {
private 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"
+ "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
private var instance: DownloaderTestImpl? = null
/**
@@ -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 69%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
rename to app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
index 50dd667b..4014e34d 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/MainAPI.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
@@ -1,43 +1,41 @@
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.SyncIdName
+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 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/91.0.4472.124 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
@@ -50,10 +48,8 @@ object APIHolder {
val allProviders = threadSafeListOf()
fun initAll() {
- synchronized(allProviders) {
- for (api in allProviders) {
- api.init()
- }
+ for (api in allProviders) {
+ api.init()
}
apiMap = null
}
@@ -66,35 +62,27 @@ object APIHolder {
var apiMap: Map? = null
fun addPluginMapping(plugin: MainAPI) {
- synchronized(apis) {
- apis = apis + plugin
- }
+ apis = apis + plugin
initMap(true)
}
fun removePluginMapping(plugin: MainAPI) {
- synchronized(apis) {
- apis = apis.filter { it != plugin }
- }
+ apis = apis.filter { it != plugin }
initMap(true)
}
private fun initMap(forcedUpdate: Boolean = false) {
- synchronized(apis) {
- if (apiMap == null || forcedUpdate)
- apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
- }
+ if (apiMap == null || forcedUpdate)
+ apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
}
fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null
synchronized(allProviders) {
initMap()
- synchronized(apis) {
- return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
- // Leave the ?. null check, it can crash regardless
- ?: allProviders.firstOrNull { it.name == apiName }
- }
+ return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
+ // Leave the ?. null check, it can crash regardless
+ ?: allProviders.firstOrNull { it?.name == apiName }
}
}
@@ -108,6 +96,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 +120,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 +162,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 +174,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 +182,186 @@ 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(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 = apis.filter { hasUniversal || langs.contains(it.lang) }
+ .filter { api -> api.hasMainPage || !hasHomePageIsRequired }
+ return if (currentPrefMedia.isEmpty()) {
+ allApis
+ } else {
+ // Filter API depending on preferred media type
+ allApis.filter { api -> api.supportedTypes.any { currentPrefMedia.contains(it.ordinal) } }
}
- """.trimIndent().trim()
+ }
- 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 +551,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 +585,7 @@ abstract class MainAPI {
//emptyList() //
open val mainPage = listOf(MainPageData("", "", false))
- // @WorkerThread
+ @WorkerThread
open suspend fun getMainPage(
page: Int,
request: MainPageRequest,
@@ -490,17 +593,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 +621,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 +652,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 +710,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
}
@@ -615,20 +735,6 @@ fun fixTitle(str: String): String {
}
}
-/**
- * Get rhino context in a safe way as it needs to be initialized on the main thread.
- * Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
- * Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
- **/
-suspend fun getRhinoContext(): org.mozilla.javascript.Context {
- return Coroutines.mainWork {
- val rhino = org.mozilla.javascript.Context.enter()
- rhino.initSafeStandardObjects()
- rhino.optimizationLevel = -1
- rhino
- }
-}
-
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
fun imdbUrlToId(url: String): String? {
return Regex("/title/(tt[0-9]*)").find(url)?.groupValues?.get(1)
@@ -677,25 +783,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),
-}
-
-public enum class AutoDownloadMode(val value: Int) {
- Disable(0),
- FilterByLang(1),
- All(2),
- NsfwOnly(3)
- ;
-
- companion object {
- infix fun getEnum(value: Int): AutoDownloadMode? =
- AutoDownloadMode.values().firstOrNull { it.value == value }
- }
+ Others(12)
}
// IN CASE OF FUTURE ANIME MOVIE OR SMTH
@@ -1012,30 +1100,14 @@ 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
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
+ return this.type.isMovieType()
}
@JvmName("addActorNames")
@@ -1053,20 +1125,6 @@ interface LoadResponse {
this.actors = actors?.map { (actor, role) -> ActorData(actor, role = role) }
}
- /**
- * Internal helper function to add simkl ids from other databases.
- */
- private fun LoadResponse.addSimklId(
- database: SimklSyncServices,
- id: String?
- ) {
- normalSafeApiCall {
- this.syncData[simklIdPrefix] =
- addIdToString(this.syncData[simklIdPrefix], database, id.toString())
- ?: return@normalSafeApiCall
- }
- }
-
@JvmName("addActorsOnly")
fun LoadResponse.addActors(actors: List?) {
this.actors = actors?.map { actor -> ActorData(actor) }
@@ -1080,30 +1138,12 @@ 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())
}
fun LoadResponse.addAniListId(id: Int?) {
this.syncData[aniListIdPrefix] = (id ?: return).toString()
- this.addSimklId(SimklSyncServices.AniList, id.toString())
- }
-
- fun LoadResponse.addSimklId(id: Int?) {
- this.addSimklId(SimklSyncServices.Simkl, id.toString())
}
fun LoadResponse.addImdbUrl(url: String?) {
@@ -1185,7 +1225,6 @@ interface LoadResponse {
fun LoadResponse.addImdbId(id: String?) {
// TODO add imdb sync
- this.addSimklId(SimklSyncServices.Imdb, id)
}
fun LoadResponse.addTrackId(id: String?) {
@@ -1198,7 +1237,6 @@ interface LoadResponse {
fun LoadResponse.addTMDbId(id: String?) {
// TODO add TMDb sync
- this.addSimklId(SimklSyncServices.Tmdb, id)
}
fun LoadResponse.addRating(text: String?) {
@@ -1274,27 +1312,14 @@ fun LoadResponse?.isAnimeBased(): Boolean {
fun TvType?.isEpisodeBased(): Boolean {
if (this == null) return false
- return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
+ return (this == TvType.TvSeries || this == TvType.Anime)
}
+
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
@@ -1311,16 +1336,6 @@ interface EpisodeResponse {
var showStatus: ShowStatus?
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 +1373,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,90 +1404,7 @@ 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) ->
- val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
- .takeUnless { it == Int.MIN_VALUE }
- status to episodes
- .filter { it.season == maxSeason }
- .maxOfOrNull { it.episode ?: Int.MIN_VALUE }
- .takeUnless { it == Int.MIN_VALUE }
- }.toMap()
- }
-
- 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
- )
-}
+) : LoadResponse, EpisodeResponse
/**
* If episodes already exist appends the list.
@@ -1571,36 +1455,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 +1478,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 +1526,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 +1536,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 +1578,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,81 +1602,7 @@ 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 =
- episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
- val max = episodes
- .filter { it.season == maxSeason }
- .maxOfOrNull { it.episode ?: Int.MIN_VALUE }
- .takeUnless { it == Int.MIN_VALUE }
- return mapOf(DubStatus.None to max)
- }
-
- 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
- )
-}
+) : LoadResponse, EpisodeResponse
suspend fun MainAPI.newTvSeriesLoadResponse(
name: String,
@@ -1940,43 +1642,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..eddec15e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt
@@ -1,36 +1,21 @@
package com.lagradost.cloudstream3
-import android.animation.ValueAnimator
import android.content.ComponentName
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
import android.util.AttributeSet
import android.util.Log
-import android.view.KeyEvent
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
-import android.view.WindowManager
+import android.view.*
import android.widget.Toast
-import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
-import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
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
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
@@ -41,119 +26,71 @@ import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.LinearSnapHelper
-import androidx.recyclerview.widget.RecyclerView
-import com.google.android.gms.cast.framework.CastContext
-import com.google.android.gms.cast.framework.Session
-import com.google.android.gms.cast.framework.SessionManager
-import com.google.android.gms.cast.framework.SessionManagerListener
-import com.google.android.material.bottomnavigation.BottomNavigationView
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.google.android.gms.cast.framework.*
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView
-import com.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.logError
-import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
-import com.lagradost.cloudstream3.mvvm.observe
-import com.lagradost.cloudstream3.mvvm.observeNullable
+import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
-import com.lagradost.cloudstream3.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.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.BackupUtils.backup
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.html
+import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
+import com.lagradost.cloudstream3.utils.AppUtils.loadCache
+import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
+import com.lagradost.cloudstream3.utils.AppUtils.loadResult
+import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
+import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
-import com.lagradost.cloudstream3.utils.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.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
@@ -162,22 +99,21 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
-import com.lagradost.cloudstream3.utils.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.safefile.SafeFile
+import com.lagradost.nicehttp.Requests
+import com.lagradost.nicehttp.ResponseParser
+import kotlinx.android.synthetic.main.activity_main.*
+import kotlinx.android.synthetic.main.bottom_resultview_preview.*
+import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
-import java.lang.ref.WeakReference
import java.net.URI
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,113 +124,112 @@ 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,
+ "org.videolan.vlc.player.result",
+ "extra_position",
+ "extra_duration",
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
+ }
+}
+
+val MPV = object : ResultResume(
+ MPV_PACKAGE,
+ //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
+ position = "position",
+ duration = "duration",
+) {
+ override fun getPosition(intent: Intent?): Long {
+ return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+
+ override fun getDuration(intent: Intent?): Long {
+ return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
+ }
+}
+
+val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
+
+val resumeApps = arrayOf(
+ VLC, MPV, WEB_VIDEO
+)
+
+// Short name for requests client to make it nicer to use
+
+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
@@ -303,7 +238,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
*
* This is a very bad solution but I was unable to find a better one.
**/
- var nextSearchQuery: String? = null
+ private var nextSearchQuery: String? = null
/**
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
@@ -319,16 +254,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)
@@ -340,8 +265,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
isWebview: Boolean
): Boolean =
with(activity) {
- // TODO MUCH BETTER HANDLING
-
// Invalid URIs can crash
fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
@@ -351,7 +274,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 {
@@ -367,6 +290,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this@with.runOnUiThread {
try {
showToast(
+ this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
@@ -381,45 +305,20 @@ 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) {
nextSearchQuery =
- try {
- URLDecoder.decode(query, "UTF-8")
- } catch (t: Throwable) {
- logError(t)
- query
- }
- // Use both navigation views to support both layouts.
- // It might be better to use the QuickSearch.
- activity?.findViewById(R.id.nav_view)?.selectedItemId =
- R.id.navigation_search
- activity?.findViewById(R.id.nav_rail_view)?.selectedItemId =
- R.id.navigation_search
- } else if (safeURI(str)?.scheme == APP_STRING_PLAYER) {
- val uri = Uri.parse(str)
- val name = uri.getQueryParameter("name")
- val url = URLDecoder.decode(uri.authority, "UTF-8")
-
- navigate(
- R.id.global_to_navigation_player,
- GeneratorPlayer.newInstance(
- LinkGenerator(
- listOf(BasicLink(url, name)),
- extract = true,
- )
- )
- )
- } else if (safeURI(str)?.scheme == APP_STRING_RESUME_WATCHING) {
+ URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
+ nav_view.selectedItemId = R.id.navigation_search
+ } else if (safeURI(str)?.scheme == appStringResumeWatching) {
val id =
- str.substringAfter("$APP_STRING_RESUME_WATCHING://").toIntOrNull()
+ str.substringAfter("$appStringResumeWatching://").toIntOrNull()
?: return false
ioSafe {
val resumeWatchingCard =
@@ -435,12 +334,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this.navigate(R.id.navigation_downloads)
return true
} else {
- synchronized(apis) {
- for (api in apis) {
- if (str.startsWith(api.mainUrl)) {
- loadResult(str, api.name)
- return true
- }
+ for (api in apis) {
+ if (str.startsWith(api.mainUrl)) {
+ loadResult(str, api.name)
+ return true
}
}
}
@@ -451,30 +348,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 +368,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
@@ -499,7 +378,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
this.hideKeyboard()
// Fucks up anime info layout since that has its own layout
- binding?.castMiniControllerHolder?.isVisible =
+ cast_mini_controller_holder?.isVisible =
!listOf(
R.id.navigation_results_phone,
R.id.navigation_results_tv,
@@ -523,7 +402,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
R.id.navigation_settings_general,
R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins,
- R.id.navigation_test_providers,
).contains(destination.id)
@@ -533,30 +411,17 @@ 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 {
+ nav_host_fragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
- val push =
- if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
-
- if (!this.isLtr()) {
- params.setMargins(
- params.leftMargin,
- params.topMargin,
- push,
- params.bottomMargin
- )
- } else {
- params.setMargins(
- push,
- params.topMargin,
- params.rightMargin,
- params.bottomMargin
- )
- }
+ params.setMargins(
+ if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
+ params.topMargin,
+ params.rightMargin,
+ params.bottomMargin
+ )
layoutParams = params
}
@@ -564,53 +429,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
Configuration.ORIENTATION_LANDSCAPE -> {
true
}
-
Configuration.ORIENTATION_PORTRAIT -> {
- isLayout(TV or EMULATOR)
+ false
}
-
else -> {
false
}
}
- binding?.apply {
- navRailView.isVisible = isNavVisible && landscape
- navView.isVisible = isNavVisible && !landscape
+ nav_view?.isVisible = isNavVisible && !landscape
+ nav_rail_view?.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()
+ nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
+ nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
}
//private var mCastSession: CastSession? = null
- var mSessionManager: SessionManager? = null
+ lateinit var mSessionManager: SessionManager
private val mSessionManagerListener: SessionManagerListener by lazy { SessionManagerListenerImpl() }
private inner class SessionManagerListenerImpl : SessionManagerListener {
@@ -647,10 +484,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 +503,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
try {
if (isCastApiAvailable()) {
- mSessionManager?.removeSessionManagerListener(mSessionManagerListener)
+ mSessionManager.removeSessionManagerListener(mSessionManagerListener)
//mCastSession = null
}
} catch (e: Exception) {
@@ -674,10 +511,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- val response = CommonActivity.dispatchKeyEvent(this, event)
- if (response != null)
- return response
+
+ override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
+ CommonActivity.dispatchKeyEvent(this, event)?.let {
+ return it
+ }
return super.dispatchKeyEvent(event)
}
@@ -704,16 +542,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)
@@ -763,381 +620,73 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
- synchronized(allProviders) {
- // Load cloned sites after plugins have been loaded since clones depend on plugins.
- try {
- getKey>(USER_PROVIDER_API)?.let { list ->
- list.forEach { custom ->
- allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
- ?.let {
- allProviders.add(it.javaClass.getDeclaredConstructor().newInstance().apply {
- name = custom.name
- lang = custom.lang
- mainUrl = custom.url.trimEnd('/')
- canBeOverridden = false
- })
- }
- }
+ // Load cloned sites after plugins have been loaded since clones depend on plugins.
+ try {
+ getKey>(USER_PROVIDER_API)?.let { list ->
+ list.forEach { custom ->
+ allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
+ ?.let {
+ allProviders.add(it.javaClass.newInstance().apply {
+ name = custom.name
+ lang = custom.lang
+ mainUrl = custom.url.trimEnd('/')
+ canBeOverridden = false
+ })
+ }
}
- // it.hashCode() is not enough to make sure they are distinct
- apis =
- allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
- APIHolder.apiMap = null
- } catch (e: Exception) {
- logError(e)
}
+ // it.hashCode() is not enough to make sure they are distinct
+ apis =
+ allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
+ APIHolder.apiMap = null
+ } catch (e: Exception) {
+ logError(e)
}
}
}
}
lateinit var viewModel: ResultViewModel2
- 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)
}
private fun hidePreviewPopupDialog() {
+ viewModel.clear()
bottomPreviewPopup.dismissSafe(this)
- bottomPreviewPopup = null
- bottomPreviewBinding = null
}
- private var bottomPreviewPopup: BottomSheetDialog? = null
- private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
- private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
- val ret = (bottomPreviewBinding ?: run {
+ var bottomPreviewPopup: BottomSheetDialog? = null
+ private fun showPreviewPopupDialog(): BottomSheetDialog {
+ val ret = (bottomPreviewPopup ?: run {
val builder =
BottomSheetDialog(this)
- val binding: BottomResultviewPreviewBinding =
- BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false)
- bottomPreviewBinding = binding
- builder.setContentView(binding.root)
+ builder.setContentView(R.layout.bottom_resultview_preview)
builder.setOnDismissListener {
bottomPreviewPopup = null
- bottomPreviewBinding = null
viewModel.clear()
}
builder.setCanceledOnTouchOutside(true)
builder.show()
- bottomPreviewPopup = builder
- binding
+ builder
})
-
+ bottomPreviewPopup = ret
return ret
}
- var binding: ActivityMainBinding? = null
-
- object TvFocus {
- data class FocusTarget(
- val width: Int,
- val height: Int,
- val x: Float,
- val y: Float,
- ) {
- companion object {
- fun lerp(a: FocusTarget, b: FocusTarget, lerp: Float): FocusTarget {
- val ilerp = 1 - lerp
- return FocusTarget(
- width = (a.width * ilerp + b.width * lerp).toInt(),
- height = (a.height * ilerp + b.height * lerp).toInt(),
- x = a.x * ilerp + b.x * lerp,
- y = a.y * ilerp + b.y * lerp
- )
- }
- }
- }
-
- var last: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
- var current: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
-
- var focusOutline: WeakReference = WeakReference(null)
- var lastFocus: WeakReference = WeakReference(null)
- private val layoutListener: View.OnLayoutChangeListener =
- View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
- // shitty fix for layouts
- lastFocus.get()?.apply {
- updateFocusView(
- this, same = true
- )
- postDelayed({
- updateFocusView(
- lastFocus.get(), same = false
- )
- }, 300)
- }
- }
- private val attachListener: View.OnAttachStateChangeListener =
- object : View.OnAttachStateChangeListener {
- override fun onViewAttachedToWindow(v: View) {
- updateFocusView(v)
- }
-
- override fun onViewDetachedFromWindow(v: View) {
- // removes the focus view but not the listener as updateFocusView(null) will remove the listener
- 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 {
- layoutParams = layoutParams?.apply {
- width = target.width
- height = target.height
- }
-
- translationX = target.x
- translationY = target.y
- bringToFront()
- }
- }
-
- private var animator: ValueAnimator? = null
-
- /** if this is enabled it will keep the focus unmoving
- * during listview move */
- private const val NO_MOVE_LIST: Boolean = false
-
- /** If this is enabled then it will try to move the
- * listview focus to the left instead of center */
- private const val LEFTMOST_MOVE_LIST: Boolean = true
-
- private val reflectedScroll by lazy {
- try {
- RecyclerView::class.java.declaredMethods.firstOrNull {
- it.name == "scrollStep"
- }?.also { it.isAccessible = true }
- } catch (t: Throwable) {
- null
- }
- }
-
- @MainThread
- fun updateFocusView(newFocus: View?, same: Boolean = false) {
- val focusOutline = focusOutline.get() ?: return
- val lastView = lastFocus.get()
- val exactlyTheSame = lastView == newFocus && newFocus != null
- if (!exactlyTheSame) {
- lastView?.removeOnLayoutChangeListener(layoutListener)
- lastView?.removeOnAttachStateChangeListener(attachListener)
- (lastView?.parent as? RecyclerView)?.apply {
- removeOnLayoutChangeListener(layoutListener)
- //removeOnScrollListener(scrollListener)
- }
- }
-
- val wasGone = focusOutline.isGone
-
- val visible =
- newFocus != null && newFocus.measuredHeight > 0 && newFocus.measuredWidth > 0 && newFocus.isShown && newFocus.tag != "tv_no_focus_tag"
- focusOutline.isVisible = visible
-
- if (newFocus != null) {
- lastFocus = WeakReference(newFocus)
- val parent = newFocus.parent
- var targetDx = 0
- if (parent is RecyclerView) {
- val layoutManager = parent.layoutManager
- if (layoutManager is LinearListLayout && layoutManager.orientation == LinearLayoutManager.HORIZONTAL) {
- val dx =
- LinearSnapHelper().calculateDistanceToFinalSnap(layoutManager, newFocus)
- ?.get(0)
-
- if (dx != null) {
- val rdx = if (LEFTMOST_MOVE_LIST) {
- // this makes the item the leftmost in ltr, instead of center
- val diff =
- ((layoutManager.width - layoutManager.paddingStart - newFocus.measuredWidth) / 2) - newFocus.marginStart
- dx + if (parent.isRtl()) {
- -diff
- } else {
- diff
- }
- } else {
- if (dx > 0) dx else 0
- }
-
- if (!NO_MOVE_LIST) {
- parent.smoothScrollBy(rdx, 0)
- } else {
- val smoothScroll = reflectedScroll
- if (smoothScroll == null) {
- parent.smoothScrollBy(rdx, 0)
- } else {
- try {
- // this is very fucked but because it is a protected method to
- // be able to compute the scroll I use reflection, scroll, then
- // scroll back, then smooth scroll and set the no move
- val out = IntArray(2)
- smoothScroll.invoke(parent, rdx, 0, out)
- val scrolledX = out[0]
- if (abs(scrolledX) <= 0) { // newFocus.measuredWidth*2
- smoothScroll.invoke(parent, -rdx, 0, out)
- parent.smoothScrollBy(scrolledX, 0)
- if (NO_MOVE_LIST) targetDx = scrolledX
- }
- } catch (t: Throwable) {
- parent.smoothScrollBy(rdx, 0)
- }
- }
- }
- }
- }
- }
-
- val out = IntArray(2)
- newFocus.getLocationInWindow(out)
- val (screenX, screenY) = out
- var (x, y) = screenX.toFloat() to screenY.toFloat()
- val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY
-
- if (!newFocus.isLtr()) {
- x = x - focusOutline.rootView.width + newFocus.measuredWidth
- }
- x -= targetDx
-
- // out of bounds = 0,0
- if (screenX == 0 && screenY == 0) {
- focusOutline.isVisible = false
- }
- if (!exactlyTheSame) {
- (newFocus.parent as? RecyclerView)?.apply {
- addOnLayoutChangeListener(layoutListener)
- //addOnScrollListener(scrollListener)
- }
- newFocus.addOnLayoutChangeListener(layoutListener)
- newFocus.addOnAttachStateChangeListener(attachListener)
- }
- val start = FocusTarget(
- x = currentX,
- y = currentY,
- width = focusOutline.measuredWidth,
- height = focusOutline.measuredHeight
- )
- val end = FocusTarget(
- x = x,
- y = y,
- width = newFocus.measuredWidth,
- height = newFocus.measuredHeight
- )
-
- // 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) {
- animator?.cancel()
- last = start
- current = end
- setTargetPosition(end)
- return
- }
-
- // if running then "reuse"
- if (animator?.isRunning == true) {
- current = end
- return
- } else {
- animator?.cancel()
- }
-
-
- last = start
- current = end
-
- // if previously gone, then tp
- if (wasGone) {
- setTargetPosition(current)
- return
- }
-
- // animate between a and b
- animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
- startDelay = 0
- duration = 200
- addUpdateListener { animation ->
- val animatedValue = animation.animatedValue as Float
- val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
- setTargetPosition(target)
- }
- start()
- }
-
- // post check
- if (!same) {
- newFocus.postDelayed({
- updateFocusView(lastFocus.get(), same = true)
- }, 200)
- }
-
- /*
-
- the following is working, but somewhat bad code code
-
- if (!wasGone) {
- (focusOutline.parent as? ViewGroup)?.let {
- TransitionManager.endTransitions(it)
- TransitionManager.beginDelayedTransition(
- it,
- TransitionSet().addTransition(ChangeBounds())
- .addTransition(ChangeTransform())
- .setDuration(100)
- )
- }
- }
-
- focusOutline.layoutParams = focusOutline.layoutParams?.apply {
- width = newFocus.measuredWidth
- height = newFocus.measuredHeight
- }
- focusOutline.translationX = x.toFloat()
- focusOutline.translationY = y.toFloat()*/
- }
- }
- }
-
- 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)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val errorFile = filesDir.resolve("last_error")
+ var lastError: String? = null
if (errorFile.exists() && errorFile.isFile) {
lastError = errorFile.readText(Charset.defaultCharset())
errorFile.delete()
- } else {
- lastError = null
}
val settingsForProvider = SettingsJson()
@@ -1151,131 +700,30 @@ 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)
+ } catch (e: Exception) {
+ logError(e)
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
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 {
- 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)
- }
- }
+ if (isTvSettings()) {
+ setContentView(R.layout.activity_main_tv)
+ } else {
+ setContentView(R.layout.activity_main)
}
- // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
- binding = try {
- if (isLayout(TV or EMULATOR)) {
- val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
- setContentView(newLocalBinding.root)
+ changeStatusBarState(isEmulatorSettings())
- 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
- }
-
- 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)
- }
- }
-
- ActivityMainBinding.bind(newLocalBinding.root) // this may crash
- } else {
- val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
- setContentView(newLocalBinding.root)
- newLocalBinding
- }
- } catch (t: Throwable) {
- showToast(txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG)
- 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
- }
- }
-
- // Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
- if (this.getKey(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
- main {
- if (checkGithubConnectivity()) {
- 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) }
- }
- }
- }
-
- ioSafe { SafeFile.check(this@MainActivity) }
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
- showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
+ showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} 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)
@@ -1292,18 +740,13 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
loadAllOnlinePlugins(this@MainActivity)
}
- //Automatically download not existing plugins, using mode specified.
- val autoDownloadPlugin = AutoDownloadMode.getEnum(
- settingsManager.getInt(
+ //Automatically download not existing plugins
+ if (settingsManager.getBoolean(
getString(R.string.auto_download_plugins_key),
- 0
- )
- ) ?: AutoDownloadMode.Disable
- if (autoDownloadPlugin != AutoDownloadMode.Disable) {
- PluginManager.downloadNotExistingPluginsAndLoad(
- this@MainActivity,
- autoDownloadPlugin
+ false
)
+ ) {
+ PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
}
}
@@ -1328,184 +771,59 @@ 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()
+ bottomPreviewPopup.dismissSafe(this)
return@observeNullable
}
when (resource) {
is Resource.Failure -> {
- showToast(R.string.error)
- viewModel.clear()
+ showToast(this, R.string.error)
hidePreviewPopupDialog()
}
-
is Resource.Loading -> {
showPreviewPopupDialog().apply {
- resultviewPreviewLoading.isVisible = true
- resultviewPreviewResult.isVisible = false
- resultviewPreviewLoadingShimmer.startShimmer()
+ resultview_preview_loading?.isVisible = true
+ resultview_preview_result?.isVisible = false
+ resultview_preview_loading_shimmer?.startShimmer()
}
}
-
is Resource.Success -> {
val d = resource.value
showPreviewPopupDialog().apply {
- resultviewPreviewLoading.isVisible = false
- resultviewPreviewResult.isVisible = true
- resultviewPreviewLoadingShimmer.stopShimmer()
+ resultview_preview_loading?.isVisible = false
+ resultview_preview_result?.isVisible = true
+ resultview_preview_loading_shimmer?.stopShimmer()
- resultviewPreviewTitle.text = d.title
+ resultview_preview_title?.text = d.title
- resultviewPreviewMetaType.setText(d.typeText)
- resultviewPreviewMetaYear.setText(d.yearText)
- resultviewPreviewMetaDuration.setText(d.durationText)
- resultviewPreviewMetaRating.setText(d.ratingText)
+ resultview_preview_meta_type.setText(d.typeText)
+ resultview_preview_meta_year.setText(d.yearText)
+ resultview_preview_meta_duration.setText(d.durationText)
+ resultview_preview_meta_rating.setText(d.ratingText)
- resultviewPreviewDescription.setTextHtml(d.plotText)
- resultviewPreviewPoster.setImage(
+ resultview_preview_description?.setText(d.plotText)
+ resultview_preview_poster?.setImage(
d.posterImage ?: d.posterBackgroundImage
)
- setUserData(syncViewModel.userData.value)
- setWatchStatus(viewModel.watchStatus.value)
- setSubscribeStatus(viewModel.subscribeStatus.value)
-
- resultviewPreviewBookmark.setOnClickListener {
+ resultview_preview_poster?.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])
+ bookmarksUpdatedEvent(true)
}
}
- 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
- resultviewPreviewDescription.setOnClickListener { view ->
+ if (!isTvSettings()) // dont want this clickable on tv layout
+ resultview_preview_description?.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
@@ -1515,8 +833,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
- resultviewPreviewMoreInfo.setOnClickListener {
- viewModel.clear()
+ resultview_preview_more_info?.setOnClickListener {
hidePreviewPopupDialog()
lastPopup?.let {
loadSearchResult(it)
@@ -1550,26 +867,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)
@@ -1577,9 +874,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
- apis = synchronized(allProviders) {
- allProviders.distinctBy { it }
- }
+ apis = allProviders.distinctBy { it }
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@@ -1592,23 +887,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
// Intercept search and add a query
- updateNavBar(navDestination)
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
bundle?.apply {
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
+ nextSearchQuery = null
}
}
-
- 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)
@@ -1621,47 +905,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
.setPopExitAnim(R.anim.nav_pop_exit)
.setPopUpTo(navController.graph.startDestination, false)
.build()*/
+ nav_view?.setupWithNavController(navController)
+ val nav_rail = findViewById(R.id.nav_rail_view)
+ nav_rail?.setupWithNavController(navController)
+ if (isTvSettings()) {
+ nav_rail?.background?.alpha = 200
+ } else {
+ nav_rail?.background?.alpha = 255
- val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
-
- binding?.navView?.apply {
- itemRippleColor = rippleColor
- itemActiveIndicatorColor = rippleColor
- setupWithNavController(navController)
- setOnItemSelectedListener { item ->
- onNavDestinationSelected(
- item,
- navController
- )
- }
}
-
- binding?.navRailView?.apply {
- itemRippleColor = rippleColor
- itemActiveIndicatorColor = rippleColor
- setupWithNavController(navController)
- if (isLayout(TV or EMULATOR)) {
- background?.alpha = 200
- } else {
- background?.alpha = 255
- }
-
- setOnItemSelectedListener { item ->
- onNavDestinationSelected(
- item,
- navController
- )
- }
-
- fun noFocus(view: View) {
- view.tag = view.context.getString(R.string.tv_no_focus_tag)
- (view as? ViewGroup)?.let {
- for (child in it.children) {
- noFocus(child)
- }
- }
- }
- noFocus(this)
+ nav_rail?.setOnItemSelectedListener { item ->
+ onNavDestinationSelected(
+ item,
+ navController
+ )
+ }
+ nav_view?.setOnItemSelectedListener { item ->
+ onNavDestinationSelected(
+ item,
+ navController
+ )
+ }
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ updateNavBar(destination)
}
loadCache()
@@ -1684,12 +950,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
true
}*/
+ val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
+ nav_view?.itemRippleColor = rippleColor
+ nav_rail?.itemRippleColor = rippleColor
+ nav_rail?.itemActiveIndicatorColor = rippleColor
+ nav_view?.itemActiveIndicatorColor = rippleColor
if (!checkWrite()) {
requestRW()
if (checkWrite()) return
}
- //CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
+ CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
// THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION
//if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) {
@@ -1756,15 +1027,14 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
- synchronized(allProviders) {
- for (api in allProviders) {
- providersAndroidManifestString += "\n"
- }
+ for (api in allProviders) {
+ providersAndroidManifestString += "\n"
}
+
println(providersAndroidManifestString)
}
@@ -1774,15 +1044,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 +1060,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 +1075,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,42 +1088,5 @@ 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 {
- return try {
- app.get(
- "https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck",
- timeout = 5
- ).text.trim() == "ok"
- } catch (t: Throwable) {
- false
- }
- }
-}
\ 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 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
index 4bed3169..7a62fb52 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/AsianLoad.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/AsianLoad.kt
@@ -9,7 +9,7 @@ import java.net.URI
open class AsianLoad : ExtractorApi() {
override var name = "AsianLoad"
- override var mainUrl = "https://asianhdplay.pro"
+ override var mainUrl = "https://asianembed.io"
override val requiresReferer = true
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
@@ -43,4 +43,4 @@ open class AsianLoad : ExtractorApi() {
return extractedLinksList
}
}
-}
+}
\ No newline at end of file
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 92%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt
index 2d56fe1f..3e0a03c0 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/ByteShare.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/ByteShare.kt
@@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.utils.*
open class ByteShare : ExtractorApi() {
override val name = "ByteShare"
- override val mainUrl = "https://byteshare.to"
+ override val mainUrl = "https://byteshare.net"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List {
@@ -20,4 +20,4 @@ open class ByteShare : ExtractorApi() {
)
return sources
}
-}
+}
\ No newline at end of file
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 94%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt
index 42f6eddb..6a2f399d 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Cda.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Cda.kt
@@ -1,11 +1,13 @@
package com.lagradost.cloudstream3.extractors
-import com.lagradost.cloudstream3.USER_AGENT
-import com.lagradost.cloudstream3.app
-import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import com.lagradost.cloudstream3.*
+import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import android.util.Log
import java.net.URLDecoder
open class Cda: ExtractorApi() {
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 60%
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..125e4bcf 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Dailymotion.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Dailymotion.kt
@@ -6,19 +6,13 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
-import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
+import com.lagradost.cloudstream3.utils.Qualities
import java.net.URL
-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,68 +26,68 @@ 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) ->
+ metaData.qualities.forEach { (key, video) ->
video.forEach {
- getStream(it.url, this.name, callback)
+ callback.invoke(
+ ExtractorLink(
+ name,
+ "$name $key",
+ it.url,
+ "",
+ Qualities.Unknown.value,
+ true
+ )
+ )
}
}
}
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
}
return null
}
- private suspend fun getStream(
- streamLink: String,
- name: String,
- callback: (ExtractorLink) -> Unit
- ) {
- return generateM3u8(
- name,
- streamLink,
- "",
- ).forEach(callback)
- }
data class Config(
val context: Context,
val dmInternalData: InternalData
)
data class InternalData(
- val ts: 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 65%
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..7ec1fb22 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/DoodExtractor.kt
@@ -7,22 +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"
-}
-
class DoodWfExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.wf"
}
@@ -54,9 +38,6 @@ class DoodWsExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.ws"
}
-class DoodYtExtractor : DoodLaExtractor() {
- override var mainUrl = "https://dood.yt"
-}
open class DoodLaExtractor : ExtractorApi() {
override var name = "DoodStream"
@@ -68,14 +49,13 @@ 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(
- this.name,
+ trueUrl,
this.name,
trueUrl,
mainUrl,
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 66%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
index 3e38b446..eddbf6df 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Evolaod.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Evolaod.kt
@@ -16,7 +16,26 @@ open class Evoload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List {
- val id = url.replace("https://evoload.io/e/", "") // wanted media id
+ val lang = url.substring(0, 2)
+ val flag =
+ if (lang == "vo") {
+ " \uD83C\uDDEC\uD83C\uDDE7"
+ }
+ else if (lang == "vf"){
+ " \uD83C\uDDE8\uD83C\uDDF5"
+ } else {
+ ""
+ }
+
+ val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
+ url
+ } else {
+ url.substring(2, url.length)
+ }
+ //println(lang)
+ //println(cleaned_url)
+
+ val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
@@ -25,9 +44,9 @@ open class Evoload : ExtractorApi() {
return listOf(
ExtractorLink(
name,
- name,
+ name + flag,
link,
- url,
+ cleaned_url,
Qualities.Unknown.value,
)
)
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/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
new file mode 100644
index 00000000..bc910a7e
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Filesim.kt
@@ -0,0 +1,57 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.*
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
+import java.net.URI
+
+class FileMoon : Filesim() {
+ override val mainUrl = "https://filemoon.to"
+ override val name = "FileMoon"
+}
+
+open class Filesim : ExtractorApi() {
+ override val name = "Filesim"
+ override val mainUrl = "https://files.im"
+ override val requiresReferer = false
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ with(app.get(url).document) {
+ this.select("script").forEach { script ->
+ if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
+ val data = getAndUnpack(script.data())
+ val foundData = Regex("""sources:\[(.*?)]""").find(data)?.groupValues?.get(1) ?: return@forEach
+ val fixedData = foundData.replace("file:", """"file":""")
+
+ parseJson>("[$fixedData]").forEach {
+ callback.invoke(
+ ExtractorLink(
+ name,
+ name,
+ it.file,
+ "$mainUrl/",
+ Qualities.Unknown.value,
+ URI(it.file).path.endsWith(".m3u8")
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+
+ private data class ResponseSource(
+ @JsonProperty("file") val file: String,
+ @JsonProperty("type") val type: String?,
+ @JsonProperty("label") val label: String?
+ )
+
+}
\ No newline at end of file
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/GuardareStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
similarity index 98%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
index 3d046267..2adc00d5 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/GuardareStream.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/GuardareStream.kt
@@ -58,7 +58,7 @@ open class GuardareStream : ExtractorApi() {
jsonVideoData.data.forEach {
callback.invoke(
ExtractorLink(
- this.name,
+ it.file + ".${it.type}",
this.name,
it.file + ".${it.type}",
mainUrl,
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 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
index bfd7cae5..f5dde774 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Hxfile.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Hxfile.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Neonime7n : Hxfile() {
override val name = "Neonime7n"
- override val mainUrl = "https://neonime.fun"
+ override val mainUrl = "https://7njctn.neonime.watch"
override val redirect = false
}
@@ -19,7 +19,7 @@ class Neonime8n : Hxfile() {
class KotakAnimeid : Hxfile() {
override val name = "KotakAnimeid"
- override val mainUrl = "https://nontonanimeid.bio"
+ override val mainUrl = "https://kotakanimeid.com"
override val requiresReferer = true
}
@@ -97,4 +97,4 @@ open class Hxfile : ExtractorApi() {
@JsonProperty("label") val label: String?
)
-}
+}
\ No newline at end of file
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/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
new file mode 100644
index 00000000..c28a8900
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Linkbox.kt
@@ -0,0 +1,48 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.getQualityFromName
+
+open class Linkbox : ExtractorApi() {
+ override val name = "Linkbox"
+ override val mainUrl = "https://www.linkbox.to"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2)
+ app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe()?.data?.rList?.map { link ->
+ callback.invoke(
+ ExtractorLink(
+ name,
+ name,
+ link.url,
+ url,
+ getQualityFromName(link.resolution)
+ )
+ )
+ }
+ }
+
+ data class RList(
+ @JsonProperty("url") val url: String,
+ @JsonProperty("resolution") val resolution: String?,
+ )
+
+ data class Data(
+ @JsonProperty("rList") val rList: List?,
+ )
+
+ data class Responses(
+ @JsonProperty("data") val data: Data?,
+ )
+
+}
\ No newline at end of file
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 96%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
index 51939cc2..aaa33ca1 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Moviehab.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt
@@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class MoviehabNet : Moviehab() {
- override var mainUrl = "https://play.moviehab.asia"
+ override var mainUrl = "https://play.moviehab.net"
}
open class Moviehab : ExtractorApi() {
@@ -41,4 +41,4 @@ open class Moviehab : ExtractorApi() {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
new file mode 100644
index 00000000..93a280ed
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Mp4Upload.kt
@@ -0,0 +1,34 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.Qualities
+import com.lagradost.cloudstream3.utils.getAndUnpack
+
+open class Mp4Upload : ExtractorApi() {
+ override var name = "Mp4Upload"
+ override var mainUrl = "https://www.mp4upload.com"
+ private val srcRegex = Regex("""player\.src\("(.*?)"""")
+ override val requiresReferer = true
+
+ override suspend fun getUrl(url: String, referer: String?): List? {
+ with(app.get(url)) {
+ getAndUnpack(this.text).let { unpackedText ->
+ val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
+ srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
+ return listOf(
+ ExtractorLink(
+ name,
+ name,
+ link,
+ url,
+ quality ?: Qualities.Unknown.value,
+ )
+ )
+ }
+ }
+ }
+ return null
+ }
+}
\ No newline at end of file
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 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
index c7f4ac76..44657196 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/MultiQuality.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/MultiQuality.kt
@@ -9,7 +9,7 @@ import java.net.URI
open class MultiQuality : ExtractorApi() {
override var name = "MultiQuality"
- override var mainUrl = "https://anihdplay.com"
+ override var mainUrl = "https://gogo-play.net"
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
private val urlRegex = Regex("""(.*?)([^/]+$)""")
@@ -56,4 +56,4 @@ open class MultiQuality : ExtractorApi() {
return extractedLinksList
}
}
-}
+}
\ No newline at end of file
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/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/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/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..8ef6c463 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.nl"
+}
- override val name = "Minoplres" // formerly SpeedoStream
+open class SpeedoStream : ExtractorApi() {
+ override val name = "SpeedoStream"
+ override val mainUrl = "https://speedostream.com"
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,
)
-}
+
+
+}
\ No newline at end of file
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 66%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
index df050cf3..958d63fb 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/StreamSB.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/StreamSB.kt
@@ -6,51 +6,6 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
-import kotlin.random.Random
-
-class Sblona : StreamSB() {
- override var name = "Sblona"
- override var mainUrl = "https://sblona.com"
-}
-
-class Lvturbo : StreamSB() {
- override var name = "Lvturbo"
- override var mainUrl = "https://lvturbo.com"
-}
-
-class Sbrapid : StreamSB() {
- override var name = "Sbrapid"
- override var mainUrl = "https://sbrapid.com"
-}
-
-class Sbface : StreamSB() {
- override var name = "Sbface"
- override var mainUrl = "https://sbface.com"
-}
-
-class Sbsonic : StreamSB() {
- override var name = "Sbsonic"
- override var mainUrl = "https://sbsonic.com"
-}
-
-class Vidgomunimesb : StreamSB() {
- override var mainUrl = "https://vidgomunimesb.xyz"
-}
-
-class Sbasian : StreamSB() {
- override var mainUrl = "https://sbasian.pro"
- override var name = "Sbasian"
-}
-
-class Sbnet : StreamSB() {
- override var name = "Sbnet"
- override var mainUrl = "https://sbnet.one"
-}
-
-class Keephealth : StreamSB() {
- override var name = "Keephealth"
- override var mainUrl = "https://keephealth.info"
-}
class Sbspeed : StreamSB() {
override var name = "Sbspeed"
@@ -122,70 +77,24 @@ class StreamSB10 : StreamSB() {
override var mainUrl = "https://sbplay2.xyz"
}
-class StreamSB11 : StreamSB() {
- override var mainUrl = "https://sbbrisk.com"
-}
-
-class Sblongvu : StreamSB() {
- override var mainUrl = "https://sblongvu.com"
-}
-
+// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
+// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
open class StreamSB : ExtractorApi() {
override var name = "StreamSB"
override var mainUrl = "https://watchsb.com"
override val requiresReferer = false
- private val alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
- override suspend fun getUrl(
- url: String,
- referer: String?,
- subtitleCallback: (SubtitleFile) -> Unit,
- callback: (ExtractorLink) -> Unit
- ) {
- val regexID =
- Regex("(embed-[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+|/e/[a-zA-Z\\d]{0,8}[a-zA-Z\\d_-]+)")
- val id = regexID.findAll(url).map {
- it.value.replace(Regex("(embed-|/e/)"), "")
- }.first()
- val master = "$mainUrl/375664356a494546326c4b797c7c6e756577776778623171737/${encodeId(id)}"
- val headers = mapOf(
- "watchsb" to "sbstream",
- )
- val mapped = app.get(
- master.lowercase(),
- headers = headers,
- referer = url,
- ).parsedSafe()
- M3u8Helper.generateM3u8(
- name,
- mapped?.streamData?.file ?: return,
- url,
- headers = headers
- ).forEach(callback)
+ private val hexArray = "0123456789ABCDEF".toCharArray()
- mapped.streamData.subs?.map {sub ->
- subtitleCallback.invoke(
- SubtitleFile(
- sub.label.toString(),
- sub.file ?: return@map null,
- )
- )
- }
- }
-
- private fun encodeId(id: String): String {
- val code = "${createHashTable()}||$id||${createHashTable()}||streamsb"
- return code.toCharArray().joinToString("") { char ->
- char.code.toString(16)
- }
- }
-
- private fun createHashTable(): String {
- return buildString {
- repeat(12) {
- append(alphabet[Random.nextInt(alphabet.length)])
- }
+ private fun bytesToHex(bytes: ByteArray): String {
+ val hexChars = CharArray(bytes.size * 2)
+ for (j in bytes.indices) {
+ val v = bytes[j].toInt() and 0xFF
+
+ hexChars[j * 2] = hexArray[v ushr 4]
+ hexChars[j * 2 + 1] = hexArray[v and 0x0F]
}
+ return String(hexChars)
}
data class Subs (
@@ -209,4 +118,42 @@ open class StreamSB : ExtractorApi() {
@JsonProperty("status_code") val statusCode: Int,
)
-}
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val regexID =
+ Regex("(embed-[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+|/e/[a-zA-Z0-9]{0,8}[a-zA-Z0-9_-]+)")
+ val id = regexID.findAll(url).map {
+ it.value.replace(Regex("(embed-|/e/)"), "")
+ }.first()
+// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
+ val master = "$mainUrl/sources50/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
+ val headers = mapOf(
+ "watchsb" to "sbstream",
+ )
+ val mapped = app.get(
+ master.lowercase(),
+ headers = headers,
+ referer = url,
+ ).parsedSafe()
+ // val urlmain = mapped.streamData.file.substringBefore("/hls/")
+ M3u8Helper.generateM3u8(
+ name,
+ mapped?.streamData?.file ?: return,
+ url,
+ headers = headers
+ ).forEach(callback)
+
+ mapped.streamData.subs?.map {sub ->
+ subtitleCallback.invoke(
+ SubtitleFile(
+ sub.label.toString(),
+ sub.file ?: return@map null,
+ )
+ )
+ }
+ }
+}
\ No newline at end of file
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/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 97%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
index 13aa48c6..d721dea8 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Tantifilm.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Tantifilm.kt
@@ -30,7 +30,7 @@ open class Tantifilm : ExtractorApi() {
val jsonvideodata = parseJson(response)
return jsonvideodata.data.map {
ExtractorLink(
- this.name,
+ it.file+".${it.type}",
this.name,
it.file+".${it.type}",
mainUrl,
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 57%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
index 86bd9e0b..5109acc3 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/Uqload.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Uqload.kt
@@ -7,10 +7,6 @@ class Uqload1 : Uqload() {
override var mainUrl = "https://uqload.com"
}
-class Uqload2 : Uqload() {
- override var mainUrl = "https://uqload.co"
-}
-
open class Uqload : ExtractorApi() {
override val name: String = "Uqload"
override val mainUrl: String = "https://www.uqload.com"
@@ -19,14 +15,30 @@ open class Uqload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List? {
- with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
+ val lang = url.substring(0, 2)
+ val flag =
+ if (lang == "vo") {
+ " \uD83C\uDDEC\uD83C\uDDE7"
+ }
+ else if (lang == "vf"){
+ " \uD83C\uDDE8\uD83C\uDDF5"
+ } else {
+ ""
+ }
+
+ val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
+ url
+ } else {
+ url.substring(2, url.length)
+ }
+ with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
return listOf(
ExtractorLink(
name,
- name,
+ name + flag,
link,
- url,
+ cleaned_url,
Qualities.Unknown.value,
)
)
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/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/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..12a76a9b
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Voe.kt
@@ -0,0 +1,32 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.lagradost.cloudstream3.SubtitleFile
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.M3u8Helper
+
+open class Voe : ExtractorApi() {
+ override val name = "Voe"
+ override val mainUrl = "https://voe.sx"
+ override val requiresReferer = true
+
+ override suspend fun getUrl(
+ url: String,
+ referer: String?,
+ subtitleCallback: (SubtitleFile) -> Unit,
+ callback: (ExtractorLink) -> Unit
+ ) {
+ val res = app.get(url, referer = referer).document
+ val link = res.select("script").find { it.data().contains("const sources") }?.data()
+ ?.substringAfter("\"hls\": \"")?.substringBefore("\",")
+
+ M3u8Helper.generateM3u8(
+ name,
+ link ?: return,
+ "$mainUrl/",
+ headers = mapOf("Origin" to "$mainUrl/")
+ ).forEach(callback)
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt
new file mode 100644
index 00000000..ad3f0150
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/VoeExtractor.kt
@@ -0,0 +1,54 @@
+package com.lagradost.cloudstream3.extractors
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.lagradost.cloudstream3.app
+import com.lagradost.cloudstream3.utils.AppUtils.parseJson
+import com.lagradost.cloudstream3.utils.ExtractorApi
+import com.lagradost.cloudstream3.utils.ExtractorLink
+import com.lagradost.cloudstream3.utils.getQualityFromName
+
+open class VoeExtractor : ExtractorApi() {
+ override val name: String = "Voe"
+ override val mainUrl: String = "https://voe.sx"
+ override val requiresReferer = false
+
+ private data class ResponseLinks(
+ @JsonProperty("hls") val hls: String?,
+ @JsonProperty("mp4") val mp4: String?,
+ @JsonProperty("video_height") val label: Int?
+ //val type: String // Mp4
+ )
+
+ override suspend fun getUrl(url: String, referer: String?): List {
+ val html = app.get(url).text
+ if (html.isNotBlank()) {
+ val src = html.substringAfter("const sources =").substringBefore(";")
+ // Remove last comma, it is not proper json otherwise
+ .replace("0,", "0")
+ // Make json use the proper quotes
+ .replace("'", "\"")
+
+ //Log.i(this.name, "Result => (src) ${src}")
+ parseJson(src)?.let { voeLink ->
+ //Log.i(this.name, "Result => (voeLink) ${voeLink}")
+
+ // Always defaults to the hls link, but returns the mp4 if null
+ val linkUrl = voeLink.hls ?: voeLink.mp4
+ val linkLabel = voeLink.label?.toString() ?: ""
+ if (!linkUrl.isNullOrEmpty()) {
+ return listOf(
+ ExtractorLink(
+ name = this.name,
+ source = this.name,
+ url = linkUrl,
+ quality = getQualityFromName(linkLabel),
+ referer = url,
+ isM3u8 = voeLink.hls != null
+ )
+ )
+ }
+ }
+ }
+ return emptyList()
+ }
+}
\ 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/XStreamCdn.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
similarity index 94%
rename from library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
rename to app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
index ccb2fde7..15ff0436 100644
--- a/library/src/commonMain/kotlin/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/XStreamCdn.kt
@@ -8,16 +8,6 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
-class StreamM4u : XStreamCdn() {
- override val name: String = "StreamM4u"
- override val mainUrl: String = "https://streamm4u.club"
-}
-
-class Fembed9hd : XStreamCdn() {
- override var mainUrl = "https://fembed9hd.com"
- override var name = "Fembed9hd"
-}
-
class Cdnplayer: XStreamCdn() {
override val name: String = "Cdnplayer"
override val mainUrl: String = "https://cdnplayer.online"
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/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/CrossTmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
index 5bbb4538..07aa904e 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt
@@ -21,11 +21,10 @@ class CrossTmdbProvider : TmdbProvider() {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
}
- private val validApis
- get() =
- synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
- //.distinctBy { it.uniqueId }
-
+ private val validApis by lazy {
+ apis.filter { it.lang == this.lang && it::class.java != this::class.java }
+ //.distinctBy { it.uniqueId }
+ }
data class CrossMetaData(
@JsonProperty("isSuccess") val isSuccess: Boolean,
@@ -61,8 +60,7 @@ class CrossTmdbProvider : TmdbProvider() {
override suspend fun load(url: String): LoadResponse? {
val base = super.load(url)?.apply {
- this.recommendations =
- this.recommendations?.filterIsInstance() // TODO REMOVE
+ this.recommendations = this.recommendations?.filterIsInstance() // TODO REMOVE
val matchName = filterName(this.name)
when (this) {
is MovieLoadResponse -> {
@@ -100,7 +98,6 @@ class CrossTmdbProvider : TmdbProvider() {
this.dataUrl =
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
}
-
else -> {
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
}
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..e8ac1876
--- /dev/null
+++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt
@@ -0,0 +1,70 @@
+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 by lazy {
+ 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 50%
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..afe956cc 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,40 @@ 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) }
+}
+
+inline fun some(value: T?): Some {
+ return if (value == null) {
+ Some.None
+ } else {
+ Some.Success(value)
+ }
+}
+
+sealed class Some {
+ data class Success(val value: T) : Some()
+ object None : Some()
+
+ override fun toString(): String {
+ return when (this) {
+ is None -> "None"
+ is Success -> "Some(${value.toString()})"
+ }
+ }
+}
+
+sealed class ResourceSome {
+ data class Success(val value: T) : ResourceSome()
+ object None : ResourceSome()
+ data class Loading(val data: Any? = null) : ResourceSome()
+}
+
sealed class Resource {
data class Success(val value: T) : Resource()
data class Failure(
@@ -84,21 +121,13 @@ suspend fun suspendSafeApiCall(apiCall: suspend () -> T): T? {
}
}
-fun Throwable.getAllMessages(): String {
- return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "")
-}
-
-fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
- val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else ""
- return prefix + this.stackTrace.joinToString(
- separator = "\n"
- ) {
- "${it.fileName} ${it.lineNumber}"
- }
-}
-
fun safeFail(throwable: Throwable): Resource {
- val stackTraceMsg = throwable.getStackTracePretty()
+ val stackTraceMsg =
+ (throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
+ separator = "\n"
+ ) {
+ "${it.fileName} ${it.lineNumber}"
+ }
return Resource.Failure(false, null, null, stackTraceMsg)
}
@@ -118,70 +147,6 @@ fun CoroutineScope.launchSafe(
return this.launch(context, start, obj)
}
-fun throwAbleToResource(
- throwable: Throwable
-): Resource {
- return when (throwable) {
- is NullPointerException -> {
- for (line in throwable.stackTrace) {
- if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
- return Resource.Failure(
- false,
- null,
- null,
- "NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
- )
- }
- }
- safeFail(throwable)
- }
- is SocketTimeoutException, is InterruptedIOException -> {
- Resource.Failure(
- true,
- null,
- null,
- "Connection Timeout\nPlease try again later."
- )
- }
-// 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}")
- }
- is ErrorLoadingException -> {
- Resource.Failure(
- true,
- null,
- null,
- throwable.message ?: "Error loading, try again later."
- )
- }
- is NotImplementedError -> {
- Resource.Failure(false, null, null, "This operation is not implemented.")
- }
- is SSLHandshakeException -> {
- Resource.Failure(
- true,
- null,
- null,
- (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
- )
- }
- is CancellationException -> {
- throwable.cause?.let {
- throwAbleToResource(it)
- } ?: safeFail(throwable)
- }
- else -> safeFail(throwable)
- }
-}
-
suspend fun safeApiCall(
apiCall: suspend () -> T,
): Resource {
@@ -190,7 +155,60 @@ suspend fun safeApiCall(
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
logError(throwable)
- throwAbleToResource(throwable)
+ when (throwable) {
+ is NullPointerException -> {
+ for (line in throwable.stackTrace) {
+ if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
+ return@withContext Resource.Failure(
+ false,
+ null,
+ null,
+ "NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
+ )
+ }
+ }
+ safeFail(throwable)
+ }
+ is SocketTimeoutException, is InterruptedIOException -> {
+ Resource.Failure(
+ true,
+ null,
+ null,
+ "Connection Timeout\nPlease try again later."
+ )
+ }
+ 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.")
+ }
+ is ErrorLoadingException -> {
+ Resource.Failure(
+ true,
+ null,
+ null,
+ throwable.message ?: "Error loading, try again later."
+ )
+ }
+ is NotImplementedError -> {
+ Resource.Failure(false, null, null, "This operation is not implemented.")
+ }
+ is SSLHandshakeException -> {
+ Resource.Failure(
+ true,
+ null,
+ null,
+ (throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
+ )
+ }
+ else -> safeFail(throwable)
+ }
}
}
}
\ No newline at end of file
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