mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Compare commits
1 commit
master
...
fuck-stora
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3659906516 |
596 changed files with 11878 additions and 28938 deletions
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
|
|
@ -80,13 +80,13 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
options:
|
||||||
- label: I am sure my issue is related to the app and **NOT some extension**.
|
|
||||||
required: true
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
- label: I have written a short but informative title.
|
- label: I have written a short but informative title.
|
||||||
required: true
|
required: true
|
||||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
||||||
required: true
|
required: true
|
||||||
|
- label: If related to a provider, I have checked the site and it works, but not the app.
|
||||||
|
required: true
|
||||||
- label: I will fill out all of the requested information in this form.
|
- label: I will fill out all of the requested information in this form.
|
||||||
required: true
|
required: true
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -2,7 +2,7 @@ blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Request a new provider or report bug with an existing provider
|
- name: Request a new provider or report bug with an existing provider
|
||||||
url: https://github.com/recloudstream
|
url: https://github.com/recloudstream
|
||||||
about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
|
||||||
- name: Discord
|
- name: Discord
|
||||||
url: https://discord.gg/5Hus6fM
|
url: https://discord.gg/5Hus6fM
|
||||||
about: Join our discord for faster support on smaller issues.
|
about: Join our discord for faster support on smaller issues.
|
||||||
|
|
|
||||||
6
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
6
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
|
@ -27,7 +27,9 @@ body:
|
||||||
label: Acknowledgements
|
label: Acknowledgements
|
||||||
description: Your issue will be closed if you haven't done these steps.
|
description: Your issue will be closed if you haven't done these steps.
|
||||||
options:
|
options:
|
||||||
- label: My suggestion is **NOT** about adding a new provider
|
|
||||||
required: true
|
|
||||||
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have written a short but informative title.
|
||||||
|
required: true
|
||||||
|
- label: I will fill out all of the requested information in this form.
|
||||||
|
required: true
|
||||||
|
|
|
||||||
6
.github/locales.py
vendored
6
.github/locales.py
vendored
|
|
@ -1,7 +1,6 @@
|
||||||
import re
|
import re
|
||||||
import glob
|
import glob
|
||||||
import requests
|
import requests
|
||||||
import os
|
|
||||||
import lxml.etree as ET # builtin library doesn't preserve comments
|
import lxml.etree as ET # builtin library doesn't preserve comments
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,16 +53,11 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
|
||||||
try:
|
try:
|
||||||
tree = ET.parse(file)
|
tree = ET.parse(file)
|
||||||
for child in tree.getroot():
|
for child in tree.getroot():
|
||||||
if not child.text:
|
|
||||||
continue
|
|
||||||
if child.text.startswith("\\@string/"):
|
if child.text.startswith("\\@string/"):
|
||||||
print(f"[{file}] fixing {child.attrib['name']}")
|
print(f"[{file}] fixing {child.attrib['name']}")
|
||||||
child.text = child.text.replace("\\@string/", "@string/")
|
child.text = child.text.replace("\\@string/", "@string/")
|
||||||
with open(file, 'wb') as fp:
|
with open(file, 'wb') as fp:
|
||||||
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
|
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
|
||||||
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
|
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
|
||||||
# Remove trailing new line to be consistent with weblate
|
|
||||||
fp.seek(-1, os.SEEK_END)
|
|
||||||
fp.truncate()
|
|
||||||
except ET.ParseError as ex:
|
except ET.ParseError as ex:
|
||||||
print(f"[{file}] {ex}")
|
print(f"[{file}] {ex}")
|
||||||
|
|
|
||||||
10
.github/workflows/build_to_archive.yml
vendored
10
.github/workflows/build_to_archive.yml
vendored
|
|
@ -19,21 +19,21 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- name: Generate access token (archive)
|
- name: Generate access token (archive)
|
||||||
id: generate_archive_token
|
id: generate_archive_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
|
|
@ -58,7 +58,7 @@ jobs:
|
||||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||||
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: "recloudstream/cloudstream-archive"
|
repository: "recloudstream/cloudstream-archive"
|
||||||
token: ${{ steps.generate_archive_token.outputs.token }}
|
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||||
|
|
|
||||||
7
.github/workflows/generate_dokka.yml
vendored
7
.github/workflows/generate_dokka.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
|
@ -43,13 +43,12 @@ jobs:
|
||||||
rm -rf "./-cloudstream"
|
rm -rf "./-cloudstream"
|
||||||
|
|
||||||
- name: Setup JDK 17
|
- name: Setup JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v1
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: 'adopt'
|
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v2
|
||||||
|
|
||||||
- name: Generate Dokka
|
- name: Generate Dokka
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
8
.github/workflows/issue_action.yml
vendored
8
.github/workflows/issue_action.yml
vendored
|
|
@ -10,7 +10,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
|
@ -27,7 +27,7 @@ jobs:
|
||||||
comment-body: '${index}. ${similarity} #${number}'
|
comment-body: '${index}. ${similarity} #${number}'
|
||||||
- name: Label if possible duplicate
|
- name: Label if possible duplicate
|
||||||
if: steps.similarity.outputs.similar-issues-found =='true'
|
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate_token.outputs.token }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
labels: ["possible duplicate"]
|
labels: ["possible duplicate"]
|
||||||
})
|
})
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Automatically close issues that dont follow the issue template
|
- name: Automatically close issues that dont follow the issue template
|
||||||
uses: lucasbento/auto-close-issues@v1.0.2
|
uses: lucasbento/auto-close-issues@v1.0.2
|
||||||
with:
|
with:
|
||||||
|
|
@ -68,7 +68,7 @@ jobs:
|
||||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||||
- name: Label if mentions provider
|
- name: Label if mentions provider
|
||||||
if: steps.provider_check.outputs.name != 'none'
|
if: steps.provider_check.outputs.name != 'none'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate_token.outputs.token }}
|
github-token: ${{ steps.generate_token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|
|
||||||
9
.github/workflows/prerelease.yml
vendored
9
.github/workflows/prerelease.yml
vendored
|
|
@ -18,14 +18,14 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/secrets"
|
repository: "recloudstream/secrets"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
|
|
@ -43,8 +43,7 @@ jobs:
|
||||||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: |
|
run: |
|
||||||
./gradlew assemblePrerelease build androidSourcesJar
|
./gradlew assemblePrerelease makeJar androidSourcesJar
|
||||||
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
|
|
||||||
env:
|
env:
|
||||||
SIGNING_KEY_ALIAS: "key0"
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
|
|
||||||
6
.github/workflows/pull_request.yml
vendored
6
.github/workflows/pull_request.yml
vendored
|
|
@ -6,9 +6,9 @@ jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 17
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v2
|
||||||
with:
|
with:
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
- name: Run Gradle
|
- name: Run Gradle
|
||||||
run: ./gradlew assemblePrereleaseDebug
|
run: ./gradlew assemblePrereleaseDebug
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: pull-request-build
|
name: pull-request-build
|
||||||
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||||
|
|
|
||||||
4
.github/workflows/update_locales.yml
vendored
4
.github/workflows/update_locales.yml
vendored
|
|
@ -18,12 +18,12 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Generate access token
|
- name: Generate access token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
uses: tibdex/github-app-token@v2
|
uses: tibdex/github-app-token@v1
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.GH_APP_ID }}
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
private_key: ${{ secrets.GH_APP_KEY }}
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
repository: "recloudstream/cloudstream"
|
repository: "recloudstream/cloudstream"
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate_token.outputs.token }}
|
token: ${{ steps.generate_token.outputs.token }}
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|
|
||||||
6
.idea/gradle.xml
generated
6
.idea/gradle.xml
generated
|
|
@ -4,16 +4,16 @@
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
<option name="delegatedBuild" value="true" />
|
||||||
|
<option name="testRunner" value="GRADLE" />
|
||||||
|
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/library" />
|
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveExternalAnnotations" value="false" />
|
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
import org.jetbrains.dokka.gradle.DokkaTask
|
import org.jetbrains.dokka.gradle.DokkaTask
|
||||||
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.archivesName
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("com.google.devtools.ksp")
|
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
|
id("kotlin-kapt")
|
||||||
id("org.jetbrains.dokka")
|
id("org.jetbrains.dokka")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,16 +32,15 @@ android {
|
||||||
enable = true
|
enable = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/* disable this for now
|
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path("CMakeLists.txt")
|
path("CMakeLists.txt")
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
if (prereleaseStoreFile != null) {
|
|
||||||
create("prerelease") {
|
create("prerelease") {
|
||||||
|
if (prereleaseStoreFile != null) {
|
||||||
storeFile = file(prereleaseStoreFile)
|
storeFile = file(prereleaseStoreFile)
|
||||||
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||||
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||||
|
|
@ -52,16 +49,16 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdk = 34
|
compileSdk = 33
|
||||||
buildToolsVersion = "34.0.0"
|
buildToolsVersion = "30.0.3"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.lagradost.cloudstream3"
|
applicationId = "com.lagradost.cloudstream3"
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 33 /* Android 14 is Fu*ked
|
targetSdk = 29
|
||||||
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
|
|
||||||
versionCode = 64
|
versionCode = 59
|
||||||
versionName = "4.4.0"
|
versionName = "4.1.7"
|
||||||
|
|
||||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||||
|
|
@ -71,9 +68,9 @@ android {
|
||||||
val localProperties = gradleLocalProperties(rootDir)
|
val localProperties = gradleLocalProperties(rootDir)
|
||||||
|
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"long",
|
"String",
|
||||||
"BUILD_DATE",
|
"BUILDDATE",
|
||||||
"${System.currentTimeMillis()}"
|
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||||
)
|
)
|
||||||
buildConfigField(
|
buildConfigField(
|
||||||
"String",
|
"String",
|
||||||
|
|
@ -87,9 +84,8 @@ android {
|
||||||
)
|
)
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
ksp {
|
kapt {
|
||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
includeCompileClasspath = true
|
||||||
arg("exportSchema", "true")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +108,6 @@ android {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions.add("state")
|
flavorDimensions.add("state")
|
||||||
productFlavors {
|
productFlavors {
|
||||||
create("stable") {
|
create("stable") {
|
||||||
|
|
@ -124,31 +119,30 @@ android {
|
||||||
resValue("bool", "is_prerelease", "true")
|
resValue("bool", "is_prerelease", "true")
|
||||||
buildConfigField("boolean", "BETA", "true")
|
buildConfigField("boolean", "BETA", "true")
|
||||||
applicationIdSuffix = ".prerelease"
|
applicationIdSuffix = ".prerelease"
|
||||||
if (signingConfigs.names.contains("prerelease")) {
|
|
||||||
signingConfig = signingConfigs.getByName("prerelease")
|
signingConfig = signingConfigs.getByName("prerelease")
|
||||||
} else {
|
|
||||||
logger.warn("No prerelease signing config!")
|
|
||||||
}
|
|
||||||
versionNameSuffix = "-PRE"
|
versionNameSuffix = "-PRE"
|
||||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//toolchain {
|
||||||
|
// languageVersion.set(JavaLanguageVersion.of(17))
|
||||||
|
// }
|
||||||
|
// jvmToolchain(17)
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = "1.8"
|
||||||
|
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
||||||
|
}
|
||||||
lint {
|
lint {
|
||||||
abortOnError = false
|
abortOnError = false
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
buildConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace = "com.lagradost.cloudstream3"
|
namespace = "com.lagradost.cloudstream3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,132 +151,127 @@ repositories {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Testing
|
implementation("com.google.android.mediahome:video:1.0.0")
|
||||||
testImplementation("junit:junit:4.13.2")
|
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||||
testImplementation("org.json:json:20240303")
|
testImplementation("org.json:json:20180813")
|
||||||
androidTestImplementation("androidx.test:core")
|
|
||||||
implementation("androidx.test.ext:junit-ktx:1.2.1")
|
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
|
||||||
|
|
||||||
// Android Core & Lifecycle
|
implementation("androidx.core:core-ktx:1.10.1")
|
||||||
implementation("androidx.core:core-ktx:1.13.1")
|
implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
|
||||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.8.3")
|
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3")
|
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
|
|
||||||
|
|
||||||
// Design & UI
|
// dont change this to 1.6.0 it looks ugly af
|
||||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
implementation("com.google.android.material:material:1.5.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
|
||||||
implementation("com.google.android.material:material:1.12.0")
|
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
|
implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
|
||||||
|
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||||
|
testImplementation("junit:junit:4.13.2")
|
||||||
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
|
androidTestImplementation("androidx.test:core")
|
||||||
|
|
||||||
|
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
||||||
|
// implementation("org.jsoup:jsoup:1.13.1")
|
||||||
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||||
|
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||||
|
|
||||||
|
implementation("com.github.bumptech.glide:glide:4.13.1")
|
||||||
|
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
||||||
|
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
|
||||||
|
|
||||||
|
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||||
|
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
|
|
||||||
// Glide Module
|
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
||||||
ksp("com.github.bumptech.glide:ksp:4.16.0")
|
|
||||||
implementation("com.github.bumptech.glide:glide:4.16.0")
|
|
||||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.16.0")
|
|
||||||
|
|
||||||
// For KSP -> Official Annotation Processors are Not Yet Supported for KSP
|
// Media 3
|
||||||
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
|
|
||||||
implementation("com.google.guava:guava:33.2.1-android")
|
|
||||||
implementation("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
|
|
||||||
|
|
||||||
// Media 3 (ExoPlayer)
|
|
||||||
implementation("androidx.media3:media3-ui:1.1.1")
|
|
||||||
implementation("androidx.media3:media3-cast:1.1.1")
|
|
||||||
implementation("androidx.media3:media3-common:1.1.1")
|
implementation("androidx.media3:media3-common:1.1.1")
|
||||||
implementation("androidx.media3:media3-session:1.1.1")
|
|
||||||
implementation("androidx.media3:media3-exoplayer:1.1.1")
|
implementation("androidx.media3:media3-exoplayer:1.1.1")
|
||||||
implementation("com.google.android.mediahome:video:1.0.0")
|
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-ui:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-session:1.1.1")
|
||||||
|
implementation("androidx.media3:media3-cast:1.1.1")
|
||||||
implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
|
implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
|
||||||
implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
|
implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
|
||||||
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
|
// Custom ffmpeg extension for audio codecs
|
||||||
|
implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
|
||||||
|
|
||||||
// PlayBack
|
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
||||||
implementation("com.jaredrummler:colorpicker:1.1.0") // Subtitle Color Picker
|
|
||||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Custom FF-MPEG Lib for Audio Codecs
|
|
||||||
implementation("com.github.teamnewpipe:NewPipeExtractor:176da72") /* For Trailers
|
|
||||||
^ Update to Latest Commits if Trailers Misbehave, github.com/TeamNewPipe/NewPipeExtractor/commits/dev */
|
|
||||||
implementation("com.github.albfernandez:juniversalchardet:2.5.0") // Subtitle Decoding
|
|
||||||
|
|
||||||
// Crash Reports (AcraApplication.kt)
|
// Bug reports
|
||||||
implementation("ch.acra:acra-core:5.11.3")
|
implementation("ch.acra:acra-core:5.11.0")
|
||||||
implementation("ch.acra:acra-toast:5.11.3")
|
implementation("ch.acra:acra-toast:5.11.0")
|
||||||
|
|
||||||
|
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
||||||
|
//either for java sources:
|
||||||
|
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
||||||
|
//or for kotlin sources (requires kapt gradle plugin):
|
||||||
|
kapt("com.google.auto.service:auto-service:1.0")
|
||||||
|
|
||||||
|
// subtitle color picker
|
||||||
|
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||||
|
|
||||||
|
//run JS
|
||||||
|
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||||
|
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
||||||
|
implementation("org.mozilla:rhino:1.7.13")
|
||||||
|
|
||||||
|
// TorrentStream
|
||||||
|
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
||||||
|
|
||||||
|
// Downloading
|
||||||
|
implementation("androidx.work:work-runtime:2.8.1")
|
||||||
|
implementation("androidx.work:work-runtime-ktx:2.8.1")
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
|
||||||
|
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
|
||||||
|
implementation("com.github.Blatzar:NiceHttp:0.4.3")
|
||||||
|
// To fix SSL fuckery on android 9
|
||||||
|
implementation("org.conscrypt:conscrypt-android:2.2.1")
|
||||||
|
// Util to skip the URI file fuckery 🙏
|
||||||
|
implementation("com.github.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("androidx.tvprovider:tvprovider:1.0.0")
|
||||||
implementation("com.github.discord:OverlappingPanels:0.1.5") // Gestures
|
|
||||||
implementation("androidx.biometric:biometric:1.2.0-alpha05") // Fingerprint Authentication
|
|
||||||
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") // SeekBar Preview
|
|
||||||
implementation("io.github.g0dkar:qrcode-kotlin:4.2.0") // QR code for PIN Auth on TV
|
|
||||||
|
|
||||||
// Extensions & Other Libs
|
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||||
implementation("org.mozilla:rhino:1.7.15") // run JavaScript
|
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0") // Library/Ext Searching with Levenshtein Distance
|
|
||||||
implementation("com.github.LagradOst:SafeFile:0.0.6") // To Prevent the URI File Fu*kery
|
|
||||||
implementation("org.conscrypt:conscrypt-android:2.5.2") // To Fix SSL Fu*kery on Android 9
|
|
||||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.11.0") // TMDB API v3 Wrapper Made with RetroFit
|
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs_nio:2.0.4") //nio flavor needed for NewPipeExtractor
|
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") /* JSON Parser
|
|
||||||
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
|
|
||||||
Level 25 or Less. */
|
|
||||||
|
|
||||||
// Downloading & Networking
|
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
|
||||||
implementation("androidx.work:work-runtime:2.9.0")
|
// this should be updated frequently to avoid trailer fu*kery
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
implementation("com.github.TeamNewPipe:NewPipeExtractor:1f08d28")
|
||||||
implementation("com.github.Blatzar:NiceHttp:0.4.11") // HTTP Lib
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||||
|
|
||||||
implementation(project(":library") {
|
// Library/extensions searching with Levenshtein distance
|
||||||
// There does not seem to be a good way of getting the android flavor.
|
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||||
val isDebug = gradle.startParameter.taskRequests.any { task ->
|
|
||||||
task.args.any { arg ->
|
|
||||||
arg.contains("debug", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.extra.set("isDebug", isDebug)
|
// color palette for images -> colors
|
||||||
})
|
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Jar>("androidSourcesJar") {
|
tasks.register("androidSourcesJar", Jar::class) {
|
||||||
archiveClassifier.set("sources")
|
archiveClassifier.set("sources")
|
||||||
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources
|
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copyJar") {
|
// this is used by the gradlew plugin
|
||||||
from(
|
tasks.register("makeJar", Copy::class) {
|
||||||
"build/intermediates/compile_app_classes_jar/prereleaseDebug",
|
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||||
"../library/build/libs"
|
into("build")
|
||||||
)
|
include("classes.jar")
|
||||||
into("build/app-classes")
|
dependsOn("build")
|
||||||
include("classes.jar", "library-jvm*.jar")
|
|
||||||
// Remove the version
|
|
||||||
rename("library-jvm.*.jar", "library-jvm.jar")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the app classes and the library classes into classes.jar
|
|
||||||
tasks.register<Jar>("makeJar") {
|
|
||||||
// Duplicates cause hard to catch errors, better to fail at compile time.
|
|
||||||
duplicatesStrategy = DuplicatesStrategy.FAIL
|
|
||||||
dependsOn(tasks.getByName("copyJar"))
|
|
||||||
from(
|
|
||||||
zipTree("build/app-classes/classes.jar"),
|
|
||||||
zipTree("build/app-classes/library-jvm.jar")
|
|
||||||
)
|
|
||||||
destinationDirectory.set(layout.buildDirectory)
|
|
||||||
archivesName = "classes"
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<KotlinCompile> {
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "1.8"
|
|
||||||
freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<DokkaTask>().configureEach {
|
tasks.withType<DokkaTask>().configureEach {
|
||||||
|
|
@ -295,7 +284,6 @@ tasks.withType<DokkaTask>().configureEach {
|
||||||
|
|
||||||
// URL showing where the source code can be accessed through the web browser
|
// URL showing where the source code can be accessed through the web browser
|
||||||
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
||||||
|
|
||||||
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||||
remoteLineSuffix.set("#L")
|
remoteLineSuffix.set("#L")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
|
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
|
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
|
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
|
||||||
|
|
@ -19,7 +17,6 @@ import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
||||||
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
|
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
|
|
||||||
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
|
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
|
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
|
||||||
|
|
@ -120,12 +117,9 @@ class ExampleInstrumentedTest {
|
||||||
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||||
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||||
|
|
||||||
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
|
||||||
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
|
||||||
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
|
||||||
|
|
||||||
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
|
||||||
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,7 +148,7 @@ class ExampleInstrumentedTest {
|
||||||
fun providerCorrectHomepage() {
|
fun providerCorrectHomepage() {
|
||||||
runBlocking {
|
runBlocking {
|
||||||
getAllProviders().toList().amap { api ->
|
getAllProviders().toList().amap { api ->
|
||||||
TestingUtils.testHomepage(api, TestingUtils.Logger())
|
TestingUtils.testHomepage(api, ::println)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("Done providerCorrectHomepage")
|
println("Done providerCorrectHomepage")
|
||||||
|
|
@ -166,6 +160,7 @@ class ExampleInstrumentedTest {
|
||||||
TestingUtils.getDeferredProviderTests(
|
TestingUtils.getDeferredProviderTests(
|
||||||
this,
|
this,
|
||||||
getAllProviders(),
|
getAllProviders(),
|
||||||
|
::println
|
||||||
) { _, _ -> }
|
) { _, _ -> }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API -->
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
|
||||||
|
|
@ -14,14 +14,8 @@
|
||||||
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
|
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
|
||||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
|
||||||
|
|
||||||
<!-- Required for getting arbitrary Aniyomi packages -->
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
|
||||||
tools:ignore="QueryAllPackagesPermission" />
|
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
|
||||||
<!-- Fixes android tv fuckery -->
|
<!-- Fixes android tv fuckery -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.touchscreen"
|
android:name="android.hardware.touchscreen"
|
||||||
|
|
@ -41,11 +35,9 @@
|
||||||
<application
|
<application
|
||||||
android:name=".AcraApplication"
|
android:name=".AcraApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:appCategory="video"
|
android:appCategory="video"
|
||||||
android:banner="@mipmap/ic_banner"
|
android:banner="@mipmap/ic_banner"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
|
|
@ -53,7 +45,7 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="o">
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
|
@ -69,9 +61,7 @@
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:screenOrientation="userLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
android:supportsPictureInPicture="true"
|
android:supportsPictureInPicture="true">
|
||||||
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
|
||||||
android:launchMode="singleTask">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
|
@ -97,11 +87,17 @@
|
||||||
-->
|
-->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode"
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
android:supportsPictureInPicture="true">
|
android:supportsPictureInPicture="true">
|
||||||
|
<intent-filter android:exported="true">
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
@ -165,21 +161,6 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.account.AccountSelectActivity"
|
|
||||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter android:exported="true">
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.EasterEggMonke"
|
android:name=".ui.EasterEggMonke"
|
||||||
android:exported="true" />
|
android:exported="true" />
|
||||||
|
|
@ -187,14 +168,13 @@
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||||
android:enabled="false"
|
android:enabled="false"
|
||||||
android:exported="false">
|
android:exported="true">
|
||||||
<intent-filter android:exported="false">
|
<intent-filter android:exported="true">
|
||||||
<action android:name="restart_service" />
|
<action android:name="restart_service" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:name=".services.VideoDownloadService"
|
android:name=".services.VideoDownloadService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
@ -204,7 +184,6 @@
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:foregroundServiceType="dataSync"
|
|
||||||
android:name=".utils.PackageInstallerService"
|
android:name=".utils.PackageInstallerService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,12 @@ import android.content.Intent
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import com.lagradost.api.setContext
|
import com.google.auto.service.AutoService
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
|
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||||
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
||||||
|
|
@ -34,11 +32,12 @@ import org.acra.sender.ReportSenderFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.io.PrintStream
|
import java.io.PrintStream
|
||||||
|
import java.lang.Exception
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
class CustomReportSender : ReportSender {
|
class CustomReportSender : ReportSender {
|
||||||
// Sends all your crashes to google forms
|
// Sends all your crashes to google forms
|
||||||
override fun send(context: Context, errorContent: CrashReportData) {
|
override fun send(context: Context, errorContent: CrashReportData) {
|
||||||
|
|
@ -66,6 +65,7 @@ class CustomReportSender : ReportSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AutoService(ReportSenderFactory::class)
|
||||||
class CustomSenderFactory : ReportSenderFactory {
|
class CustomSenderFactory : ReportSenderFactory {
|
||||||
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
override fun create(context: Context, config: CoreConfiguration): ReportSender {
|
||||||
return CustomReportSender()
|
return CustomReportSender()
|
||||||
|
|
@ -82,8 +82,14 @@ class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
|
||||||
ACRA.errorReporter.handleException(error)
|
ACRA.errorReporter.handleException(error)
|
||||||
try {
|
try {
|
||||||
PrintStream(errorFile).use { ps ->
|
PrintStream(errorFile).use { ps ->
|
||||||
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
|
ps.println(String.format("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}"))
|
||||||
ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
|
ps.println(
|
||||||
|
String.format(
|
||||||
|
"Fatal exception on thread %s (%d)",
|
||||||
|
thread.name,
|
||||||
|
thread.id
|
||||||
|
)
|
||||||
|
)
|
||||||
error.printStackTrace(ps)
|
error.printStackTrace(ps)
|
||||||
}
|
}
|
||||||
} catch (ignored: FileNotFoundException) {
|
} catch (ignored: FileNotFoundException) {
|
||||||
|
|
@ -101,6 +107,7 @@ class AcraApplication : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
//NativeCrashHandler.initCrashHandler()
|
||||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||||
|
|
@ -146,7 +153,6 @@ class AcraApplication : Application() {
|
||||||
get() = _context?.get()
|
get() = _context?.get()
|
||||||
private set(value) {
|
private set(value) {
|
||||||
_context = WeakReference(value)
|
_context = WeakReference(value)
|
||||||
setContext(WeakReference(value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
|
||||||
|
|
@ -208,7 +214,7 @@ class AcraApplication : Application() {
|
||||||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||||
openBrowser(
|
openBrowser(
|
||||||
url,
|
url,
|
||||||
isLayout(TV or EMULATOR),
|
isTvSettings(),
|
||||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,12 @@ import android.app.Activity
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Gravity
|
import android.view.*
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.NO_ID
|
import android.view.View.NO_ID
|
||||||
import android.view.ViewGroup
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -30,14 +26,12 @@ import com.google.android.material.chip.ChipGroup
|
||||||
import com.google.android.material.navigationrail.NavigationRailView
|
import com.google.android.material.navigationrail.NavigationRailView
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.MainActivity.Companion.resumeApps
|
|
||||||
import com.lagradost.cloudstream3.databinding.ToastBinding
|
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
||||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||||
import com.lagradost.cloudstream3.ui.result.UiText
|
import com.lagradost.cloudstream3.ui.result.UiText
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||||
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
|
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.Event
|
import com.lagradost.cloudstream3.utils.Event
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper
|
import com.lagradost.cloudstream3.utils.UIHelper
|
||||||
|
|
@ -46,9 +40,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.Locale
|
import java.util.*
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
enum class FocusDirection {
|
enum class FocusDirection {
|
||||||
Start,
|
Start,
|
||||||
|
|
@ -66,29 +58,11 @@ object CommonActivity {
|
||||||
_activity = WeakReference(value)
|
_activity = WeakReference(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
|
||||||
fun setActivityInstance(newActivity: Activity?) {
|
|
||||||
activity = newActivity
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun Activity?.getCastSession(): CastSession? {
|
fun Activity?.getCastSession(): CastSession? {
|
||||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||||
}
|
}
|
||||||
|
|
||||||
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 canEnterPipMode: Boolean = false
|
||||||
var canShowPipMode: Boolean = false
|
var canShowPipMode: Boolean = false
|
||||||
|
|
@ -100,7 +74,8 @@ object CommonActivity {
|
||||||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||||
|
|
||||||
private var currentToast: Toast? = null
|
|
||||||
|
var currentToast: Toast? = null
|
||||||
|
|
||||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||||
val act = activity ?: return
|
val act = activity ?: return
|
||||||
|
|
@ -156,19 +131,25 @@ object CommonActivity {
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val binding = ToastBinding.inflate(act.layoutInflater)
|
val inflater =
|
||||||
binding.text.text = message.trim()
|
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
|
val layout: View = inflater.inflate(
|
||||||
|
R.layout.toast,
|
||||||
|
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
|
||||||
|
)
|
||||||
|
|
||||||
|
val text = layout.findViewById(R.id.text) as TextView
|
||||||
|
text.text = message.trim()
|
||||||
|
|
||||||
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
|
|
||||||
val toast = Toast(act)
|
val toast = Toast(act)
|
||||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
|
||||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||||
toast.view = binding.root //fixme Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
|
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||||
currentToast = toast
|
toast.view = layout
|
||||||
|
//https://github.com/PureWriter/ToastCompat
|
||||||
toast.show()
|
toast.show()
|
||||||
|
currentToast = toast
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logError(e)
|
logError(e)
|
||||||
}
|
}
|
||||||
|
|
@ -202,25 +183,23 @@ object CommonActivity {
|
||||||
setLocale(this, localeCode)
|
setLocale(this, localeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(act: Activity) {
|
fun init(act: ComponentActivity?) {
|
||||||
setActivityInstance(act)
|
if (act == null) return
|
||||||
|
activity = act
|
||||||
val componentActivity = activity as? ComponentActivity ?: return
|
|
||||||
|
|
||||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||||
canShowPipMode =
|
canShowPipMode =
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
||||||
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
|
||||||
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
|
||||||
|
|
||||||
componentActivity.updateLocale()
|
act.updateLocale()
|
||||||
componentActivity.updateTv()
|
act.updateTv()
|
||||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||||
|
|
||||||
for (resumeApp in resumeApps) {
|
for (resumeApp in resumeApps) {
|
||||||
resumeApp.launcher =
|
resumeApp.launcher =
|
||||||
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
val resultCode = result.resultCode
|
val resultCode = result.resultCode
|
||||||
val data = result.data
|
val data = result.data
|
||||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||||
|
|
@ -237,11 +216,11 @@ object CommonActivity {
|
||||||
// Ask for notification permissions on Android 13
|
// Ask for notification permissions on Android 13
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||||
ContextCompat.checkSelfPermission(
|
ContextCompat.checkSelfPermission(
|
||||||
componentActivity,
|
act,
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
) {
|
) {
|
||||||
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
val requestPermissionLauncher = act.registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { isGranted: Boolean ->
|
) { isGranted: Boolean ->
|
||||||
Log.d(TAG, "Notification permission: $isGranted")
|
Log.d(TAG, "Notification permission: $isGranted")
|
||||||
|
|
@ -277,35 +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?) {
|
fun loadThemes(act: Activity?) {
|
||||||
if (act == null) return
|
if (act == null) return
|
||||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
|
||||||
|
|
||||||
val currentTheme =
|
val currentTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
||||||
"System" -> mapSystemTheme(act)
|
|
||||||
"Black" -> R.style.AppTheme
|
"Black" -> R.style.AppTheme
|
||||||
"Light" -> R.style.LightMode
|
"Light" -> R.style.LightMode
|
||||||
"Amoled" -> R.style.AmoledMode
|
"Amoled" -> R.style.AmoledMode
|
||||||
|
|
@ -319,15 +275,12 @@ object CommonActivity {
|
||||||
val currentOverlayTheme =
|
val currentOverlayTheme =
|
||||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||||
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
|
||||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||||
"Orange" -> R.style.OverlayPrimaryColorOrange
|
|
||||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||||
"White" -> R.style.OverlayPrimaryColorWhite
|
"White" -> R.style.OverlayPrimaryColorWhite
|
||||||
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
|
||||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||||
|
|
@ -336,7 +289,6 @@ object CommonActivity {
|
||||||
"Banana" -> R.style.OverlayPrimaryColorBanana
|
"Banana" -> R.style.OverlayPrimaryColorBanana
|
||||||
"Party" -> R.style.OverlayPrimaryColorParty
|
"Party" -> R.style.OverlayPrimaryColorParty
|
||||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||||
"Lavender" -> R.style.OverlayPrimaryColorLavender
|
|
||||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||||
|
|
||||||
|
|
@ -376,14 +328,6 @@ object CommonActivity {
|
||||||
currentLook = currentLook.parent as? View ?: 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 */
|
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
|
||||||
fun continueGetNextFocus(
|
fun continueGetNextFocus(
|
||||||
root: Any?,
|
root: Any?,
|
||||||
|
|
@ -404,17 +348,16 @@ object CommonActivity {
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
next = localLook(view, nextId) ?: next
|
next = localLook(view, nextId) ?: next
|
||||||
val shown = next.hasContent()
|
|
||||||
|
|
||||||
// if cant focus but visible then break and let android decide
|
// 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
|
// the exception if is the view is a parent and has children that wants focus
|
||||||
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
|
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
|
||||||
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
|
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
|
||||||
} ?: false
|
} ?: 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 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
|
// we don't want a while true loop, so we let android decide if we find a recursive view
|
||||||
if (next == view) return null
|
if (next == view) return null
|
||||||
return getNextFocus(root, next, direction, depth + 1)
|
return getNextFocus(root, next, direction, depth + 1)
|
||||||
|
|
@ -488,6 +431,20 @@ object CommonActivity {
|
||||||
|
|
||||||
|
|
||||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||||
|
//println("Keycode: $keyCode")
|
||||||
|
//showToast(
|
||||||
|
// this,
|
||||||
|
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||||
|
// Toast.LENGTH_LONG
|
||||||
|
//)
|
||||||
|
|
||||||
|
// Tested keycodes on remote:
|
||||||
|
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||||
|
// KeyEvent.KEYCODE_MEDIA_REWIND
|
||||||
|
// KeyEvent.KEYCODE_MENU
|
||||||
|
// KeyEvent.KEYCODE_MEDIA_NEXT
|
||||||
|
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||||
|
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||||
|
|
||||||
// 149 keycode_numpad 5
|
// 149 keycode_numpad 5
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
|
||||||
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||||
import org.schabi.newpipe.extractor.downloader.Request
|
import org.schabi.newpipe.extractor.downloader.Request
|
||||||
import org.schabi.newpipe.extractor.downloader.Response
|
import org.schabi.newpipe.extractor.downloader.Response
|
||||||
|
|
@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
|
||||||
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
|
||||||
private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
private val client: OkHttpClient
|
||||||
override fun execute(request: Request): Response {
|
override fun execute(request: Request): Response {
|
||||||
val httpMethod: String = request.httpMethod()
|
val httpMethod: String = request.httpMethod()
|
||||||
val url: String = request.url()
|
val url: String = request.url()
|
||||||
|
|
@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
val dataToSend: ByteArray? = request.dataToSend()
|
val dataToSend: ByteArray? = request.dataToSend()
|
||||||
var requestBody: RequestBody? = null
|
var requestBody: RequestBody? = null
|
||||||
if (dataToSend != null) {
|
if (dataToSend != null) {
|
||||||
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
|
requestBody = RequestBody.create(null, dataToSend)
|
||||||
}
|
}
|
||||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||||
.method(httpMethod, requestBody).url(url)
|
.method(httpMethod, requestBody).url(url)
|
||||||
|
|
@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.api.Log
|
import android.util.Log
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,18 +7,13 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||||
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
class Geodailymotion : Dailymotion() {
|
|
||||||
override val name = "GeoDailymotion"
|
|
||||||
override val mainUrl = "https://geo.dailymotion.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
open class Dailymotion : ExtractorApi() {
|
open class Dailymotion : ExtractorApi() {
|
||||||
override val mainUrl = "https://www.dailymotion.com"
|
override val mainUrl = "https://www.dailymotion.com"
|
||||||
override val name = "Dailymotion"
|
override val name = "Dailymotion"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
private val baseUrl = "https://www.dailymotion.com"
|
|
||||||
|
|
||||||
@Suppress("RegExpSimplifiable")
|
@Suppress("RegExpSimplifiable")
|
||||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||||
|
|
@ -32,16 +27,21 @@ open class Dailymotion : ExtractorApi() {
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val embedUrl = getEmbedUrl(url) ?: return
|
val embedUrl = getEmbedUrl(url) ?: return
|
||||||
val req = app.get(embedUrl)
|
val doc = app.get(embedUrl).document
|
||||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||||
val configStr = req.document.selectFirst("script:containsData($prefix)")?.data() ?: return
|
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix).substringBefore(";").trim()) ?: return
|
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
||||||
val id = getVideoId(embedUrl) ?: return
|
val id = getVideoId(embedUrl) ?: return
|
||||||
val dmV1st = config.dmInternalData.v1st
|
val dmV1st = config.dmInternalData.v1st
|
||||||
val dmTs = config.dmInternalData.ts
|
val dmTs = config.dmInternalData.ts
|
||||||
val embedder = config.context.embedder
|
val metaDataUrl =
|
||||||
val metaDataUrl = "$baseUrl/player/metadata/video/$id?embedder=$embedder&locale=en-US&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = req.cookies)
|
val cookies = mapOf(
|
||||||
|
"v1st" to dmV1st,
|
||||||
|
"dmvk" to config.context.dmvk,
|
||||||
|
"ts" to dmTs.toString()
|
||||||
|
)
|
||||||
|
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
||||||
.parsedSafe<MetaData>() ?: return
|
.parsedSafe<MetaData>() ?: return
|
||||||
metaData.qualities.forEach { (_, video) ->
|
metaData.qualities.forEach { (_, video) ->
|
||||||
video.forEach {
|
video.forEach {
|
||||||
|
|
@ -51,19 +51,16 @@ open class Dailymotion : ExtractorApi() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEmbedUrl(url: String): String? {
|
private fun getEmbedUrl(url: String): String? {
|
||||||
if (url.contains("/embed/") || url.contains("/video/")) {
|
if (url.contains("/embed/")) {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
if (url.contains("geo.dailymotion.com")) {
|
val vid = getVideoId(url) ?: return null
|
||||||
val videoId = url.substringAfter("video=")
|
return "$mainUrl/embed/video/$vid"
|
||||||
return "$baseUrl/embed/video/$videoId"
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getVideoId(url: String): String? {
|
private fun getVideoId(url: String): String? {
|
||||||
val path = URL(url).path
|
val path = URL(url).path
|
||||||
val id = path.substringAfter("/video/")
|
val id = path.substringAfter("video/")
|
||||||
if (id.matches(videoIdRegex)) {
|
if (id.matches(videoIdRegex)) {
|
||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
@ -87,13 +84,13 @@ open class Dailymotion : ExtractorApi() {
|
||||||
)
|
)
|
||||||
|
|
||||||
data class InternalData(
|
data class InternalData(
|
||||||
val ts: Long,
|
val ts: Int,
|
||||||
val v1st: String
|
val v1st: String
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Context(
|
data class Context(
|
||||||
@JsonProperty("access_token") val accessToken: String?,
|
@JsonProperty("access_token") val accessToken: String?,
|
||||||
val embedder: String?,
|
val dmvk: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class MetaData(
|
data class MetaData(
|
||||||
|
|
@ -7,18 +7,6 @@ import com.lagradost.cloudstream3.utils.Qualities
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
class D0000d : DoodLaExtractor() {
|
|
||||||
override var mainUrl = "https://d0000d.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class D000dCom : DoodLaExtractor() {
|
|
||||||
override var mainUrl = "https://d000d.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class DoodstreamCom : DoodLaExtractor() {
|
|
||||||
override var mainUrl = "https://doodstream.com"
|
|
||||||
}
|
|
||||||
|
|
||||||
class Dooood : DoodLaExtractor() {
|
class Dooood : DoodLaExtractor() {
|
||||||
override var mainUrl = "https://dooood.com"
|
override var mainUrl = "https://dooood.com"
|
||||||
}
|
}
|
||||||
|
|
@ -68,10 +56,9 @@ open class DoodLaExtractor : ExtractorApi() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val newUrl= url.replace(mainUrl, "https://d0000d.com")
|
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
|
||||||
val response0 = app.get(newUrl).text // html of DoodStream page to look for /pass_md5/...
|
val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
|
||||||
val md5 ="https://d0000d.com"+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
|
val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
|
||||||
val trueUrl = app.get(md5, referer = newUrl).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
|
|
||||||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||||
return listOf(
|
return listOf(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
|
|
@ -2,10 +2,14 @@ package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
import org.jsoup.nodes.Element
|
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() {
|
class DatabaseGdrive2 : Gdriveplayer() {
|
||||||
override var mainUrl = "https://databasegdriveplayer.co"
|
override var mainUrl = "https://databasegdriveplayer.co"
|
||||||
|
|
@ -61,6 +65,78 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
?.data()?.let { getAndUnpack(it) }
|
?.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? {
|
private fun Regex.first(str: String): String? {
|
||||||
return find(str)?.groupValues?.getOrNull(1)
|
return find(str)?.groupValues?.getOrNull(1)
|
||||||
}
|
}
|
||||||
|
|
@ -78,14 +154,14 @@ open class Gdriveplayer : ExtractorApi() {
|
||||||
val document = app.get(url).document
|
val document = app.get(url).document
|
||||||
|
|
||||||
val eval = unpackJs(document)?.replace("\\", "") ?: return
|
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)
|
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||||
?.split(Regex("\\D+"))
|
?.split(Regex("\\D+"))
|
||||||
?.joinToString("") {
|
?.joinToString("") {
|
||||||
Char(it.toInt()).toString()
|
Char(it.toInt()).toString()
|
||||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||||
?: throw ErrorLoadingException("can't find password")
|
?: 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 sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
|
||||||
val subData = decryptedData?.substringAfter("tracks:[")?.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(
|
data class Tracks(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
@JsonProperty("kind") val kind: String,
|
@JsonProperty("kind") val kind: String,
|
||||||
|
|
@ -19,12 +19,12 @@ open class Gofile : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> 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 token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
||||||
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
||||||
Regex("fetchData.wt\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
||||||
}
|
}
|
||||||
app.get("$mainApi/getContent?contentId=$id&token=$token&wt=$websiteToken")
|
app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken")
|
||||||
.parsedSafe<Source>()?.data?.contents?.forEach {
|
.parsedSafe<Source>()?.data?.contents?.forEach {
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
|
|
@ -18,8 +18,7 @@ open class Linkbox : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val token = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
||||||
val id = app.get("$mainUrl/api/file/share_out_list/?sortField=utime&sortAsc=0&pageNo=1&pageSize=50&shareToken=$token").parsedSafe<Responses>()?.data?.itemId
|
|
||||||
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
|
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
|
||||||
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
||||||
callback.invoke(
|
callback.invoke(
|
||||||
|
|
@ -45,7 +44,6 @@ open class Linkbox : ExtractorApi() {
|
||||||
|
|
||||||
data class Data(
|
data class Data(
|
||||||
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
||||||
@JsonProperty("itemId") val itemId: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data class Responses(
|
data class Responses(
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
|
@ -67,7 +66,7 @@ open class Pelisplus(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
type = INFER_TYPE
|
element.attr("href").contains(".m3u8")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
import com.lagradost.api.Log
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.*
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
|
@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.ErrorLoadingException
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.base64DecodeArray
|
import com.lagradost.cloudstream3.base64DecodeArray
|
||||||
import com.lagradost.cloudstream3.base64Encode
|
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils
|
import com.lagradost.cloudstream3.utils.AppUtils
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
|
|
@ -17,52 +16,13 @@ import javax.crypto.Cipher
|
||||||
import javax.crypto.spec.IvParameterSpec
|
import javax.crypto.spec.IvParameterSpec
|
||||||
import javax.crypto.spec.SecretKeySpec
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
// No License found in https://github.com/enimax-anime/key
|
||||||
|
// special credits to @enimax for providing key
|
||||||
class Megacloud : Rabbitstream() {
|
class Megacloud : Rabbitstream() {
|
||||||
override val name = "Megacloud"
|
override val name = "Megacloud"
|
||||||
override val mainUrl = "https://megacloud.tv"
|
override val mainUrl = "https://megacloud.tv"
|
||||||
override val embed = "embed-2/ajax/e-1"
|
override val embed = "embed-2/ajax/e-1"
|
||||||
private val scriptUrl = "$mainUrl/js/player/a/prod/e1-player.min.js"
|
override val key = "https://raw.githubusercontent.com/enimax-anime/key/e6/key.txt"
|
||||||
|
|
||||||
override suspend fun extractRealKey(sources: String): Pair<String, String> {
|
|
||||||
val rawKeys = getKeys()
|
|
||||||
val sourcesArray = sources.toCharArray()
|
|
||||||
|
|
||||||
var extractedKey = ""
|
|
||||||
var currentIndex = 0
|
|
||||||
for (index in rawKeys) {
|
|
||||||
val start = index[0] + currentIndex
|
|
||||||
val end = start + index[1]
|
|
||||||
for (i in start until end) {
|
|
||||||
extractedKey += sourcesArray[i].toString()
|
|
||||||
sourcesArray[i] = ' '
|
|
||||||
}
|
|
||||||
currentIndex += index[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return extractedKey to sourcesArray.joinToString("").replace(" ", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun getKeys(): List<List<Int>> {
|
|
||||||
val script = app.get(scriptUrl).text
|
|
||||||
fun matchingKey(value: String): String {
|
|
||||||
return Regex(",$value=((?:0x)?([0-9a-fA-F]+))").find(script)?.groupValues?.get(1)
|
|
||||||
?.removePrefix("0x") ?: throw ErrorLoadingException("Failed to match the key")
|
|
||||||
}
|
|
||||||
|
|
||||||
val regex = Regex("case\\s*0x[0-9a-f]+:(?![^;]*=partKey)\\s*\\w+\\s*=\\s*(\\w+)\\s*,\\s*\\w+\\s*=\\s*(\\w+);")
|
|
||||||
val indexPairs = regex.findAll(script).toList().map { match ->
|
|
||||||
val matchKey1 = matchingKey(match.groupValues[1])
|
|
||||||
val matchKey2 = matchingKey(match.groupValues[2])
|
|
||||||
try {
|
|
||||||
listOf(matchKey1.toInt(16), matchKey2.toInt(16))
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}.filter { it.isNotEmpty() }
|
|
||||||
|
|
||||||
return indexPairs
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Dokicloud : Rabbitstream() {
|
class Dokicloud : Rabbitstream() {
|
||||||
|
|
@ -70,14 +30,13 @@ class Dokicloud : Rabbitstream() {
|
||||||
override val mainUrl = "https://dokicloud.one"
|
override val mainUrl = "https://dokicloud.one"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code found in https://github.com/eatmynerds/key
|
|
||||||
// special credits to @eatmynerds for providing key
|
|
||||||
open class Rabbitstream : ExtractorApi() {
|
open class Rabbitstream : ExtractorApi() {
|
||||||
override val name = "Rabbitstream"
|
override val name = "Rabbitstream"
|
||||||
override val mainUrl = "https://rabbitstream.net"
|
override val mainUrl = "https://rabbitstream.net"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
open val embed = "ajax/embed-4"
|
open val embed = "ajax/embed-4"
|
||||||
open val key = "https://raw.githubusercontent.com/eatmynerds/key/e4/key.txt"
|
open val key = "https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"
|
||||||
|
private var rawKey: String? = null
|
||||||
|
|
||||||
override suspend fun getUrl(
|
override suspend fun getUrl(
|
||||||
url: String,
|
url: String,
|
||||||
|
|
@ -98,7 +57,7 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
val decryptedSources = if (sources == null || encryptedMap.encrypted == false) {
|
||||||
response.parsedSafe()
|
response.parsedSafe()
|
||||||
} else {
|
} else {
|
||||||
val (key, encData) = extractRealKey(sources)
|
val (key, encData) = extractRealKey(sources, getRawKey())
|
||||||
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
val decrypted = decryptMapped<List<Sources>>(encData, key)
|
||||||
SourcesResponses(
|
SourcesResponses(
|
||||||
sources = decrypted,
|
sources = decrypted,
|
||||||
|
|
@ -117,19 +76,31 @@ open class Rabbitstream : ExtractorApi() {
|
||||||
decryptedSources?.tracks?.map { track ->
|
decryptedSources?.tracks?.map { track ->
|
||||||
subtitleCallback.invoke(
|
subtitleCallback.invoke(
|
||||||
SubtitleFile(
|
SubtitleFile(
|
||||||
track?.label ?: return@map,
|
track?.label ?: "",
|
||||||
track.file ?: return@map
|
track?.file ?: return@map
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open suspend fun extractRealKey(sources: String): Pair<String, String> {
|
private suspend fun getRawKey(): String = rawKey ?: app.get(key).text.also { rawKey = it }
|
||||||
val rawKeys = parseJson<List<Int>>(app.get(key).text)
|
|
||||||
val extractedKey = base64Encode(rawKeys.map { it.toByte() }.toByteArray())
|
private fun extractRealKey(originalString: String?, stops: String): Pair<String, String> {
|
||||||
return extractedKey to sources
|
val table = parseJson<List<List<Int>>>(stops)
|
||||||
|
val decryptedKey = StringBuilder()
|
||||||
|
var offset = 0
|
||||||
|
var encryptedString = originalString
|
||||||
|
|
||||||
|
table.forEach { (start, end) ->
|
||||||
|
decryptedKey.append(encryptedString?.substring(start - offset, end - offset))
|
||||||
|
encryptedString = encryptedString?.substring(
|
||||||
|
0,
|
||||||
|
start - offset
|
||||||
|
) + encryptedString?.substring(end - offset)
|
||||||
|
offset += end - start
|
||||||
|
}
|
||||||
|
return decryptedKey.toString() to encryptedString.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
private inline fun <reified T> decryptMapped(input: String, key: String): T? {
|
||||||
|
|
@ -7,12 +7,14 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
|
||||||
open class Minoplres : ExtractorApi() {
|
class 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 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> {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||||
val sources = mutableListOf<ExtractorLink>()
|
val sources = mutableListOf<ExtractorLink>()
|
||||||
|
|
@ -24,7 +26,7 @@ open class Minoplres : ExtractorApi() {
|
||||||
M3u8Helper.generateM3u8(
|
M3u8Helper.generateM3u8(
|
||||||
name,
|
name,
|
||||||
it.file,
|
it.file,
|
||||||
"$hostUrl/",
|
"$mainUrl/",
|
||||||
).forEach { m3uData -> sources.add(m3uData) }
|
).forEach { m3uData -> sources.add(m3uData) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,4 +37,6 @@ open class Minoplres : ExtractorApi() {
|
||||||
private data class File(
|
private data class File(
|
||||||
@JsonProperty("file") val file: String,
|
@JsonProperty("file") val file: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -9,10 +9,6 @@ class StreamTapeNet : StreamTape() {
|
||||||
override var mainUrl = "https://streamtape.net"
|
override var mainUrl = "https://streamtape.net"
|
||||||
}
|
}
|
||||||
|
|
||||||
class StreamTapeXyz : StreamTape() {
|
|
||||||
override var mainUrl = "https://streamtape.xyz"
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShaveTape : StreamTape(){
|
class ShaveTape : StreamTape(){
|
||||||
override var mainUrl = "https://shavetape.cash"
|
override var mainUrl = "https://shavetape.cash"
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ data class Files(
|
||||||
|
|
||||||
open class Supervideo : ExtractorApi() {
|
open class Supervideo : ExtractorApi() {
|
||||||
override var name = "Supervideo"
|
override var name = "Supervideo"
|
||||||
override var mainUrl = "https://supervideo.cc"
|
override var mainUrl = "https://supervideo.tv"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -25,13 +25,9 @@ open class Vidmoly : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
val headers = mapOf(
|
|
||||||
"User-Agent" to "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36",
|
|
||||||
"Sec-Fetch-Dest" to "iframe"
|
|
||||||
)
|
|
||||||
val script = app.get(
|
val script = app.get(
|
||||||
url,
|
url,
|
||||||
headers = headers,
|
|
||||||
referer = referer,
|
referer = referer,
|
||||||
).document.select("script")
|
).document.select("script")
|
||||||
.find { it.data().contains("sources:") }?.data()
|
.find { it.data().contains("sources:") }?.data()
|
||||||
|
|
@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.argamap
|
import com.lagradost.cloudstream3.argamap
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||||
import com.lagradost.cloudstream3.utils.loadExtractor
|
import com.lagradost.cloudstream3.utils.loadExtractor
|
||||||
|
|
@ -71,7 +70,7 @@ class Vidstream(val mainUrl: String) {
|
||||||
href,
|
href,
|
||||||
page.url,
|
page.url,
|
||||||
getQualityFromName(qual),
|
getQualityFromName(qual),
|
||||||
type = INFER_TYPE
|
element.attr("href").contains(".m3u8")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.extractors.helper.NineAnimeHelper.encrypt
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
|
||||||
class Vidstreamz : WcoStream() {
|
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")
|
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 {
|
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"))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4,8 +4,8 @@ import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.INFER_TYPE
|
|
||||||
import com.lagradost.cloudstream3.utils.Qualities
|
import com.lagradost.cloudstream3.utils.Qualities
|
||||||
|
import java.net.URI
|
||||||
|
|
||||||
open class Wibufile : ExtractorApi() {
|
open class Wibufile : ExtractorApi() {
|
||||||
override val name: String = "Wibufile"
|
override val name: String = "Wibufile"
|
||||||
|
|
@ -28,8 +28,10 @@ open class Wibufile : ExtractorApi() {
|
||||||
video ?: return,
|
video ?: return,
|
||||||
"$mainUrl/",
|
"$mainUrl/",
|
||||||
Qualities.Unknown.value,
|
Qualities.Unknown.value,
|
||||||
type = INFER_TYPE
|
URI(url).path.endsWith(".m3u8")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -70,18 +70,19 @@ open class YoutubeExtractor : ExtractorApi() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ytVideos[url]?.mapNotNull {
|
ytVideos[url]?.mapNotNull {
|
||||||
if (it.isVideoOnly() || it.height <= 0) return@mapNotNull null
|
if (it.isVideoOnly || it.height <= 0) return@mapNotNull null
|
||||||
|
|
||||||
ExtractorLink(
|
ExtractorLink(
|
||||||
this.name,
|
this.name,
|
||||||
this.name,
|
this.name,
|
||||||
it.content ?: return@mapNotNull null,
|
it.url ?: return@mapNotNull null,
|
||||||
"",
|
"",
|
||||||
it.height
|
it.height
|
||||||
)
|
)
|
||||||
}?.forEach(callback)
|
}?.forEach(callback)
|
||||||
ytVideosSubtitles[url]?.mapNotNull {
|
ytVideosSubtitles[url]?.mapNotNull {
|
||||||
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.content ?: return@mapNotNull null)
|
SubtitleFile(it.languageTag ?: return@mapNotNull null, it.url ?: return@mapNotNull null)
|
||||||
}?.forEach(subtitleCallback)
|
}?.forEach(subtitleCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.lagradost.cloudstream3.extractors.helper
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
import com.lagradost.api.Log
|
import android.util.Log
|
||||||
import com.lagradost.cloudstream3.SubtitleFile
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
import com.lagradost.cloudstream3.amap
|
import com.lagradost.cloudstream3.amap
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.lagradost.cloudstream3.extractors.helper
|
package com.lagradost.cloudstream3.extractors.helper
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||||
import com.lagradost.cloudstream3.app
|
import com.lagradost.cloudstream3.app
|
||||||
|
|
||||||
class WcoHelper {
|
class WcoHelper {
|
||||||
|
|
@ -28,7 +30,9 @@ class WcoHelper {
|
||||||
private suspend fun getKeys() {
|
private suspend fun getKeys() {
|
||||||
keys = keys
|
keys = keys
|
||||||
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
|
?: app.get("https://raw.githubusercontent.com/reduplicated/Cloudstream/master/docs/keys.json")
|
||||||
.parsedSafe<ExternalKeys>()
|
.parsedSafe<ExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
|
||||||
|
BACKUP_KEY_DATA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getWcoKey(): ExternalKeys? {
|
suspend fun getWcoKey(): ExternalKeys? {
|
||||||
|
|
@ -39,7 +43,9 @@ class WcoHelper {
|
||||||
private suspend fun getNewKeys() {
|
private suspend fun getNewKeys() {
|
||||||
newKeys = newKeys
|
newKeys = newKeys
|
||||||
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
|
?: app.get("https://raw.githubusercontent.com/chekaslowakiya/BruhFlow/main/keys.json")
|
||||||
.parsedSafe<NewExternalKeys>()
|
.parsedSafe<NewExternalKeys>()?.also { setKey(BACKUP_KEY_DATA, it) } ?: getKey(
|
||||||
|
BACKUP_KEY_DATA
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getNewWcoKey(): NewExternalKeys? {
|
suspend fun getNewWcoKey(): NewExternalKeys? {
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,13 +2,15 @@ package com.lagradost.cloudstream3.metaproviders
|
||||||
|
|
||||||
import com.lagradost.cloudstream3.MainAPI
|
import com.lagradost.cloudstream3.MainAPI
|
||||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||||
|
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
|
||||||
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
import com.lagradost.cloudstream3.syncproviders.SyncIdName
|
||||||
|
|
||||||
object SyncRedirector {
|
object SyncRedirector {
|
||||||
|
val syncApis = SyncApis
|
||||||
private val syncIds =
|
private val syncIds =
|
||||||
listOf(
|
listOf(
|
||||||
SyncIdName.MyAnimeList to Regex("""myanimelist\.net/anime/(\d+)"""),
|
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
|
||||||
SyncIdName.Anilist to Regex("""anilist\.co/anime/(\d+)""")
|
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun redirect(
|
suspend fun redirect(
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue