Compare commits

..

1 commit

Author SHA1 Message Date
LagradOst
3659906516 FUCK ANDROID STORAGE 2023-08-24 21:04:18 +02:00
596 changed files with 11878 additions and 28938 deletions

View file

@ -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

View file

@ -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.

View file

@ -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
- label: I have written a short but informative title.
required: true
- label: I will fill out all of the requested information in this form.
required: true

6
.github/locales.py vendored
View file

@ -1,7 +1,6 @@
import re
import glob
import requests
import os
import lxml.etree as ET # builtin library doesn't preserve comments
@ -54,16 +53,11 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
try:
tree = ET.parse(file)
for child in tree.getroot():
if not child.text:
continue
if child.text.startswith("\\@string/"):
print(f"[{file}] fixing {child.attrib['name']}")
child.text = child.text.replace("\\@string/", "@string/")
with open(file, 'wb') as fp:
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\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}")

View file

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

View file

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

View file

@ -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: |

View file

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

View file

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

View file

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

6
.idea/gradle.xml generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,16 +5,12 @@ import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.util.DisplayMetrics
import android.util.Log
import android.view.Gravity
import android.view.KeyEvent
import android.view.View
import android.view.*
import android.view.View.NO_ID
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
@ -30,14 +26,12 @@ import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
@ -46,9 +40,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
import java.util.*
enum class FocusDirection {
Start,
@ -66,29 +58,11 @@ object CommonActivity {
_activity = WeakReference(value)
}
@MainThread
fun setActivityInstance(newActivity: Activity?) {
activity = newActivity
}
@MainThread
fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
}
val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
// screenWidth and screenHeight does always
// refer to the screen while in landscape mode
val screenWidth: Int
get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
val screenHeight: Int
get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false
@ -100,7 +74,8 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
private var currentToast: Toast? = null
var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return
@ -156,19 +131,25 @@ object CommonActivity {
} catch (e: Exception) {
logError(e)
}
try {
val binding = ToastBinding.inflate(act.layoutInflater)
binding.text.text = message.trim()
val inflater =
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(
R.layout.toast,
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
)
val text = layout.findViewById(R.id.text) as TextView
text.text = message.trim()
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
val toast = Toast(act)
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
currentToast = toast
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.view = layout
//https://github.com/PureWriter/ToastCompat
toast.show()
currentToast = toast
} catch (e: Exception) {
logError(e)
}
@ -202,25 +183,23 @@ object CommonActivity {
setLocale(this, localeCode)
}
fun init(act: Activity) {
setActivityInstance(act)
val componentActivity = activity as? ComponentActivity ?: return
fun init(act: ComponentActivity?) {
if (act == null) return
activity = act
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
componentActivity.updateLocale()
componentActivity.updateTv()
act.updateLocale()
act.updateTv()
NewPipe.init(DownloaderTestImpl.getInstance())
for (resumeApp in resumeApps) {
resumeApp.launcher =
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
@ -237,11 +216,11 @@ object CommonActivity {
// Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
componentActivity,
act,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
val requestPermissionLauncher = componentActivity.registerForActivityResult(
val requestPermissionLauncher = act.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted")
@ -277,35 +256,12 @@ object CommonActivity {
}
}
fun updateTheme(act: Activity) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
if (settingsManager
.getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
loadThemes(act)
}
}
private fun mapSystemTheme(act: Activity): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val currentNightMode =
act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme
else -> R.style.AppTheme // Night mode is active, we're using dark theme
}
} else {
return R.style.AppTheme
}
}
fun loadThemes(act: Activity?) {
if (act == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
val currentTheme =
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
"System" -> mapSystemTheme(act)
"Black" -> R.style.AppTheme
"Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode
@ -319,15 +275,12 @@ object CommonActivity {
val currentOverlayTheme =
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
"Normal" -> R.style.OverlayPrimaryColorNormal
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
"Orange" -> R.style.OverlayPrimaryColorOrange
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
"Maroon" -> R.style.OverlayPrimaryColorMaroon
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
"Grey" -> R.style.OverlayPrimaryColorGrey
"White" -> R.style.OverlayPrimaryColorWhite
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
"Brown" -> R.style.OverlayPrimaryColorBrown
"Purple" -> R.style.OverlayPrimaryColorPurple
"Green" -> R.style.OverlayPrimaryColorGreen
@ -336,7 +289,6 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink
"Lavender" -> R.style.OverlayPrimaryColorLavender
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
@ -376,14 +328,6 @@ object CommonActivity {
currentLook = currentLook.parent as? View ?: break
}*/
private fun View.hasContent(): Boolean {
return isShown && when (this) {
//is RecyclerView -> this.childCount > 0
is ViewGroup -> this.childCount > 0
else -> true
}
}
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
fun continueGetNextFocus(
root: Any?,
@ -404,17 +348,16 @@ object CommonActivity {
} ?: return null
next = localLook(view, nextId) ?: next
val shown = next.hasContent()
// if cant focus but visible then break and let android decide
// the exception if is the view is a parent and has children that wants focus
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
} ?: false
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
if (!next.isFocusable && next.isShown && !hasChildrenThatWantsFocus) return null
// if not shown then continue because we will "skip" over views to get to a replacement
if (!shown) {
if (!next.isShown) {
// we don't want a while true loop, so we let android decide if we find a recursive view
if (next == view) return null
return getNextFocus(root, next, direction, depth + 1)
@ -488,6 +431,20 @@ object CommonActivity {
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
// Tested keycodes on remote:
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
// KeyEvent.KEYCODE_MEDIA_REWIND
// KeyEvent.KEYCODE_MENU
// KeyEvent.KEYCODE_MEDIA_NEXT
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
// 149 keycode_numpad 5
when (keyCode) {

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors
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

View file

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

View file

@ -0,0 +1,140 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
class Moviesapi : Chillx() {
override val name = "Moviesapi"
override val mainUrl = "https://w1.moviesapi.club"
}
class Bestx : Chillx() {
override val name = "Bestx"
override val mainUrl = "https://bestx.stream"
}
class Watchx : Chillx() {
override val name = "Watchx"
override val mainUrl = "https://watchx.top"
}
open class Chillx : ExtractorApi() {
override val name = "Chillx"
override val mainUrl = "https://chillx.top"
override val requiresReferer = true
companion object {
private const val KEY = "11x&W5UBrcqn\$9Yl"
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val master = Regex("MasterJS\\s*=\\s*'([^']+)").find(
app.get(
url,
referer = referer
).text
)?.groupValues?.get(1)
val encData = AppUtils.tryParseJson<AESData>(base64Decode(master ?: return))
val decrypt = cryptoAESHandler(encData ?: return, KEY, false)
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
// required
val headers = mapOf(
"Accept" to "*/*",
"Connection" to "keep-alive",
"Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "cross-site",
"Origin" to mainUrl,
)
callback.invoke(
ExtractorLink(
name,
name,
source ?: return,
"$mainUrl/",
Qualities.P1080.value,
headers = headers,
isM3u8 = true
)
)
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
?.filter { it.kind == "captions" }?.map { track ->
subtitleCallback.invoke(
SubtitleFile(
track.label ?: "",
track.file ?: return@map null
)
)
}
}
private fun cryptoAESHandler(
data: AESData,
pass: String,
encrypt: Boolean = true
): String {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512")
val spec = PBEKeySpec(
pass.toCharArray(),
data.salt?.hexToByteArray(),
data.iterations?.toIntOrNull() ?: 1,
256
)
val key = factory.generateSecret(spec)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
return if (!encrypt) {
cipher.init(
Cipher.DECRYPT_MODE,
SecretKeySpec(key.encoded, "AES"),
IvParameterSpec(data.iv?.hexToByteArray())
)
String(cipher.doFinal(base64DecodeArray(data.ciphertext.toString())))
} else {
cipher.init(
Cipher.ENCRYPT_MODE,
SecretKeySpec(key.encoded, "AES"),
IvParameterSpec(data.iv?.hexToByteArray())
)
base64Encode(cipher.doFinal(data.ciphertext?.toByteArray()))
}
}
private fun String.hexToByteArray(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
data class AESData(
@JsonProperty("ciphertext") val ciphertext: String? = null,
@JsonProperty("iv") val iv: String? = null,
@JsonProperty("salt") val salt: String? = null,
@JsonProperty("iterations") val iterations: String? = null,
)
data class Tracks(
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
@JsonProperty("kind") val kind: String? = null,
)
}

View file

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

View file

@ -7,18 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
import kotlinx.coroutines.delay
class D0000d : DoodLaExtractor() {
override var mainUrl = "https://d0000d.com"
}
class D000dCom : DoodLaExtractor() {
override var mainUrl = "https://d000d.com"
}
class DoodstreamCom : DoodLaExtractor() {
override var mainUrl = "https://doodstream.com"
}
class Dooood : DoodLaExtractor() {
override var mainUrl = "https://dooood.com"
}
@ -68,10 +56,9 @@ open class DoodLaExtractor : ExtractorApi() {
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
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("<title>").substringBefore("</title>"))?.groupValues?.get(0)
return listOf(
ExtractorLink(

View file

@ -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<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
return listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
)
} catch (e: DigestException) {
return null
}
}
private fun cryptoAESHandler(
data: AesData,
pass: ByteArray,
encrypt: Boolean = true
): String? {
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(data.ct)))
} else {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(data.ct.toByteArray()))
}
}
private fun Regex.first(str: String): String? {
return find(str)?.groupValues?.getOrNull(1)
}
@ -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<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+"))
?.joinToString("") {
Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password")
val decryptedData = cryptoAESHandler(data, password, false, "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,

View file

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

View file

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

View file

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

View file

@ -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")
)
)
}

View file

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

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors
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.*

View file

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

View file

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

View file

@ -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"
}

View file

@ -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<ExtractorLink>? {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()

View file

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

View file

@ -25,13 +25,9 @@ open class Vidmoly : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
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()

View file

@ -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")
)
)
}

View file

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

View file

@ -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<Response>().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"))
}
}
}

View file

@ -4,8 +4,8 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URI
open class Wibufile : ExtractorApi() {
override val name: String = "Wibufile"
@ -28,8 +28,10 @@ open class Wibufile : ExtractorApi() {
video ?: return,
"$mainUrl/",
Qualities.Unknown.value,
type = INFER_TYPE
URI(url).path.endsWith(".m3u8")
)
)
}
}

View file

@ -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)
}
}

View file

@ -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

View file

@ -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<ExternalKeys>()
.parsedSafe<ExternalKeys>()?.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<NewExternalKeys>()
.parsedSafe<NewExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
BACKUP_KEY_DATA
)
}
suspend fun getNewWcoKey(): NewExternalKeys? {

View file

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

View file

@ -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(

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