Compare commits

...

77 commits

Author SHA1 Message Date
firelight
748d425d63
Fixed subdl, closes #2937 (#2938) 2026-06-20 16:29:00 +02:00
Luna712
0894e5041b
AuthAPI: use unixTime from APIHolder (#2926) 2026-06-20 16:02:06 +02:00
KingLucius
84ae5fc024
Move MAL & Anilist Keys to BuildConfig (#2933) 2026-06-20 15:56:06 +02:00
Luna712
14f2a29dcc
UnshortenUrl: replace ByteArray.toString (#2928) 2026-06-20 15:51:39 +02:00
Luna712
f2d7483136
Subdl: migrate to kotlinx serialization (#2890)
* Subdl: migrate to kotlinx serialization

* Keep JsonProperty where different than variable name

* Keep all JsonProperty for now
2026-06-20 11:31:36 +00:00
Luna712
fd934a8b7f
[skip ci] Add tests for StringUtils (#2934) 2026-06-20 11:19:16 +00:00
KingLucius
3c6bf2984e
SyncApi Search query fix (#2932) 2026-06-19 00:06:46 +02:00
Luna712
6f458fc9b5
Remove unused classgraph dependency (#2924) 2026-06-17 23:41:02 +00:00
Bnyro
b4100dbfca
feat(extractors): add flyfile.app extractor (#2925) 2026-06-17 23:40:30 +00:00
Luna712
943bc551e9
[skip ci] HlsPlaylistParser: use base64DecodeArray (#2929) 2026-06-17 23:34:25 +00:00
Luna712
c045bfdc0d
[skip ci] MainAPI: remove @OptIn(ExperimentalEncodingApi::class) (#2930)
It is stable since Kotlin 2.2.0
2026-06-17 23:03:28 +00:00
firelight
2c03a3d976
fix gradient (#2912) 2026-06-13 02:59:02 +02:00
firelight
3417fe0160
Feat: OnlyPlayer (#2905) 2026-06-13 02:58:43 +02:00
firelight
55450a02fa
Merge pull request #2904 from recloudstream/mpvrx
Feat: MpvRx
2026-06-13 02:56:45 +02:00
Osten
6f9646e52f
Fix one last issue in JSON parsing :I 2026-06-11 15:06:23 +02:00
Luna712
b222911e29
Fix parseJson inline on stable once again! (#2908) 2026-06-11 12:29:29 +02:00
firelight
5667f52648
Translated using Weblate (German) (#2876)
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (729 of 729 strings)





Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translation: Cloudstream/App

Co-authored-by: Deleted User <pandoroo@users.noreply.hosted.weblate.org>
Co-authored-by: WertZuz <97708601+wertzuz@users.noreply.github.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
2026-06-11 02:28:26 +02:00
Hosted Weblate
b2a02a174f
Merge remote-tracking branch 'origin/master' 2026-06-11 02:26:56 +02:00
Luna712
18a857723b
Replace java.net.uri in library (#2839) 2026-06-11 00:26:49 +00:00
Hosted Weblate
292d3f1442
Merge remote-tracking branch 'origin/master' 2026-06-11 02:24:56 +02:00
Luna712
8012c58069
Set explicitNulls = false for kotlinx serialization (#2897)
This is more inline with Jackson's behavior otherwise for nullable types with non default values, and they don't exist, gives a Missingfield exception whereas it worked on Jackson. We may need coerceInputValues = true also but I am unsure of that right now.
2026-06-11 00:24:50 +00:00
Hosted Weblate
4f8a79669c
Merge remote-tracking branch 'origin/master' 2026-06-11 02:09:06 +02:00
Luna712
2181243dd1
Bump NewPipeExtractor to fix trailers and other YouTube videos (#2906) 2026-06-11 00:08:58 +00:00
Hosted Weblate
eae18bb50d
Merge remote-tracking branch 'origin/master' 2026-06-09 21:03:54 +00:00
Luna712
f7cbf25b30
Replace EnumSet for dubStatus (#2845) 2026-06-09 23:03:48 +02:00
Hosted Weblate
fd579fcc18
Translated using Weblate (German)
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (German)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: Deleted User <pandoroo@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: WertZuz <97708601+wertzuz@users.noreply.github.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translation: Cloudstream/App
2026-06-08 22:22:51 +02:00
Luna712
79cc3fb501
Fix kotlinx serialization in parseJson (#2902)
It doing runCatching on the Result itself, that results in the serializer always returning null. Just do what we do everywhere else here also, getContextual doesn't throw anything.
2026-06-08 20:22:34 +00:00
KingLucius
d78b991d66
Trakt meta provider logo support (#2894) 2026-06-07 23:59:42 +02:00
firelight
70053ebbae
[skip ci] Fix serialization testing (#2896) 2026-06-06 19:48:37 +00:00
Luna712
a4a4c31f8d
Replace toByteArray() in some places in library (#2866) 2026-06-03 16:09:30 +02:00
Osten
3844c896f1
Fixed Json again 2026-06-02 14:14:38 +02:00
Osten
11f77fbe11
Fixed parseJson inline problem 2026-06-01 18:11:54 +02:00
Luna712
62662cb064
Bump a few libs (#2840) 2026-06-01 01:24:31 +02:00
Luna712
8e0c664b1e
Add TRAKT_CLIENT_ID to Archive build as well (#2861) 2026-06-01 01:10:03 +02:00
fgmitesh
4836e2b371
fix: treat KEYCODE_ENTER as select for TV remotes (LG Magic Remote) (#2853)
* Improve key event handling in FullScreenPlayer

Refactor key event handling for play/pause and chapter skipping.

* Handle KEYCODE_ENTER for SearchView focus
2026-06-01 01:07:22 +02:00
firelight
0e16f429af
Update TRAKT_CLIENT_ID (#2860) 2026-06-01 01:04:36 +02:00
KingLucius
5de7f207f2
feat(MetaProviders): Fix Trakt (#2858) 2026-06-01 00:53:47 +02:00
Luna712
e1aacce93d
ExtractorAPI: support Kotlin Uuid (#2855) 2026-06-01 00:53:37 +02:00
Luna712
8e1b41ea61
Fix one instance of String(byteArray) I previously missed (#2859) 2026-06-01 00:49:23 +02:00
Luna712
b7f5826a19
Add expect/actual for YoutubeExtractor (#2844)
NewPipeExtractor won't work in non-JVM platforms.
2026-05-31 01:48:06 +00:00
Luna712
8e7569df53
Fix T::class in parseJson causing type erasure in some cases (#2852) 2026-05-31 01:47:46 +00:00
Luna712
0728dd06a1
[skip ci] Replace using Char constructor in a couple extractors (#2856) 2026-05-31 01:09:43 +00:00
Luna712
041d21a486
Emergency patch (#2851) 2026-05-29 19:30:55 +02:00
Luna712
a124450ddc
Migrate Java date utils in library to kotlinx-datetime (#2798) 2026-05-29 10:58:08 +00:00
Luna712
028a794ea5
Add support for using kotlinx-serialization rather than Jackson (#2791) 2026-05-28 21:16:31 +00:00
PiterDev
c1b6fc2eeb
Fix Updated/ReleaseDate/Rating sorting for Kitsu SyncProvider (#2780) 2026-05-28 10:46:49 +00:00
Luna712
647c274944
[skip ci] Move versionCode and versionName to version catalog 2026-05-27 21:05:18 +00:00
firelight
22be73619e
Translated using Weblate (Norwegian Nynorsk) (#2784)
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar_SA/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/pt/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane

Co-authored-by: Andrei Stepanov <adem4ik@gmail.com>
Co-authored-by: Dark <darkbeamer.official@gmail.com>
Co-authored-by: Douglas de Santana Ramos <ramos.ti@live.com>
Co-authored-by: Gabriel <bloxgabriel18@gmail.com>
Co-authored-by: Johannes Bø <johannes.bo@gmail.com>
Co-authored-by: Maarten De Jong <maarten.de.jong2003@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Saleh ALHarbi <alfraidigamerofficial@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
2026-05-27 22:57:43 +02:00
Hosted Weblate
a3c100e75b
Merge remote-tracking branch 'origin/master' 2026-05-27 20:57:16 +00:00
Luna712
d24f8bca0f
Don't use any external library for Levenshtein ratio matching (#2802) 2026-05-27 20:57:09 +00:00
Hosted Weblate
4c3c463a19
Merge remote-tracking branch 'origin/master' 2026-05-26 19:22:13 +02:00
Luna712
007c0ff9bc
Fix some bugs in DownloadedPlayerActivity (#2758)
* Fix some bugs in DownloadedPlayerActivity

* Remove savedInstanceState check and use better long comment format
2026-05-26 19:22:06 +02:00
Hosted Weblate
c8bc999d22
Translated using Weblate (Norwegian Nynorsk)
Currently translated at 26.4% (193 of 729 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (480 of 729 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 100.0% (729 of 729 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Dutch)

Currently translated at 90.8% (662 of 729 strings)

Translated using Weblate (Arabic (Saudi Arabia))

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: Andrei Stepanov <adem4ik@gmail.com>
Co-authored-by: Dark <darkbeamer.official@gmail.com>
Co-authored-by: Douglas de Santana Ramos <ramos.ti@live.com>
Co-authored-by: Gabriel <bloxgabriel18@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Johannes Bø <johannes.bo@gmail.com>
Co-authored-by: Maarten De Jong <maarten.de.jong2003@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Saleh ALHarbi <alfraidigamerofficial@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar_SA/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/pt/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-05-26 17:00:51 +00:00
Luna712
b353cf2017
Remove back-compat constructor for AnimeSearchResponse (#2815)
This is in preparation to remove the use of EnumSet for `dubStatus`. Those that use the builder, use `addDubStatus` which means we can easily do this without breaking bytecode compatibility, but first we need to remove the back-compat constructor, as once we do that it wouldn't work anymore anyway.
2026-05-26 19:00:36 +02:00
IndusAryan
70ed1c753d
(fix): implement hardware check for image loading (#2765) 2026-05-24 22:59:43 +00:00
Luna712
00e943ebc4
Replace synchronized usage in library with kotlinx-atomicfu (#2808) 2026-05-24 22:31:27 +00:00
Luna712
0afb23eb2e
Bump material to 1.14.0 stable (#2752) 2026-05-24 22:16:53 +00:00
Luna712
0b642bb47f
[skip ci] Use StringUtils.decodeUri in a couple places (#2831)
Gives us just one place to update the API when we get there
2026-05-23 22:44:31 +00:00
Luna712
c6c70d5751
[skip ci] Remove an unused import (#2830) 2026-05-23 22:43:32 +00:00
Luna712
c1b49d0dcb
[skip ci] Replace charset string conversions with Kotlin native equivalents (#2807) 2026-05-23 20:34:13 +00:00
Luna712
85cc10c2e0
[skip ci] Replace usage of String.format in library (#2819) 2026-05-23 20:20:50 +00:00
Luna712
dd016341c0
[skip ci] Replace ArrayList in extractor (#2826) 2026-05-23 20:11:46 +00:00
Luna712
ac0a0d2941
[skip ci] Replace Integer.parseInt with Kotlin-native equivalent (#2827) 2026-05-23 20:10:21 +00:00
Osten
4ab97e4605
Delete issue workflow to prevent security issues 2026-05-23 18:02:53 +02:00
Luna712
f894b8f7ec
SubtitleHelper: replace usage of java.lang.Character (#2817) 2026-05-22 22:53:46 +00:00
Luna712
72386cb98c
[skip ci] HlsPlaylistParser: don't use java.lang.StringBuilder directly (#2811)
Just using StringBuilder will allow it to use kotlin.text.StringBuilder from Kotlin instead, which it already does in some places, making using java.lang.StringBuilder in here very inconsistent with other parts of the same class.
2026-05-21 22:01:46 +00:00
Luna712
419b902ead
[skip ci] Use this::class rather than javaClass in MainAPI (#2809) 2026-05-21 21:45:04 +00:00
Luna712
638d749945
[skip ci] Remove usage of junit within the app itself (#2820)
When I was testing compose, I realized that this causes issues where junit was wiring up instrumentation within the app, which overrode and conflicted with compose resource context. We don't need it as a dependency for just TestingUtils, so this refsctors it to just use AssertionError directly.
2026-05-21 21:29:33 +00:00
Luna712
0f41ca2641
[skip ci] Don't pass locale to titlecase in String.capitalize (#2810)
That is the default anyway.
2026-05-21 21:17:03 +00:00
Luna712
a6000fbe04
[skip ci] AppDebug: use kotlin.concurrent.Volatile (#2818)
By default this uses kotlin.jvm.Volatile, which we should be using kotlin.concurrent.Volatile instead.
2026-05-21 21:09:13 +00:00
Abodabodd
862e2590d2
Update StreamWishExtractor.kt (#2770) 2026-05-19 16:45:58 +00:00
fire-light43
9bc5027ea7
shared buffer to decrease alloc (#2787) 2026-05-19 01:19:16 +02:00
Luna712
7e406cb5eb
CryptoJS: replace array copies with Kotlin stdlib equivalents (#2799)
* Remove use of java.util.Arrays

* Remove unused import

* Replace more
2026-05-19 01:13:57 +02:00
Luna712
a24dc2600e
JsHunter/JsUnpacker: use Kotlin-native regex (#2803) 2026-05-19 01:06:07 +02:00
Luna712
89cc63673b
Remove okhttp3.HttpUrl version of loadImage (#2790)
It is currently unused and at some point we will want to move coil to use ktor and fully phase out the dependency on okhttp so we don't an unnecessary extra dependency on it.
2026-05-19 01:00:10 +02:00
Luna712
ab85737637
[skip ci] TraktProvider: use text rather than toString for app.get (#2804)
toString is just an alias to text at the moment, but isn't really clear, and isn't really what it is meant for.
2026-05-18 22:35:44 +00:00
Alvin
9a53e267ac
fix: only return subtitle if not null (#2794) 2026-05-18 01:29:59 +02:00
124 changed files with 3221 additions and 1295 deletions

View file

@ -71,7 +71,10 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
MAL_KEY: ${{ secrets.MAL_KEY }}
ANILIST_KEY: ${{ secrets.ANILIST_KEY }}
- uses: actions/checkout@v6
with:

View file

@ -1,98 +0,0 @@
name: Issue automatic actions
on:
issues:
types: [opened]
permissions:
contents: read
issues: write
jobs:
issue-moderator:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v2
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
- name: Similarity analysis
id: similarity
uses: actions-cool/issues-similarity-analysis@v1
with:
token: ${{ steps.generate_token.outputs.token }}
filter-threshold: 0.60
title-excludes: ''
comment-title: |
### Your issue looks similar to these issues:
Please close if duplicate.
comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true'
uses: actions/github-script@v9
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible duplicate"]
})
- uses: actions/checkout@v6
- name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2
with:
github-token: ${{ steps.generate_token.outputs.token }}
issue-close-message: |
@${issue.user.login}: hello! :wave:
This issue is being automatically closed because it does not follow the issue template."
closed-issues-label: "invalid"
- name: Check if issue mentions a provider
id: provider_check
env:
GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
run: |
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
pip3 install httpx
RES="$(python3 ./check_issue.py)"
echo "name=${RES}" >> $GITHUB_OUTPUT
- name: Comment if issue mentions a provider
if: steps.provider_check.outputs.name != 'none'
uses: actions-cool/issues-helper@v3
with:
actions: 'create-comment'
token: ${{ steps.generate_token.outputs.token }}
body: |
Hello ${{ github.event.issue.user.login }}.
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v9
with:
github-token: ${{ steps.generate_token.outputs.token }}
script: |
github.rest.issues.addLabels({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ["possible provider issue"]
})
- name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0
with:
type: 'issue'
token: ${{ steps.generate_token.outputs.token }}
emoji: 'eyes'

View file

@ -62,7 +62,10 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
MAL_KEY: ${{ secrets.MAL_KEY }}
ANILIST_KEY: ${{ secrets.ANILIST_KEY }}
- name: Create pre-release
uses: marvinpinto/action-automatic-releases@latest

View file

@ -27,7 +27,7 @@ jobs:
cache-read-only: false
- name: Run Gradle
run: ./gradlew assemblePrereleaseDebug lint
run: ./gradlew assemblePrereleaseDebug lint check
- name: Upload Artifact
uses: actions/upload-artifact@v7

View file

@ -8,6 +8,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.serialization)
}
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
@ -103,8 +104,8 @@ android {
applicationId = "com.lagradost.cloudstream3"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 68
versionName = "4.7.0"
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
@ -126,6 +127,16 @@ android {
"SIMKL_CLIENT_SECRET",
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
)
buildConfigField(
"String",
"MAL_KEY",
"\"" + (System.getenv("MAL_KEY") ?: localProperties["mal.key"]) + "\""
)
buildConfigField(
"String",
"ANILIST_KEY",
"\"" + (System.getenv("ANILIST_KEY") ?: localProperties["anilist.key"]) + "\""
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@ -206,9 +217,11 @@ dependencies {
testImplementation(libs.junit)
testImplementation(libs.json)
androidTestImplementation(libs.core)
implementation(libs.junit.ktx)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.instancio.core)
androidTestImplementation(libs.junit.ktx)
androidTestImplementation(libs.kotlin.test)
// Android Core & Lifecycle
implementation(libs.core.ktx)
@ -219,6 +232,7 @@ dependencies {
implementation(libs.bundles.lifecycle)
implementation(libs.bundles.navigation)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.serialization.json) // JSON Parser
// Design & UI
implementation(libs.preference.ktx)
@ -255,13 +269,15 @@ dependencies {
// Extensions & Other Libs
implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript
implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance
implementation(libs.safefile) // To Prevent the URI File Fu*kery
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9
implementation(libs.jackson.module.kotlin) // JSON Parser
implementation(libs.zipline)
// Deprecated; will be removed once extensions have time to migrate from using it
implementation("me.xdrop:fuzzywuzzy:1.4.0")
// Torrent Support
implementation(libs.torrentserver)
@ -310,6 +326,7 @@ tasks.withType<KotlinJvmCompile> {
optIn.addAll(
"com.lagradost.cloudstream3.InternalAPI",
"com.lagradost.cloudstream3.Prerelease",
"kotlin.uuid.ExperimentalUuidApi",
)
}
}

View file

@ -0,0 +1,134 @@
package com.lagradost.cloudstream3
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import dalvik.system.DexFile
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import kotlinx.serialization.serializerOrNull
import org.instancio.Instancio
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
@RunWith(AndroidJUnit4::class)
class SerializationClassTester {
// Same as app, or using app reference
val jacksonMapper = mapper
val kotlinxMapper = json
@Test
fun isIdenticalSerialization() {
val serializableClasses = findSerializableClasses("com.lagradost")
println("Number of serializable classes: ${serializableClasses.size}")
serializableClasses.forEach { kClass ->
val instance = Instancio.create(kClass.java)
val jacksonJson = jacksonMapper.writeValueAsString(instance)
val kotlinxJson = serializeWithKotlinx(kClass, instance)
assertEquals(
jacksonJson,
kotlinxJson,
"""
Serialization mismatch for:
${kClass.qualifiedName}
Jackson:
$jacksonJson
Kotlinx:
$kotlinxJson
""".trimIndent()
)
println("Identical serialization for: ${kClass.jvmName}")
}
}
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
@Test
fun isIdenticalDeserialization() {
val serializableClasses = findSerializableClasses("com.lagradost")
println("Number of serializable classes: ${serializableClasses.size}")
serializableClasses.forEach { kClass ->
val instance = Instancio.create(kClass.java)
// Convert to JSON to get example JSON object
// We prefer jackson here because the app may have many jackson JSON strings in local storage
val originalJson = jacksonMapper.writeValueAsString(instance)
// Create an object from the JSON using kotlinx
val serializer =
kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass)
assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!")
val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson)
// Create an object from the JSON using jackson
val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java)
// Deep inspect both object using the mapper toJson function.
// This deep equality check can be performed using other methods, but this just works.
val jacksonJson = mapperDecoded.toJson()
val kotlinxJson = kotlinxDecoded.toJson()
assertEquals(
jacksonJson,
kotlinxJson,
"""
Serialization mismatch for:
${kClass.qualifiedName}
Jackson:
$jacksonJson
Kotlinx:
$kotlinxJson
""".trimIndent()
)
println("Identical deserialization for: ${kClass.jvmName}")
}
}
// DEX files are the best solution to read all our classes dynamically.
// classgraph could be used instead, but it only gives results on the JVM, not Android.
@Suppress("DEPRECATION")
private fun findSerializableClasses(packageName: String): List<KClass<*>> {
val context = InstrumentationRegistry
.getInstrumentation()
.targetContext
val dexFile = DexFile(context.packageCodePath)
return dexFile.entries()
.toList()
.filter { it.startsWith(packageName) }
.mapNotNull {
runCatching { Class.forName(it).kotlin }.getOrNull()
}.filter { kClass ->
// Not possible to use .hasAnnotation() on newer Android versions.
kClass.java.annotations.any {
it is Serializable
}
}
}
@OptIn(InternalSerializationApi::class)
@Suppress("UNCHECKED_CAST")
private fun serializeWithKotlinx(
kClass: KClass<*>,
value: Any
): String {
val serializer = kClass.serializer() as KSerializer<Any>
return kotlinxMapper.encodeToString(serializer, value)
}
}

View file

@ -0,0 +1,157 @@
package com.lagradost.cloudstream3.utils.serializers
import android.net.Uri
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KeepGeneratedSerializer
import kotlinx.serialization.Serializable
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = NonEmptyData.Serializer::class)
data class NonEmptyData(
val title: String = "",
val tags: List<String> = emptyList(),
val meta: Map<String, String> = emptyMap(),
val name: String = "hello",
) {
object Serializer : NonEmptySerializer<NonEmptyData>(NonEmptyData.generatedSerializer())
}
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = WriteOnlyData.Serializer::class)
data class WriteOnlyData(
val fieldA: String = "",
val fieldB: String = "",
) {
object Serializer : WriteOnlySerializer<WriteOnlyData>(
WriteOnlyData.generatedSerializer(),
setOf("fieldB"),
)
}
@OptIn(ExperimentalSerializationApi::class)
@KeepGeneratedSerializer
@Serializable(with = MultiWriteOnly.Serializer::class)
data class MultiWriteOnly(
val fieldA: String = "",
val fieldB: String = "",
val fieldC: String = "",
) {
object Serializer : WriteOnlySerializer<MultiWriteOnly>(
MultiWriteOnly.generatedSerializer(),
setOf("fieldB", "fieldC"),
)
}
@Serializable
data class UriData(
@Serializable(with = UriSerializer::class)
val uri: Uri = Uri.EMPTY,
)
class SerializerTest {
@Test
fun nonEmptySerializerOmitsEmptyStrings() {
val data = NonEmptyData(title = "", name = "hello")
val result = data.toJson()
assertFalse(result.contains("title"))
assertTrue(result.contains("name"))
}
@Test
fun nonEmptySerializerOmitsEmptyLists() {
val data = NonEmptyData(tags = emptyList(), name = "hello")
val result = data.toJson()
assertFalse(result.contains("tags"))
}
@Test
fun nonEmptySerializerOmitsEmptyMaps() {
val data = NonEmptyData(meta = emptyMap(), name = "hello")
val result = data.toJson()
assertFalse(result.contains("meta"))
}
@Test
fun nonEmptySerializerKeepsNonEmptyFields() {
val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v"))
val result = data.toJson()
assertTrue(result.contains("title"))
assertTrue(result.contains("tags"))
assertTrue(result.contains("meta"))
}
@Test
fun nonEmptySerializerDoesNotAffectDeserialization() {
val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}"""
val result = parseJson<NonEmptyData>(input)
assertEquals("hello", result.title)
assertEquals(listOf("a"), result.tags)
assertEquals(mapOf("k" to "v"), result.meta)
assertEquals("world", result.name)
}
@Test
fun writeOnlySerializerOmitsFieldOnSerialize() {
val data = WriteOnlyData(fieldA = "hello", fieldB = "secret")
val result = data.toJson()
assertTrue(result.contains("fieldA"))
assertFalse(result.contains("fieldB"))
}
@Test
fun writeOnlySerializerDeserializesNormally() {
val input = """{"fieldA":"hello","fieldB":"secret"}"""
val result = parseJson<WriteOnlyData>(input)
assertEquals("hello", result.fieldA)
assertEquals("secret", result.fieldB)
}
@Test
fun writeOnlySerializerDeserializesMissingAsDefault() {
val input = """{"fieldA":"hello"}"""
val result = parseJson<WriteOnlyData>(input)
assertEquals("hello", result.fieldA)
assertEquals("", result.fieldB)
}
@Test
fun writeOnlySerializerHandlesMultipleKeys() {
val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2")
val result = data.toJson()
assertTrue(result.contains("fieldA"))
assertFalse(result.contains("fieldB"))
assertFalse(result.contains("fieldC"))
}
@Test
fun uriSerializerSerializesUriToString() {
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
val result = data.toJson()
assertTrue(result.contains("https://example.com/path?query=1"))
}
@Test
fun uriSerializerDeserializesStringToUri() {
val input = """{"uri":"https://example.com/path?query=1"}"""
val result = parseJson<UriData>(input)
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
}
@Test
fun uriSerializerRoundtripsCorrectly() {
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
val encoded = data.toJson()
val decoded = parseJson<UriData>(encoded)
assertEquals(data.uri, decoded.uri)
}
}

View file

@ -579,8 +579,10 @@ object CommonActivity {
// TODO: Figure out why removing the check for SearchAutoComplete seems
// to break focus on TV as it shouldn't need to be used.
// Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote)
// send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button.
@SuppressLint("RestrictedApi")
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) {
showInputMethod(act.currentFocus?.findFocus())
@ -601,4 +603,4 @@ object CommonActivity {
}
return null
}
}
}

View file

@ -408,13 +408,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
return true
}
synchronized(apis) {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name, "")
return true
}
}
val matchedApi = apis.filter { str.startsWith(it.mainUrl) }.firstOrNull()
if (matchedApi != null) {
loadResult(str, matchedApi.name, "")
return true
}
}
}
@ -809,12 +806,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
}
}
private val pluginsLock = Mutex()
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
synchronized(allProviders) {
allProviders.withLock {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
@ -1657,9 +1653,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
apis = synchronized(allProviders) {
allProviders.distinctBy { it }
}
apis = allProviders.distinctBy { it }
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -1967,7 +1961,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricCa
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
synchronized(allProviders) {
allProviders.withLock {
for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix(

View file

@ -20,8 +20,10 @@ import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
@ -32,8 +34,8 @@ import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.UiText
import kotlinx.coroutines.Dispatchers
@ -43,7 +45,7 @@ import java.util.concurrent.FutureTask
import kotlin.reflect.jvm.jvmName
object VideoClickActionHolder {
val allVideoClickActions = threadSafeListOf(
val allVideoClickActions = atomicListOf(
// Default
PlayInBrowserAction(),
CopyClipboardAction(),
@ -64,6 +66,8 @@ object VideoClickActionHolder {
MpvYTDLPackage(),
MpvKtPackage(),
MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option
AlwaysAskAction(),
// added by plugins

View file

@ -0,0 +1,75 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.api.Log
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.isEpisodeBased
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Riteshp2001/mpvRx
*
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L132
* https://github.com/Riteshp2001/mpvRx/blob/00e0c5e803ab53e5757426cbf2248448ba1f49bf/app/src/main/java/app/gyrolet/mpvrx/utils/media/MediaUtils.kt#L56
* */
class MpvRxPackage : OpenInAppAction(
appName = txt("mpvRx"),
packageName = "app.gyrolet.mpvrx",
intentClass = "app.gyrolet.mpvrx.ui.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.apply {
putExtra("title", video.name)
val link = result.links[index!!]
val headers = link.headers
setData(link.url.toUri())
if (headers.isNotEmpty()) {
// PlayerActivity expects a flat array: [key1, value1, key2, value2, ...]
val flat = headers.entries.flatMap { listOf(it.key, it.value) }.toTypedArray()
intent.putExtra("headers", flat)
}
/*val subs = result.subs // disabled due to https://github.com/Riteshp2001/mpvRx/issues/146
intent.putExtra("subs", subs.map { it.url.toUri() }.toTypedArray())
intent.putExtra(
"subs.titles",
subs.map { it.name }.toTypedArray(),
)
intent.putExtra(
"subs.langs",
subs.map { it.languageCode }.toTypedArray(),
)
val selected = subs.firstOrNull { it.matchesLanguageCode("en") }?.url?.toUri()
intent.putExtra("subs.enable", selected?.let { arrayOf(it) } ?: arrayOf<Uri>() )*/
if (video.tvType.isEpisodeBased()) {
video.season?.let { intent.putExtra("introdb_season", it) }
video.episode.let { intent.putExtra("introdb_episode", it) }
}
val position = getViewPos(video.id)?.position
if (position != null)
putExtra("position", position.toInt())
}
}
override fun onResult(activity: Activity, intent: Intent?) {
val position = intent?.getIntExtra("position", -1) ?: -1
val duration = intent?.getIntExtra("duration", -1) ?: -1
Log.d("MPV", "Position: $position, Duration: $duration")
updateDurationAndPosition(position.toLong(), duration.toLong())
}
}

View file

@ -0,0 +1,44 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/Kindness-Kismet/only_player/tree/main
* https://github.com/Kindness-Kismet/only_player/blob/main/feature/player/src/main/java/one/only/player/feature/player/PlayerActivity.kt */
class OnlyPlayer : OpenInAppAction(
txt("Only Player"),
"one.only.player",
intentClass = "one.only.player.feature.player.PlayerActivity"
) {
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
/** https://github.com/Kindness-Kismet/only_player/blob/d3f55049a2913fa762d31b311146073cc2da46cb/app/src/main/java/one/only/player/navigation/CloudNavGraph.kt#L39 */
intent.apply {
val link = result.links[index!!]
setData(link.url.toUri())
putExtra("headers", Bundle().apply {
for ((key, value) in link.headers) {
putExtra(key, value)
}
})
}
}
override fun onResult(activity: Activity, intent: Intent?) {
/* onResult does not get called */
}
}

View file

@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import kotlin.Throws
abstract class Plugin : BasePlugin() {
/**
* Called when your Plugin is loaded
@ -26,9 +25,7 @@ abstract class Plugin : BasePlugin() {
fun registerVideoClickAction(element: VideoClickAction) {
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
element.sourcePlugin = this.filename
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.add(element)
}
VideoClickActionHolder.allVideoClickActions.add(element)
}
/**
@ -40,4 +37,4 @@ abstract class Plugin : BasePlugin() {
* This will add a button in the settings allowing you to add custom settings
*/
var openSettings: ((context: Context) -> Unit)? = null
}
}

View file

@ -610,7 +610,7 @@ object PluginManager {
return false
}
InputStreamReader(stream).use { reader ->
manifest = parseJson(reader, BasePlugin.Manifest::class.java)
manifest = parseJson<BasePlugin.Manifest>(reader.readText())
}
}
@ -651,9 +651,15 @@ object PluginManager {
context.resources.configuration
)
}
plugins[filePath] = pluginInstance
classLoaders[loader] = pluginInstance
urlPlugins[data.url ?: filePath] = pluginInstance
synchronized(plugins) {
plugins[filePath] = pluginInstance
}
synchronized(classLoaders) {
classLoaders[loader] = pluginInstance
}
synchronized(urlPlugins) {
urlPlugins[data.url ?: filePath] = pluginInstance
}
if (pluginInstance is Plugin) {
pluginInstance.load(context)
} else {
@ -689,21 +695,20 @@ object PluginManager {
}
// remove all registered apis
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it)
}
}
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename }
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it)
}
synchronized(extractorApis) {
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename }
APIHolder.allProviders.withLock {
APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
}
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
extractorApis.withLock {
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
}
VideoClickActionHolder.allVideoClickActions.withLock {
VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename }
}
synchronized(classLoaders) {

View file

@ -1,50 +1,14 @@
package com.lagradost.cloudstream3.syncproviders
import android.util.Base64
import androidx.annotation.WorkerThread
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.NextAiring
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID
import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.LocalList
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi
import com.lagradost.cloudstream3.syncproviders.providers.OpenSubtitlesApi
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.syncproviders.providers.SubDlApi
import com.lagradost.cloudstream3.syncproviders.providers.SubSourceApi
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppContextUtils.splitQuery
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.utils.txt
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.net.URL
import java.net.URI
import java.security.SecureRandom
import java.util.Date
import java.util.concurrent.TimeUnit
data class AuthLoginPage(
/** The website to open to authenticate */
@ -81,10 +45,10 @@ data class AuthToken(
val payload: String? = null,
) {
fun isAccessTokenExpired(marginSec: Long = 10L) =
accessTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= accessTokenLifetime
accessTokenLifetime != null && unixTime + marginSec >= accessTokenLifetime
fun isRefreshTokenExpired(marginSec: Long = 10L) =
refreshTokenLifetime != null && (System.currentTimeMillis() / 1000) + marginSec >= refreshTokenLifetime
refreshTokenLifetime != null && unixTime + marginSec >= refreshTokenLifetime
}
data class AuthUser(
@ -179,16 +143,33 @@ abstract class AuthAPI {
open val inAppLoginRequirement: AuthLoginRequirement? = null
companion object {
@Deprecated(
message = "Use APIHolder.unixTime instead",
replaceWith = ReplaceWith(
expression = "APIHolder.unixTime",
imports = ["com.lagradost.cloudstream3.APIHolder"]
),
level = DeprecationLevel.WARNING,
)
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
get() = APIHolder.unixTime
@Deprecated(
message = "Use APIHolder.unixTimeMS instead",
replaceWith = ReplaceWith(
expression = "unixTimeMS",
imports = ["com.lagradost.cloudstream3.APIHolder.unixTimeMS"]
),
level = DeprecationLevel.WARNING,
)
val unixTimeMs: Long
get() = System.currentTimeMillis()
get() = unixTimeMS
fun splitRedirectUrl(redirectUrl: String): Map<String, String> {
return splitQuery(
URL(
URI(
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
)
).toURL()
)
}
@ -198,9 +179,8 @@ abstract class AuthAPI {
val secureRandom = SecureRandom()
val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128
secureRandom.nextBytes(codeVerifierBytes)
return Base64.encodeToString(codeVerifierBytes, Base64.DEFAULT).trimEnd('=')
.replace("+", "-")
.replace("/", "_").replace("\n", "")
return base64Encode(codeVerifierBytes).trimEnd('=')
.replace("+", "-").replace("/", "_").replace("\n", "")
}
}

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.subtitles.SubtitleResource
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
/** Stateless safe abstraction of SubtitleAPI */
class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
@ -24,26 +24,30 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
)
// maybe make this a generic struct? right now there is a lot of boilerplate
private val searchCache = threadSafeListOf<SavedSearchResponse>()
private val searchCache = atomicListOf<SavedSearchResponse>()
private var searchCacheIndex: Int = 0
private val resourceCache = threadSafeListOf<SavedResourceResponse>()
private val resourceCache = atomicListOf<SavedResourceResponse>()
private var resourceCacheIndex: Int = 0
const val CACHE_SIZE = 20
}
@WorkerThread
suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = runCatching {
synchronized(resourceCache) {
val cached = resourceCache.withLock {
var found: SubtitleResource? = null
for (item in resourceCache) {
// 20 min save
if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
return@runCatching item.response
found = item.response
break
}
}
found
}
if (cached != null) return@runCatching cached
val returnValue = api.resource(freshAuth(), data)
synchronized(resourceCache) {
resourceCache.withLock {
val add = SavedResourceResponse(unixTime, returnValue, data)
if (resourceCache.size > CACHE_SIZE) {
resourceCache[resourceCacheIndex] = add // rolling cache
@ -58,22 +62,25 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
@WorkerThread
suspend fun search(query: SubtitleSearch): Result<List<SubtitleEntity>> {
return runCatching {
synchronized(searchCache) {
val cached = searchCache.withLock {
var found: List<SubtitleEntity>? = null
for (item in searchCache) {
// 120 min save
if (item.query == query && (unixTime - item.unixTime) < 60 * 120) {
return@runCatching item.response
found = item.response
break
}
}
found
}
val returnValue =
api.search(freshAuth(), query) ?: emptyList()
if (cached != null) return@runCatching cached
val returnValue = api.search(freshAuth(), query) ?: emptyList()
// only cache valid return values
if (returnValue.isNotEmpty()) {
val add = SavedSearchResponse(unixTime, returnValue, query)
synchronized(searchCache) {
searchCache.withLock {
if (searchCache.size > CACHE_SIZE) {
searchCache[searchCacheIndex] = add // rolling cache
searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE
@ -86,4 +93,3 @@ class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
}
}
}

View file

@ -10,8 +10,8 @@ import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.Levenshtein
import com.lagradost.cloudstream3.utils.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.util.Date
/**
@ -138,7 +138,7 @@ abstract class SyncAPI : AuthAPI() {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-FuzzySearch.partialRatio(
-Levenshtein.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
@ -191,4 +191,4 @@ abstract class SyncAPI : AuthAPI() {
override var score: Score? = null,
val tags: List<String>? = null
) : SearchResponse
}
}

View file

@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.Actor
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.ActorRole
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.ErrorLoadingException
@ -35,7 +37,7 @@ class AniListApi : SyncAPI() {
override var name = "AniList"
override val idPrefix = "anilist"
val key = "6871"
private val key = BuildConfig.ANILIST_KEY
override val redirectUrlIdentifier = "anilistlogin"
override var requireLibraryRefresh = true
override val hasOAuth2 = true
@ -50,9 +52,10 @@ class AniListApi : SyncAPI() {
override suspend fun login(redirectUrl: String, payload: String?): AuthToken? {
val sanitizer = splitRedirectUrl(redirectUrl)
val token = AuthToken(
accessToken = sanitizer["access_token"] ?: throw ErrorLoadingException("No access token"),
accessToken = sanitizer["access_token"]
?: throw ErrorLoadingException("No access token"),
//refreshToken = sanitizer["refresh_token"],
accessTokenLifetime = unixTime + sanitizer["expires_in"]!!.toLong(),
accessTokenLifetime = APIHolder.unixTime + sanitizer["expires_in"]!!.toLong(),
)
return token
}
@ -83,8 +86,8 @@ class AniListApi : SyncAPI() {
return "$mainUrl/anime/$id"
}
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(name) ?: return null
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val data = searchShows(query) ?: return null
return data.data?.page?.media?.map {
SyncAPI.SyncSearchResult(
it.title.romaji ?: return null,
@ -96,7 +99,7 @@ class AniListApi : SyncAPI() {
}
}
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
val internalId = (Regex("anilist\\.co/anime/(\\d*)").find(id)?.groupValues?.getOrNull(1)
?: id).toIntOrNull() ?: throw ErrorLoadingException("Invalid internalId")
val season = getSeason(internalId).data.media
@ -106,7 +109,7 @@ class AniListApi : SyncAPI() {
nextAiring = season.nextAiringEpisode?.let {
NextAiring(
it.episode ?: return@let null,
(it.timeUntilAiring ?: return@let null) + unixTime
(it.timeUntilAiring ?: return@let null) + APIHolder.unixTime
)
},
title = season.title?.userPreferred,
@ -158,7 +161,7 @@ class AniListApi : SyncAPI() {
)
}
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
val internalId = id.toIntOrNull() ?: return null
val data = getDataAboutId(auth ?: return null, internalId) ?: return null
@ -459,7 +462,7 @@ class AniListApi : SyncAPI() {
}
}
private suspend fun getDataAboutId(auth : AuthData, id: Int): AniListTitleHolder? {
private suspend fun getDataAboutId(auth: AuthData, id: Int): AniListTitleHolder? {
val q =
"""query (${'$'}id: Int = $id) { # Define which variables will be used in the query (id)
Media (id: ${'$'}id, type: ANIME) { # Insert our variables into the query arguments (id) (type: ANIME is hard-coded in the query)
@ -506,7 +509,7 @@ class AniListApi : SyncAPI() {
}
private suspend fun postApi(token : AuthToken, q: String, cache: Boolean = false): String? {
private suspend fun postApi(token: AuthToken, q: String, cache: Boolean = false): String? {
return app.post(
"https://graphql.anilist.co/",
headers = mapOf(
@ -638,7 +641,7 @@ class AniListApi : SyncAPI() {
}
}
override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? {
override suspend fun library(auth: AuthData?): SyncAPI.LibraryMetadata? {
val list = getAniListAnimeListSmart(auth ?: return null)?.groupBy {
convertAniListStringToStatus(it.status ?: "").stringRes
}?.mapValues { group ->
@ -666,7 +669,7 @@ class AniListApi : SyncAPI() {
)
}
private suspend fun getFullAniListList(auth : AuthData): FullAnilistList? {
private suspend fun getFullAniListList(auth: AuthData): FullAnilistList? {
val userID = auth.user.id
val mediaType = "ANIME"
@ -714,7 +717,7 @@ class AniListApi : SyncAPI() {
return text?.toKotlinObject()
}
suspend fun toggleLike(auth : AuthData, id: Int): Boolean {
suspend fun toggleLike(auth: AuthData, id: Int): Boolean {
val q = """mutation (${'$'}animeId: Int = $id) {
ToggleFavourite (animeId: ${'$'}animeId) {
anime {
@ -737,7 +740,7 @@ class AniListApi : SyncAPI() {
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId(
auth : AuthData,
auth: AuthData,
id: Int,
type: AniListStatusType,
score: Score?,
@ -786,7 +789,7 @@ class AniListApi : SyncAPI() {
return data != ""
}
private suspend fun getUser(token : AuthToken): AniListUser? {
private suspend fun getUser(token: AuthToken): AniListUser? {
val q = """
{
Viewer {

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R
@ -27,9 +28,8 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.text.SimpleDateFormat
import java.time.Instant
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.ZoneId
import java.util.Date
import java.util.Locale
@ -107,7 +107,7 @@ class KitsuApi: SyncAPI() {
).parsed<ResponseToken>()
return AuthToken(
accessTokenLifetime = unixTime + token.expiresIn.toLong(),
accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(),
refreshToken = token.refreshToken,
accessToken = token.accessToken,
)
@ -126,7 +126,7 @@ class KitsuApi: SyncAPI() {
return AuthToken(
accessToken = res.accessToken,
refreshToken = res.refreshToken,
accessTokenLifetime = unixTime + res.expiresIn.toLong()
accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong()
)
}
@ -202,7 +202,7 @@ class KitsuApi: SyncAPI() {
id = id,
totalEpisodes = anime.episodeCount,
title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(),
publicScore = Score.from(anime.ratingTwenty.toString(), 20),
publicScore = Score.from(anime.ratingTwenty, 20),
duration = anime.episodeLength,
synopsis = anime.synopsis,
airStatus = when(anime.status) {
@ -250,7 +250,7 @@ class KitsuApi: SyncAPI() {
}
return SyncStatus(
score = Score.from(anime.ratingTwenty.toString(), 20),
score = Score.from(anime.ratingTwenty, 20),
status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)),
isFavorite = null,
watchedEpisodes = anime.progress,
@ -454,8 +454,8 @@ class KitsuApi: SyncAPI() {
private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array<KitsuNode> {
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","episodeCount")
val libraryEntriesSelectedFields = arrayOf("progress","rating","updatedAt", "status")
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","synopsis","startDate","endDate","episodeCount")
val libraryEntriesSelectedFields = arrayOf("progress","ratingTwenty","updatedAt", "status")
val limit = 500
var url = "$apiUrl/library-entries?filter[userId]=$userId&filter[kind]=anime&include=anime&page[limit]=$limit&page[offset]=0&fields[anime]=${animeSelectedFields.joinToString(",")}&fields[libraryEntries]=${libraryEntriesSelectedFields.joinToString(",")}"
@ -526,7 +526,7 @@ class KitsuApi: SyncAPI() {
this.id,
this.attributes.progress,
numEpisodes,
Score.from(this.attributes.ratingTwenty.toString(), 20),
Score.from(this.attributes.ratingTwenty, 20),
parseDateLong(this.attributes.updatedAt),
"Kitsu",
TvType.Anime,
@ -535,12 +535,9 @@ class KitsuApi: SyncAPI() {
null,
plot = synopsis,
releaseDate = if (startDate == null) null else try {
Date.from(
Instant.from(
DateTimeFormatter.ofPattern(if (startDate.length == 4) "yyyy" else if (startDate.length == 7) "yyyy-MM" else "yyyy-MM-dd")
.parse(startDate)
)
)
Date.from(LocalDate.parse(startDate).atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant())
} catch (_: RuntimeException) {
null
}
@ -583,7 +580,7 @@ class KitsuApi: SyncAPI() {
@JsonProperty("avatar") val avatar: KitsuUserAvatar?,
/* User list anime attributes */
@JsonProperty("progress") val progress: Int?,
@JsonProperty("ratingTwenty") val ratingTwenty: Float?,
@JsonProperty("ratingTwenty") val ratingTwenty: Int?,
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("status") val status: String?,
)
@ -632,7 +629,7 @@ class KitsuApi: SyncAPI() {
const val KITSU_CACHED_LIST: String = "kitsu_cached_list"
private fun parseDateLong(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()).parse(
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {

View file

@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R
@ -34,7 +36,7 @@ class MALApi : SyncAPI() {
override var name = "MAL"
override val idPrefix = "mal"
val key = "1714d6f2f4f7cc19644384f8c4629910"
private val key = BuildConfig.MAL_KEY
private val apiUrl = "https://api.myanimelist.net"
override val hasOAuth2 = true
override val redirectUrlIdentifier: String? = "mallogin"
@ -78,7 +80,7 @@ class MALApi : SyncAPI() {
)
).parsed<ResponseToken>()
return AuthToken(
accessTokenLifetime = unixTime + token.expiresIn.toLong(),
accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(),
refreshToken = token.refreshToken,
accessToken = token.accessToken
)
@ -98,9 +100,9 @@ class MALApi : SyncAPI() {
)
}
override suspend fun search(auth : AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
val auth = auth?.token?.accessToken ?: return null
val url = "$apiUrl/v2/anime?q=$name&limit=$MAL_MAX_SEARCH_LIMIT"
val url = "$apiUrl/v2/anime?q=$query&limit=$MAL_MAX_SEARCH_LIMIT"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth",
@ -122,7 +124,7 @@ class MALApi : SyncAPI() {
Regex("""/anime/((.*)/|(.*))""").find(url)!!.groupValues.first()
override suspend fun updateStatus(
auth : AuthData?,
auth: AuthData?,
id: String,
newStatus: SyncAPI.AbstractSyncStatus
): Boolean {
@ -225,7 +227,7 @@ class MALApi : SyncAPI() {
)
}
override suspend fun load(auth : AuthData?, id: String): SyncAPI.SyncResult? {
override suspend fun load(auth: AuthData?, id: String): SyncAPI.SyncResult? {
val auth = auth?.token?.accessToken ?: return null
val internalId = id.toIntOrNull() ?: return null
val url =
@ -271,7 +273,7 @@ class MALApi : SyncAPI() {
}
}
override suspend fun status(auth : AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
override suspend fun status(auth: AuthData?, id: String): SyncAPI.AbstractSyncStatus? {
val auth = auth?.token?.accessToken ?: return null
// https://myanimelist.net/apiconfig/references/api/v2#operation/anime_anime_id_get
@ -366,7 +368,7 @@ class MALApi : SyncAPI() {
return AuthToken(
accessToken = res.accessToken,
refreshToken = res.refreshToken,
accessTokenLifetime = unixTime + res.expiresIn.toLong()
accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong()
)
}
@ -477,7 +479,7 @@ class MALApi : SyncAPI() {
@JsonProperty("start_time") val startTime: String?
)
override suspend fun library(auth : AuthData?): LibraryMetadata? {
override suspend fun library(auth: AuthData?): LibraryMetadata? {
val list = getMalAnimeListSmart(auth ?: return null)?.groupBy {
convertToStatus(it.listStatus?.status ?: "").stringRes
}?.mapValues { group ->
@ -505,7 +507,7 @@ class MALApi : SyncAPI() {
)
}
private suspend fun getMalAnimeListSmart(auth : AuthData): Array<Data>? {
private suspend fun getMalAnimeListSmart(auth: AuthData): Array<Data>? {
return if (requireLibraryRefresh) {
val list = getMalAnimeList(auth.token)
setKey(MAL_CACHED_LIST, auth.user.id.toString(), list)

View file

@ -2,9 +2,11 @@ package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement
@ -43,17 +45,17 @@ class OpenSubtitlesApi : SubtitleAPI() {
}
private fun canDoRequest(): Boolean {
return unixTimeMs > currentCoolDown
return unixTimeMS > currentCoolDown
}
private fun throwIfCantDoRequest() {
if (!canDoRequest()) {
throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMs) / 1000L}s")
throw ErrorLoadingException("Too many requests wait for ${(currentCoolDown - unixTimeMS) / 1000L}s")
}
}
private fun throwGotTooManyRequests() {
currentCoolDown = unixTimeMs + COOLDOWN_DURATION
currentCoolDown = unixTimeMS + COOLDOWN_DURATION
throw ErrorLoadingException("Too many requests")
}
@ -89,7 +91,7 @@ class OpenSubtitlesApi : SubtitleAPI() {
accessToken = response.token
?: throw ErrorLoadingException("Invalid password or username"),
/// JWT token is valid 24 hours after successfully authentication of user
accessTokenLifetime = unixTime + 60 * 60 * 24,
accessTokenLifetime = APIHolder.unixTime + 60 * 60 * 24,
payload = form.toJson()
)
}

View file

@ -4,6 +4,7 @@ import androidx.annotation.StringRes
import androidx.core.net.toUri
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CloudStreamApp
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
@ -16,7 +17,6 @@ import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.SimklSyncServices
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
@ -30,6 +30,7 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.DataStoreHelper.toYear
import com.lagradost.cloudstream3.utils.txt
import java.math.BigInteger
@ -77,15 +78,15 @@ class SimklApi : SyncAPI() {
private class SimklCacheWrapper<T>(
@JsonProperty("obj") val obj: T?,
@JsonProperty("validUntil") val validUntil: Long,
@JsonProperty("cacheTime") val cacheTime: Long = unixTime,
@JsonProperty("cacheTime") val cacheTime: Long = APIHolder.unixTime,
) {
/** Returns true if cache is newer than cacheDays */
fun isFresh(): Boolean {
return validUntil > unixTime
return validUntil > APIHolder.unixTime
}
fun remainingTime(): Duration {
val unixTime = unixTime
val unixTime = APIHolder.unixTime
return if (validUntil > unixTime) {
(validUntil - unixTime).toDuration(DurationUnit.SECONDS)
} else {
@ -109,7 +110,7 @@ class SimklApi : SyncAPI() {
SIMKL_CACHE_KEY,
path,
// Storing as plain sting is required to make generics work.
SimklCacheWrapper(value, unixTime + cacheTime.inWholeSeconds).toJson()
SimklCacheWrapper(value, APIHolder.unixTime + cacheTime.inWholeSeconds).toJson()
)
}
@ -117,13 +118,8 @@ class SimklApi : SyncAPI() {
* Gets cached object, if object is not fresh returns null and removes it from cache
*/
inline fun <reified T : Any> getKey(path: String): T? {
// Required for generic otherwise "LinkedHashMap cannot be cast to MediaObject"
val type = mapper.typeFactory.constructParametricType(
SimklCacheWrapper::class.java,
T::class.java
)
val cache = getKey<String>(SIMKL_CACHE_KEY, path)?.let {
mapper.readValue<SimklCacheWrapper<T>>(it, type)
tryParseJson<SimklCacheWrapper<T>>(it)
}
return if (cache?.isFresh() == true) {
@ -423,7 +419,7 @@ class SimklApi : SyncAPI() {
}
suspend fun execute(): Boolean {
val time = getDateTime(unixTime)
val time = getDateTime(APIHolder.unixTime)
val headers = this.headers ?: emptyMap()
return if (this.status == SimklListStatusType.None.value) {
app.post(
@ -573,7 +569,7 @@ class SimklApi : SyncAPI() {
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("rating") val rating: Int,
@JsonProperty("rated_at") val ratedAt: String? = getDateTime(unixTime)
@JsonProperty("rated_at") val ratedAt: String? = getDateTime(APIHolder.unixTime)
) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -582,7 +578,7 @@ class SimklApi : SyncAPI() {
@JsonProperty("year") year: Int?,
@JsonProperty("ids") ids: Ids?,
@JsonProperty("to") val to: String,
@JsonProperty("watched_at") val watchedAt: String? = getDateTime(unixTime)
@JsonProperty("watched_at") val watchedAt: String? = getDateTime(APIHolder.unixTime)
) : MediaObject(title, year, ids)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@ -867,7 +863,7 @@ class SimklApi : SyncAPI() {
newStatus: AbstractSyncStatus
): Boolean {
val parsedId = readIdFromString(id)
lastScoreTime = unixTime
lastScoreTime = APIHolder.unixTime
val simklStatus = newStatus as? SimklSyncStatus
val builder = SimklScoreBuilder.Builder()
@ -916,7 +912,7 @@ class SimklApi : SyncAPI() {
override suspend fun search(auth: AuthData?, query: String): List<SyncAPI.SyncSearchResult>? {
return app.get(
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to name)
"$mainUrl/search/", params = mapOf("client_id" to CLIENT_ID, "q" to query)
).parsedSafe<Array<MediaObject>>()?.mapNotNull { it.toSyncSearchResult() }
}

View file

@ -12,6 +12,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthToken
import com.lagradost.cloudstream3.syncproviders.AuthUser
import com.lagradost.cloudstream3.syncproviders.SubtitleAPI
import com.lagradost.cloudstream3.TvType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class SubDlApi : SubtitleAPI() {
override val name = "SubDL"
@ -24,7 +26,7 @@ class SubDlApi : SubtitleAPI() {
override val createAccountUrl = "https://subdl.com/panel/register"
companion object {
const val APIURL = "https://apiold.subdl.com"
const val APIURL = "https://api.subdl.com"
const val APIENDPOINT = "$APIURL/api/v1/subtitles"
const val DOWNLOADENDPOINT = "https://dl.subdl.com"
}
@ -122,72 +124,80 @@ class SubDlApi : SubtitleAPI() {
}
}
@Serializable
data class SubtitleOAuthEntity(
@JsonProperty("userEmail") var userEmail: String,
@JsonProperty("pass") var pass: String,
@JsonProperty("name") var name: String? = null,
@JsonProperty("accessToken") var accessToken: String? = null,
@JsonProperty("apiKey") var apiKey: String? = null,
@JsonProperty("userEmail") @SerialName("userEmail") var userEmail: String,
@JsonProperty("pass") @SerialName("pass") var pass: String,
@JsonProperty("name") @SerialName("name") var name: String? = null,
@JsonProperty("accessToken") @SerialName("accessToken") var accessToken: String? = null,
@JsonProperty("apiKey") @SerialName("apiKey") var apiKey: String? = null,
)
@Serializable
data class OAuthTokenResponse(
@JsonProperty("token") val token: String,
@JsonProperty("userData") val userData: UserData? = null,
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("message") val message: String? = null,
@JsonProperty("token") @SerialName("token") val token: String,
@JsonProperty("userData") @SerialName("userData") val userData: UserData? = null,
@JsonProperty("status") @SerialName("status") val status: Boolean? = null,
@JsonProperty("message") @SerialName("message") val message: String? = null,
)
@Serializable
data class UserData(
@JsonProperty("email") val email: String,
@JsonProperty("name") val name: String,
@JsonProperty("country") val country: String,
@JsonProperty("scStepCode") val scStepCode: String,
@JsonProperty("scVerified") val scVerified: Boolean,
@JsonProperty("username") val username: String? = null,
@JsonProperty("scUsername") val scUsername: String,
@JsonProperty("email") @SerialName("email") val email: String,
@JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("country") @SerialName("country") val country: String,
@JsonProperty("scStepCode") @SerialName("scStepCode") val scStepCode: String,
@JsonProperty("scVerified") @SerialName("scVerified") val scVerified: Boolean,
@JsonProperty("username") @SerialName("username") val username: String? = null,
@JsonProperty("scUsername") @SerialName("scUsername") val scUsername: String,
)
@Serializable
data class ApiKeyResponse(
@JsonProperty("ok") val ok: Boolean? = false,
@JsonProperty("api_key") val apiKey: String,
@JsonProperty("usage") val usage: Usage? = null,
@JsonProperty("ok") @SerialName("ok") val ok: Boolean? = false,
@JsonProperty("api_key") @SerialName("api_key") val apiKey: String,
@JsonProperty("usage") @SerialName("usage") val usage: Usage? = null,
)
@Serializable
data class Usage(
@JsonProperty("total") val total: Long? = 0,
@JsonProperty("today") val today: Long? = 0,
@JsonProperty("total") @SerialName("total") val total: Long? = 0,
@JsonProperty("today") @SerialName("today") val today: Long? = 0,
)
@Serializable
data class ApiResponse(
@JsonProperty("status") val status: Boolean? = null,
@JsonProperty("results") val results: List<Result>? = null,
@JsonProperty("subtitles") val subtitles: List<Subtitle>? = null,
@JsonProperty("status") @SerialName("status") val status: Boolean? = null,
@JsonProperty("results") @SerialName("results") val results: List<Result>? = null,
@JsonProperty("subtitles") @SerialName("subtitles") val subtitles: List<Subtitle>? = null,
)
@Serializable
data class Result(
@JsonProperty("sd_id") val sdId: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("name") val name: String? = null,
@JsonProperty("imdb_id") val imdbId: String? = null,
@JsonProperty("tmdb_id") val tmdbId: Long? = null,
@JsonProperty("first_air_date") val firstAirDate: String? = null,
@JsonProperty("year") val year: Int? = null,
@JsonProperty("sd_id") @SerialName("sd_id") val sdId: Int? = null,
@JsonProperty("type") @SerialName("type") val type: String? = null,
@JsonProperty("name") @SerialName("name") val name: String? = null,
@JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null,
@JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long? = null,
@JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null,
@JsonProperty("year") @SerialName("year") val year: Int? = null,
)
@Serializable
data class Subtitle(
@JsonProperty("release_name") val releaseName: String,
@JsonProperty("name") val name: String,
@JsonProperty("lang") val lang: String, // subdl language code
@JsonProperty("author") val author: String? = null,
@JsonProperty("url") val url: String? = null,
@JsonProperty("subtitlePage") val subtitlePage: String? = null,
@JsonProperty("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null,
@JsonProperty("language") val language: String? = null, // full language name
@JsonProperty("hi") val hearingImpaired: Boolean? = null,
@JsonProperty("release_name") @SerialName("release_name") val releaseName: String,
@JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("lang") @SerialName("lang") val lang: String, // subdl language code
@JsonProperty("author") @SerialName("author") val author: String? = null,
@JsonProperty("url") @SerialName("url") val url: String? = null,
@JsonProperty("subtitlePage") @SerialName("subtitlePage") val subtitlePage: String? = null,
@JsonProperty("season") @SerialName("season") val season: Int? = null,
@JsonProperty("episode") @SerialName("episode") val episode: Int? = null,
@JsonProperty("language") @SerialName("language") val language: String? = null, // full language name
@JsonProperty("hi") @SerialName("hi") val hearingImpaired: Boolean? = null,
)
// https://subdl.com/api-files/language_list.json
// https://subdl.com/api-files/language_list.json
// most of it is IETF BPC 47 conformant tag
// but there are some exceptions
private val langTagIETF2subdl = mapOf(
@ -197,63 +207,63 @@ class SubDlApi : SubtitleAPI() {
"en-nl" to "NL_EN", // "Dutch_English"
"pt-br" to "BR_PT", // "Brazillian Portuguese"
"zh-hant" to "ZH_BG", // "Big 5 code" -> traditional Chinese (?_?)
// "ar" to "AR", // "Arabic"
// "az" to "AZ", // "Azerbaijani"
// "be" to "BE", // "Belarusian"
// "bg" to "BG", // "Bulgarian"
// "bn" to "BN", // "Bengali"
// "bs" to "BS", // "Bosnian"
// "ca" to "CA", // "Catalan"
// "cs" to "CS", // "Czech"
// "da" to "DA", // "Danish"
// "de" to "DE", // "German"
// "el" to "EL", // "Greek"
// "en" to "EN", // "English"
// "eo" to "EO", // "Esperanto"
// "es" to "ES", // "Spanish"
// "et" to "ET", // "Estonian"
// "fa" to "FA", // "Farsi_Persian"
// "fi" to "FI", // "Finnish"
// "fr" to "FR", // "French"
// "he" to "HE", // "Hebrew"
// "hi" to "HI", // "Hindi"
// "hr" to "HR", // "Croatian"
// "hu" to "HU", // "Hungarian"
// "id" to "ID", // "Indonesian"
// "is" to "IS", // "Icelandic"
// "it" to "IT", // "Italian"
// "ja" to "JA", // "Japanese"
// "ka" to "KA", // "Georgian"
// "kl" to "KL", // "Greenlandic"
// "ko" to "KO", // "Korean"
// "ku" to "KU", // "Kurdish"
// "lt" to "LT", // "Lithuanian"
// "lv" to "LV", // "Latvian"
// "mk" to "MK", // "Macedonian"
// "ml" to "ML", // "Malayalam"
// "mni" to "MNI", // "Manipuri"
// "ms" to "MS", // "Malay"
// "my" to "MY", // "Burmese"
// "nl" to "NL", // "Dutch"
// "no" to "NO", // "Norwegian"
// "pl" to "PL", // "Polish"
// "pt" to "PT", // "Portuguese"
// "ro" to "RO", // "Romanian"
// "ru" to "RU", // "Russian"
// "si" to "SI", // "Sinhala"
// "sk" to "SK", // "Slovak"
// "sl" to "SL", // "Slovenian"
// "sq" to "SQ", // "Albanian"
// "sr" to "SR", // "Serbian"
// "sv" to "SV", // "Swedish"
// "ta" to "TA", // "Tamil"
// "te" to "TE", // "Telugu"
// "th" to "TH", // "Thai"
// "tl" to "TL", // "Tagalog"
// "tr" to "TR", // "Turkish"
// "uk" to "UK", // "Ukranian"
// "ur" to "UR", // "Urdu"
// "vi" to "VI", // "Vietnamese"
// "zh" to "ZH", // "Chinese BG code"
// "ar" to "AR", // "Arabic"
// "az" to "AZ", // "Azerbaijani"
// "be" to "BE", // "Belarusian"
// "bg" to "BG", // "Bulgarian"
// "bn" to "BN", // "Bengali"
// "bs" to "BS", // "Bosnian"
// "ca" to "CA", // "Catalan"
// "cs" to "CS", // "Czech"
// "da" to "DA", // "Danish"
// "de" to "DE", // "German"
// "el" to "EL", // "Greek"
// "en" to "EN", // "English"
// "eo" to "EO", // "Esperanto"
// "es" to "ES", // "Spanish"
// "et" to "ET", // "Estonian"
// "fa" to "FA", // "Farsi_Persian"
// "fi" to "FI", // "Finnish"
// "fr" to "FR", // "French"
// "he" to "HE", // "Hebrew"
// "hi" to "HI", // "Hindi"
// "hr" to "HR", // "Croatian"
// "hu" to "HU", // "Hungarian"
// "id" to "ID", // "Indonesian"
// "is" to "IS", // "Icelandic"
// "it" to "IT", // "Italian"
// "ja" to "JA", // "Japanese"
// "ka" to "KA", // "Georgian"
// "kl" to "KL", // "Greenlandic"
// "ko" to "KO", // "Korean"
// "ku" to "KU", // "Kurdish"
// "lt" to "LT", // "Lithuanian"
// "lv" to "LV", // "Latvian"
// "mk" to "MK", // "Macedonian"
// "ml" to "ML", // "Malayalam"
// "mni" to "MNI", // "Manipuri"
// "ms" to "MS", // "Malay"
// "my" to "MY", // "Burmese"
// "nl" to "NL", // "Dutch"
// "no" to "NO", // "Norwegian"
// "pl" to "PL", // "Polish"
// "pt" to "PT", // "Portuguese"
// "ro" to "RO", // "Romanian"
// "ru" to "RU", // "Russian"
// "si" to "SI", // "Sinhala"
// "sk" to "SK", // "Slovak"
// "sl" to "SL", // "Slovenian"
// "sq" to "SQ", // "Albanian"
// "sr" to "SR", // "Serbian"
// "sv" to "SV", // "Swedish"
// "ta" to "TA", // "Tamil"
// "te" to "TE", // "Telugu"
// "th" to "TH", // "Thai"
// "tl" to "TL", // "Tagalog"
// "tr" to "TR", // "Turkish"
// "uk" to "UK", // "Ukranian"
// "ur" to "UR", // "Urdu"
// "vi" to "VI", // "Vietnamese"
// "zh" to "ZH", // "Chinese BG code"
)
}

View file

@ -17,7 +17,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.newSearchResponseList
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@ -55,7 +55,7 @@ class APIRepository(val api: MainAPI) {
val hash: Pair<String, String>
)
private val cache = threadSafeListOf<SavedLoadResponse>()
private val cache = atomicListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0
const val CACHE_SIZE = 20
@ -66,9 +66,7 @@ class APIRepository(val api: MainAPI) {
private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) {
synchronized(cache) {
cache.clear()
}
cache.clear()
}
}
@ -91,21 +89,25 @@ class APIRepository(val api: MainAPI) {
val fixedUrl = api.fixUrl(url)
val lookingForHash = Pair(api.name, fixedUrl)
synchronized(cache) {
val cached = cache.withLock {
var found: LoadResponse? = null
for (item in cache) {
// 10 min save
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
return@withTimeout item.response
found = item.response
break
}
}
found
}
if (cached != null) return@withTimeout cached
api.load(fixedUrl)?.also { response ->
// Remove all blank tags as early as possible
response.tags = response.tags?.filter { it.isNotBlank() }
val add = SavedLoadResponse(unixTime, response, lookingForHash)
synchronized(cache) {
cache.withLock {
if (cache.size > CACHE_SIZE) {
cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % CACHE_SIZE
@ -215,4 +217,4 @@ class APIRepository(val api: MainAPI) {
return false
}
}
}
}

View file

@ -12,9 +12,6 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
import androidx.appcompat.app.AlertDialog
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.google.android.gms.cast.MediaLoadOptions
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaSeekOptions
@ -105,9 +102,6 @@ data class MetadataHolder(
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) :
UIController() {
private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
init {
view.setImageResource(R.drawable.ic_baseline_playlist_play_24)
view.setOnClickListener {
@ -449,4 +443,4 @@ class ControllerActivity : ExpandedControllerActivity() {
SkipNextEpisodeController(skipOpButton)
)
}
}
}

View file

@ -133,7 +133,7 @@ class HomeViewModel : ViewModel() {
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage } })
return APIRepository(apis.withLock { apis.first { it.hasMainPage } })
}
private val _availableWatchStatusTypes =

View file

@ -210,14 +210,13 @@ class LibraryFragment : BaseFragment<FragmentLibraryBinding>(
syncId: SyncIdName,
apiName: String? = null,
) {
val availableProviders = synchronized(allProviders) {
allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
}
val availableProviders = allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
val baseOptions = listOf(
LibraryOpenerType.Default,
LibraryOpenerType.None,

View file

@ -96,7 +96,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.applyStyle
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.CLEARKEY_UUID
import com.lagradost.cloudstream3.utils.CLEARKEY_DRM_UUID
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
@ -104,9 +104,9 @@ import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.PLAYREADY_UUID
import com.lagradost.cloudstream3.utils.PLAYREADY_DRM_UUID
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName
import com.lagradost.cloudstream3.utils.WIDEVINE_UUID
import com.lagradost.cloudstream3.utils.WIDEVINE_DRM_UUID
import com.lagradost.cloudstream3.utils.videoskip.VideoSkipStamp
import kotlinx.coroutines.delay
import okhttp3.Interceptor
@ -118,6 +118,7 @@ import java.util.concurrent.Executors
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession
import kotlin.uuid.toJavaUuid
const val TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
@ -1278,7 +1279,7 @@ class CS3IPlayer : IPlayer {
item.drm?.let { drm ->
when (drm.uuid) {
CLEARKEY_UUID -> {
CLEARKEY_DRM_UUID.toJavaUuid() -> {
// Use headers from DrmMetadata for media requests
val client = dataSourceFactory
?: throw IllegalArgumentException("Must supply onlineSource")
@ -1299,8 +1300,8 @@ class CS3IPlayer : IPlayer {
.createMediaSource(item.mediaItem)
}
WIDEVINE_UUID,
PLAYREADY_UUID -> {
WIDEVINE_DRM_UUID.toJavaUuid(),
PLAYREADY_DRM_UUID.toJavaUuid() -> {
// Use headers from DrmMetadata for media requests
val client = dataSourceFactory
?: throw IllegalArgumentException("Must supply onlineSource")
@ -1914,7 +1915,7 @@ class CS3IPlayer : IPlayer {
drm = DrmMetadata(
kid = link.kid,
key = link.key,
uuid = link.uuid,
uuid = link.uuid.toJavaUuid(),
kty = link.kty,
licenseUrl = link.licenseUrl,
keyRequestParameters = link.keyRequestParameters,

View file

@ -58,9 +58,23 @@ class DownloadedPlayerActivity : AppCompatActivity() {
enableEdgeToEdgeCompat()
setContentView(R.layout.empty_layout)
Log.i(TAG, "onCreate")
handleIntent(intent)
attachBackPressedCallback("DownloadedPlayerActivity") { finish() }
/**
* Use moveTaskToBack instead of finish() so there is always exactly one task
* entry in recents, always reflecting the current file.
*
* finish() destroys the Activity but may leave the task in recents. Each new file
* open can create a new task entry, so recents accumulates stale entries for old
* files. The user then taps a stale entry and gets the wrong file.
*
* moveTaskToBack keeps the Activity alive in the background. There is only ever
* one task entry in recents. New files opened from the file manager arrive via
* onNewIntent on the live instance, updating the player immediately. The single
* recents entry always reflects the current state, ensuring we load the
* correct file.
*/
attachBackPressedCallback("DownloadedPlayerActivity") { moveTaskToBack(true) }
}
private fun handleIntent(intent: Intent) {
@ -83,11 +97,11 @@ class DownloadedPlayerActivity : AppCompatActivity() {
url != null -> playLink(this, url)
data != null -> playUri(this, data)
extraText != null -> playLink(this, extraText)
else -> { finish(); return }
else -> finishAndRemoveTask()
}
} else if (data?.scheme == "content") {
playUri(this, data)
} else finish()
} else finishAndRemoveTask()
}
override fun onResume() {

View file

@ -945,12 +945,18 @@ open class FullScreenPlayer : AbstractPlayerFragment<FragmentPlayerBinding>(
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER -> { // space is not captured due to navigation
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
}
KeyEvent.KEYCODE_DPAD_CENTER -> {
if (isShowing) {
// KEYCODE_DPAD_CENTER and KEYCODE_ENTER both act as a "select/confirm" button.
// Some remotes (e.g. LG Magic Remote) send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER.
// When the player UI or a dialog is visible, we let the event pass through (return null)
// so the focused button/item can handle the click normally, rather than always toggling
// play/pause. Only when the UI is hidden do we treat it as a play/pause toggle.
KeyEvent.KEYCODE_DPAD_CENTER,
KeyEvent.KEYCODE_ENTER -> {
if (isShowing || isDialogOpen()) {
return null
}
// If UI is not shown make click instantly skip to next chapter even if locked

View file

@ -1732,11 +1732,11 @@ class GeneratorPlayer : FullScreenPlayer() {
): SubtitleData? {
val langCode = preferredAutoSelectSubtitles ?: return null
if (downloads) {
return sortSubs(subtitles).firstOrNull {
sortSubs(subtitles).firstOrNull {
it.origin == SubtitleOrigin.DOWNLOADED_FILE && it.matchesLanguageCode(
langCode
)
}
}?.let { return it }
}
if (!settings) return null

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat.getString
import androidx.navigation.NavOptions
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.temp.CloudStreamPackage
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
@ -12,6 +13,15 @@ import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.safefile.SafeFile
object OfflinePlaybackHelper {
/**
* Pop any existing player off the nav back stack before pushing the new one,
* keeping the stack flat (at most one player at a time). This prevents an
* OOM when many files are opened in sequence via DownloadedPlayerActivity.
*/
private val replacePlayerNavOptions = NavOptions.Builder()
.setPopUpTo(R.id.navigation_player, inclusive = true, saveState = false)
.build()
fun playLink(activity: Activity, url: String) {
activity.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
@ -20,7 +30,8 @@ object OfflinePlaybackHelper {
BasicLink(url)
), id = url.hashCode()
), 0
)
),
replacePlayerNavOptions
)
}
@ -52,7 +63,8 @@ object OfflinePlaybackHelper {
subs,
if (id != -1) id else null,
), 0
)
),
replacePlayerNavOptions
)
return true
}
@ -76,7 +88,8 @@ object OfflinePlaybackHelper {
)
)
), 0
)
),
replacePlayerNavOptions
)
}
}

View file

@ -83,6 +83,7 @@ import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppContextUtils.isConnectedToChromecast
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.CastHelper.startCast
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
@ -1324,7 +1325,7 @@ class ResultViewModel2 : ViewModel() {
episodeIds: Array<String>,
watchState: VideoWatchState
) {
val watchStateString = DataStore.mapper.writeValueAsString(watchState)
val watchStateString = watchState.toJson()
episodeIds.forEach {
if (getVideoWatchState(it.toInt()) != watchState) {
editor.setKeyRaw(
@ -1685,14 +1686,13 @@ class ResultViewModel2 : ViewModel() {
}
val realRecommendations = ArrayList<SearchResponse>()
val apiNames = synchronized(apis) {
apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
}
val apiNames = apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
}
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
@ -2706,4 +2706,4 @@ class ResultViewModel2 : ViewModel() {
}
}
}
}
}

View file

@ -49,7 +49,7 @@ class SearchViewModel : ViewModel() {
private var suggestionJob: Job? = null
private var repos = synchronized(apis) { apis.map { APIRepository(it) } }
private var repos = apis.withLock { apis.map { APIRepository(it) } }
fun clearSearch() {
_searchResponse.postValue(Resource.Success(ExpandableSearchList(emptyList(), 0, false)))
@ -68,7 +68,7 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null
fun reloadRepos() {
repos = synchronized(apis) { apis.map { APIRepository(it) } }
repos = apis.withLock { apis.map { APIRepository(it) } }
}
fun searchAndCancel(

View file

@ -219,7 +219,7 @@ class SettingsGeneral : BasePreferenceFragmentCompat() {
}
fun showAdd() {
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
val providers = allProviders.distinctBy { it::class }.sortedBy { it.name }
activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" },
-1,

View file

@ -111,10 +111,10 @@ class SettingsProviders : BasePreferenceFragmentCompat() {
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
activity?.getApiProviderLangSettings()?.let { currentLangTags ->
val languagesTagName = synchronized(APIHolder.apis) {
listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) }
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
val languagesTagName = APIHolder.apis.withLock {
listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) }
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() }
}
val currentIndexList = currentLangTags.map { langTag ->

View file

@ -23,7 +23,7 @@ import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import me.xdrop.fuzzywuzzy.FuzzySearch
import com.lagradost.cloudstream3.utils.Levenshtein
import java.io.File
// String => repository url
@ -246,7 +246,7 @@ class PluginsViewModel : ViewModel() {
this.sortedBy { it.plugin.second.name }
} else {
this.sortedBy {
-FuzzySearch.partialRatio(
-Levenshtein.partialRatio(
it.plugin.second.name.lowercase(),
query.lowercase()
)

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -40,7 +40,7 @@ class TestViewModel : ViewModel() {
get() = scope != null
private var filter = ProviderFilter.All
private val providers = threadSafeListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
private val providers = atomicListOf<Pair<MainAPI, TestingUtils.TestResultProvider>>()
private var passed = 0
private var failed = 0
private var total = 0
@ -51,9 +51,9 @@ class TestViewModel : ViewModel() {
}
private fun postProviders() {
synchronized(providers) {
providers.withLock {
val filtered = when (filter) {
ProviderFilter.All -> providers
ProviderFilter.All -> providers.toList()
ProviderFilter.Passed -> providers.filter { it.second.success }
ProviderFilter.Failed -> providers.filter { !it.second.success }
}
@ -68,7 +68,7 @@ class TestViewModel : ViewModel() {
}
private fun addProvider(api: MainAPI, results: TestingUtils.TestResultProvider) {
synchronized(providers) {
providers.withLock {
val index = providers.indexOfFirst { it.first == api }
if (index == -1) {
providers.add(api to results)
@ -81,14 +81,14 @@ class TestViewModel : ViewModel() {
}
fun init() {
total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
total = APIHolder.allProviders.withLock { APIHolder.allProviders.size }
updateProgress()
}
fun startTest() {
scope = CoroutineScope(Dispatchers.Default)
val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
val apis = APIHolder.allProviders.withLock { APIHolder.allProviders.toTypedArray() }
total = apis.size
failed = 0
passed = 0

View file

@ -84,7 +84,7 @@ class SetupFragmentExtensions : BaseFragment<FragmentSetupExtensionsBinding>(
if (isSetup)
if (
// If any available languages
synchronized(apis) { apis.distinctBy { it.lang }.size > 1 }
apis.distinctBy { it.lang }.size > 1
) {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else {

View file

@ -36,10 +36,10 @@ class SetupFragmentProviderLanguage : BaseFragment<FragmentSetupProviderLanguage
val currentLangTags = ctx.getApiProviderLangSettings()
val languagesTagName = synchronized(APIHolder.apis) {
listOf( Pair(AllLanguagesName, getString(R.string.all_languages_preference)) ) +
val languagesTagName = APIHolder.apis.withLock {
listOf(Pair(AllLanguagesName, getString(R.string.all_languages_preference))) +
APIHolder.apis.map { Pair(it.lang, getNameNextToFlagEmoji(it.lang) ?: it.lang) }
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
.toSet().sortedBy { it.second.substringAfter("\u00a0").lowercase() } // name ignoring flag emoji
}
val currentIndexList = currentLangTags.map { langTag ->

View file

@ -369,28 +369,10 @@ object AppContextUtils {
}
fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
.map { it.name })
/*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key),
hashSet
)?.toHashSet() ?: hashSet
val list = HashSet<String>()
for (name in set) {
val api = getApiFromNameNull(name) ?: continue
if (activeLangs.contains(api.lang)) {
list.add(name)
}
}*/
//if (list.isEmpty()) return hashSet
//return list
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name })
return hashSet
}
@ -481,9 +463,7 @@ object AppContextUtils {
} ?: default
val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName)
val allApis = synchronized(apis) {
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
}
val allApis = apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
return if (currentPrefMedia.isEmpty()) {
allApis
} else {

View file

@ -10,7 +10,6 @@ import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import androidx.preference.PreferenceManager
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
@ -21,11 +20,12 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi.Companion.ANILIST_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.MALApi.Companion.MAL_CACHED_LIST
import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi.Companion.KITSU_CACHED_LIST
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.mapper
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.setupStream
@ -133,9 +133,7 @@ object BackupUtils {
)
@Suppress("UNCHECKED_CAST")
private fun getBackup(context: Context?): BackupFile? {
if (context == null) return null
private fun getBackup(context: Context): BackupFile {
val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() }
val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() }
@ -214,7 +212,7 @@ object BackupUtils {
fileStream = stream.openNew()
printStream = PrintWriter(fileStream)
printStream.print(mapper.writeValueAsString(backupFile))
printStream.print(backupFile.toJson())
showToast(
R.string.backup_success,
@ -259,8 +257,8 @@ object BackupUtils {
val input = activity.contentResolver.openInputStream(uri)
?: return@ioSafe
val restoredValue =
mapper.readValue<BackupFile>(input)
val text = input.bufferedReader().readText()
val restoredValue = parseJson<BackupFile>(text)
restore(
activity,

View file

@ -2,17 +2,16 @@ package com.lagradost.cloudstream3.utils
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKeyClass
import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKeyClass
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJsonLiteral
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import androidx.core.content.edit
/** Used to display metadata about downloads and resume watching */
const val DOWNLOAD_HEADER_CACHE = "download_header_cache"
@ -88,8 +87,18 @@ data class Editor(
}
object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
// Extensions shouldn't have really been using this version of it, but it seems
// some have. Since there has always been a very easy alternative, we won't
// need to deprecate it that long, and should be able to fully remove it
// once extensions at least use the other version.
@Deprecated(
"Please do not use the mapper version from DataStore. Preferably use methods from AppUtils " +
"to parse JSON. However, you can use the stable-API version of the mapper at " +
"com.lagradost.cloudstream3.mapper to access the mapper directly if necessary.",
level = DeprecationLevel.ERROR,
replaceWith = ReplaceWith("com.lagradost.cloudstream3.mapper"),
)
val mapper = com.lagradost.cloudstream3.mapper
private fun getPreferences(context: Context): SharedPreferences {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
@ -99,7 +108,6 @@ object DataStore {
return getPreferences(this)
}
fun getFolderName(folder: String, path: String): String {
return "${folder}/${path}"
}
@ -165,17 +173,17 @@ object DataStore {
fun <T> Context.setKey(path: String, value: T) {
try {
getSharedPrefs().edit {
putString(path, mapper.writeValueAsString(value))
putString(path, value?.toJsonLiteral())
}
} catch (e: Exception) {
logError(e)
}
}
fun <T> Context.getKey(path: String, valueType: Class<T>): T? {
fun <T : Any> Context.getKey(path: String, valueType: Class<T>): T? {
try {
val json: String = getSharedPrefs().getString(path, null) ?: return null
return json.toKotlinObject(valueType)
return parseJson(json, valueType.kotlin)
} catch (e: Exception) {
return null
}
@ -186,11 +194,11 @@ object DataStore {
}
inline fun <reified T : Any> String.toKotlinObject(): T {
return mapper.readValue(this, T::class.java)
return parseJson(this)
}
fun <T> String.toKotlinObject(valueType: Class<T>): T {
return mapper.readValue(this, valueType)
fun <T : Any> String.toKotlinObject(valueType: Class<T>): T {
return parseJson(this, valueType.kotlin)
}
// GET KEY GIVEN PATH AND DEFAULT VALUE, NULL IF ERROR
@ -214,4 +222,4 @@ object DataStore {
inline fun <reified T : Any> Context.getKey(folder: String, path: String, defVal: T?): T? {
return getKey(getFolderName(folder, path), defVal) ?: defVal
}
}
}

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.utils
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.util.Log
import android.widget.ImageView
@ -11,6 +12,7 @@ import coil3.EventListener
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import coil3.decode.BitmapFactoryDecoder
import coil3.disk.DiskCache
import coil3.dispose
import coil3.load
@ -22,82 +24,86 @@ import coil3.request.CachePolicy
import coil3.request.ErrorResult
import coil3.request.ImageRequest
import coil3.request.allowHardware
import coil3.request.bitmapConfig
import coil3.request.crossfade
import coil3.util.DebugLogger
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.network.buildDefaultClient
import okhttp3.HttpUrl
import okio.Path.Companion.toOkioPath
import java.io.File
import java.nio.ByteBuffer
object ImageLoader {
private const val TAG = "CoilImgLoader"
internal fun buildImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(context)
internal fun buildImageLoader(context: PlatformContext): ImageLoader {
val isBrokenHardware = hasPotentialBrokenHardware()
return ImageLoader.Builder(context)
.crossfade(200)
.allowHardware(SDK_INT >= 28) // SDK_INT >= 28, cant use hardware bitmaps for Palette Builder
.allowHardware(SDK_INT >= 28 && !isBrokenHardware)
.diskCachePolicy(CachePolicy.ENABLED)
.networkCachePolicy(CachePolicy.ENABLED)
.memoryCache {
MemoryCache.Builder().maxSizePercent(context, 0.1) // Use 10 % of the app's available memory for caching
MemoryCache.Builder().maxSizePercent(context, 0.1)//10 % of heap for mem-cache
.strongReferencesEnabled(false)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("cs3_image_cache").toOkioPath())
.maxSizeBytes(512L * 1024 * 1024) // 512 MB
.maxSizePercent(0.04) // Use 4 % of the device's storage space for disk caching
.maxSizePercent(0.04) // max 4% of storage for disk caching
.build()
}
/** Pass interceptors with care, unnecessary passing tokens to servers
or image hosting services causes unauthorized exceptions **/
.components { add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) })) }
.also {
it.setupCoilLogger()
Log.d(TAG, "buildImageLoader: Setting COIL Image Loader.")
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { buildDefaultClient(context) }))
if (isBrokenHardware) {
add(BitmapFactoryDecoder.Factory())
} // sw decoder
}
.apply {
if (isBrokenHardware) { // coil will auto choose optimal config on modern device
bitmapConfig(Bitmap.Config.ARGB_8888)
}
setupCoilLogger()
}
.build()
}
/** Use DebugLogger on debug builds which won't slow down release builds & use EventListener for
/** DebugLogger on debug builds which won't slow down release builds & use EventListener for
Errors on release builds. **/
internal fun ImageLoader.Builder.setupCoilLogger() {
if (BuildConfig.DEBUG) {
logger(DebugLogger())
Log.d(TAG, "setupCoilLogger: Activated DEBUG_LOGGER FOR COIL")
} else {
eventListener(object : EventListener() {
override fun onError(request: ImageRequest, result: ErrorResult) {
super.onError(request, result)
Log.e(TAG, "Error loading image: ${result.throwable}")
Log.e(TAG, "Image load error: ${result.throwable.message ?: result.throwable}")
Log.e(TAG, " URL: ${request.data}")
Log.e(TAG, " allowHardware: ${request.allowHardware}")
Log.e(TAG, " hardware: ${Build.HARDWARE}, board: ${Build.BOARD}")
}
})
Log.d(TAG, "setupCoilLogger: Activated EVENT_LISTENER FOR COIL")
}
}
/** we use coil's built in loader with our global synchronized instance, this way we achieve
latest and complete functionality as well as stability **/
/** coil's built in loader attached w/ global synchronized instance **/
private fun ImageView.loadImageInternal(
imageData: Any?,
headers: Map<String, String>? = null,
builder: ImageRequest.Builder.() -> Unit = {} // for placeholder, error & transformations
) {
// clear image to avoid loading & flickering issue at fast scrolling (e.g, an image recycler)
// clear image to avoid loading & flickering issue at fast scrolling (~recycler view/lazy column)
this.dispose()
if(imageData == null) return // Just in case
if (imageData == null) return
// setImageResource is better than coil3 on resources due to attr
if(imageData is Int) {
this.setImageResource(imageData)
return
if (imageData is Int) {
this.setImageResource(imageData); return
}
// Use Coil's built-in load method but with our custom module & a decent USER-AGENT always
// which can be overridden by extensions.
// headers can be overridden by extensions.
this.load(imageData, SingletonImageLoader.get(context)) {
this.httpHeaders(NetworkHeaders.Builder().also { headerBuilder ->
headerBuilder["User-Agent"] = USER_AGENT
@ -105,11 +111,22 @@ object ImageLoader {
headerBuilder[key] = value
}
}.build())
builder() // if passed
}
}
private fun hasPotentialBrokenHardware(): Boolean {
val hardware = Build.HARDWARE?.lowercase() ?: ""
val board = Build.BOARD?.lowercase() ?: ""
val model = Build.MODEL?.lowercase() ?: ""
val manufacturer = Build.MANUFACTURER?.lowercase() ?: ""
val allwinnerPatterns = listOf("sun50iw9", "h713", "allwinner", "sunxi")
val problematicModels =
listOf("hy320", "hy300", "a10plus", "magcubic", "sinoy", "android tv box")
return allwinnerPatterns.any { it in hardware || it in board || it in manufacturer } ||
problematicModels.any { it in model }
}
/** TYPE_SAFE_LOADERS **/
fun ImageView.loadImage(
imageData: UiImage?,
@ -138,12 +155,6 @@ object ImageLoader {
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
fun ImageView.loadImage(
imageData: HttpUrl?,
headers: Map<String, String>? = null,
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, headers = headers, builder = builder)
fun ImageView.loadImage(
imageData: File?,
builder: ImageRequest.Builder.() -> Unit = {}
@ -173,4 +184,4 @@ object ImageLoader {
imageData: ByteBuffer?,
builder: ImageRequest.Builder.() -> Unit = {}
) = loadImageInternal(imageData = imageData, builder = builder)
}
}

View file

@ -93,9 +93,9 @@ object InAppUpdater {
private suspend fun Activity.getReleaseUpdate(): Update {
val url = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
val response = parseJson<List<GithubRelease>>(
val response = parseJson<Array<GithubRelease>>(
app.get(url, headers = headers).text
)
).toList()
val versionRegex = Regex("""(.*?((\d+)\.(\d+)\.(\d+))\.apk)""")
val versionRegexLocal = Regex("""(.*?((\d+)\.(\d+)\.(\d+)).*)""")
@ -103,9 +103,7 @@ object InAppUpdater {
!rel.prerelease
}.sortedWith(compareBy { release ->
release.assets.firstOrNull { it.contentType == "application/vnd.android.package-archive" }?.name?.let { it1 ->
versionRegex.find(
it1
)?.groupValues?.let {
versionRegex.find(it1)?.groupValues?.let {
it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt()
}
}
@ -150,9 +148,9 @@ object InAppUpdater {
"https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/git/ref/tags/pre-release"
val releaseUrl = "https://api.github.com/repos/$GITHUB_USER_NAME/$GITHUB_REPO/releases"
val headers = mapOf("Accept" to "application/vnd.github.v3+json")
val response = parseJson<List<GithubRelease>>(
val response = parseJson<Array<GithubRelease>>(
app.get(releaseUrl, headers = headers).text
)
).toList()
val found = response.lastOrNull { rel ->
rel.prerelease || rel.tagName == "pre-release"

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.APIHolder.apis
//import com.lagradost.cloudstream3.animeproviders.AniflixProvider
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import java.util.concurrent.TimeUnit
object SyncUtil {
@ -71,7 +71,7 @@ object SyncUtil {
val url =
"https://raw.githubusercontent.com/MALSync/MAL-Sync-Backup/master/data/pages/$site/$slug.json"
val response = app.get(url, cacheTime = 1, cacheUnit = TimeUnit.DAYS).text
val mapped = parseJson<MalSyncPage?>(response)
val mapped = tryParseJson<MalSyncPage?>(response)
val overrideMal = mapped?.malId ?: mapped?.mal?.id ?: mapped?.anilist?.malId
val overrideAnilist = mapped?.aniId ?: mapped?.anilist?.id
@ -96,10 +96,8 @@ object SyncUtil {
.mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER
synchronized(apis) {
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
}
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
}
}
return current
@ -169,4 +167,4 @@ object SyncUtil {
@JsonProperty("updatedAt") val updatedAt: String?,
@JsonProperty("deletedAt") val deletedAt: String?
)
}
}

View file

@ -3,10 +3,10 @@ package com.lagradost.cloudstream3.utils
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.*
import org.junit.Assert
import kotlin.random.Random
object TestingUtils {
open class TestResult(val success: Boolean) {
companion object {
val Pass = TestResult(true)
@ -48,6 +48,10 @@ object TestingUtils {
messageLog.add(Message(LogLevel.Error, message))
}
}
private fun fail(message: String): Nothing = throw AssertionError(message)
private fun assertTrue(message: String, condition: Boolean) { if (!condition) fail(message) }
private fun assertNotNull(message: String, value: Any?) { if (value == null) fail(message) }
class TestResultList(val results: List<SearchResponse>) : TestResult(true)
class TestResultLoad(val extractorData: String, val shouldLoadLinks: Boolean) : TestResult(true)
@ -87,7 +91,7 @@ object TestingUtils {
} catch (e: Throwable) {
when (e) {
is NotImplementedError -> {
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
fail("Provider marked as hasMainPage, while in reality is has not been implemented")
}
is CancellationException -> {
@ -115,7 +119,7 @@ object TestingUtils {
api.search(query, 1)?.items?.takeIf { it.isNotEmpty() }
} catch (e: Throwable) {
if (e is NotImplementedError) {
Assert.fail("Provider has not implemented search()")
fail("Provider has not implemented search()")
} else if (e is CancellationException) {
throw e
}
@ -125,7 +129,7 @@ object TestingUtils {
}
return if (searchResults.isNullOrEmpty()) {
Assert.fail("Api ${api.name} did not return any search responses")
fail("Api ${api.name} did not return any search responses")
TestResult.Fail // Should not be reached
} else {
TestResultList(searchResults)
@ -216,7 +220,7 @@ object TestingUtils {
// return TestResult(validResults)
} catch (e: Throwable) {
if (e is NotImplementedError) {
Assert.fail("Provider has not implemented load()")
fail("Provider has not implemented load()")
}
throw e
}
@ -228,14 +232,14 @@ object TestingUtils {
url: String?,
logger: Logger
): TestResult {
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
assertNotNull("Api ${api.name} has invalid url on episode", url)
if (url == null) return TestResult.Fail // Should never trigger
var linksLoaded = 0
try {
val success = api.loadLinks(url, false, {}) { link ->
logger.log("Video loaded: ${link.name}")
Assert.assertTrue(
assertTrue(
"Api ${api.name} returns link with invalid url ${link.url}",
link.url.length > 4
)
@ -245,12 +249,12 @@ object TestingUtils {
logger.log("Links loaded: $linksLoaded")
return TestResult(linksLoaded > 0)
} else {
Assert.fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
fail("Api ${api.name} returns false on loadLinks() with $linksLoaded links loaded")
}
} catch (e: Throwable) {
when (e) {
is NotImplementedError -> {
Assert.fail("Provider has not implemented loadLinks()")
fail("Provider has not implemented loadLinks()")
}
else -> {
@ -276,7 +280,7 @@ object TestingUtils {
// Test Homepage
val homepage = testHomepage(api, logger)
Assert.assertTrue("Homepage failed to load", homepage.success)
assertTrue("Homepage failed to load", homepage.success)
val homePageList = (homepage as? TestResultList)?.results ?: emptyList()
// Test Search Results
@ -287,7 +291,7 @@ object TestingUtils {
listOf("over", "iron", "guy")).take(3)
val searchResults = testSearch(api, searchQueries, logger)
Assert.assertTrue("Failed to get search results", searchResults.success)
assertTrue("Failed to get search results", searchResults.success)
searchResults as TestResultList
// Test Load and LoadLinks
@ -321,4 +325,4 @@ object TestingUtils {
}
}
}
}
}

View file

@ -804,6 +804,7 @@ object VideoDownloadManager {
private suspend fun resolve(
startByte: Long,
endByte: Long?,
buffer: ByteArray,
callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit)
): Long = withContext(Dispatchers.IO) {
var currentByte: Long = startByte
@ -822,7 +823,6 @@ object VideoDownloadManager {
)
val requestStream = request.body.byteStream()
val buffer = ByteArray(bufferSize)
var read: Int
try {
@ -853,6 +853,7 @@ object VideoDownloadManager {
suspend fun resolveSafe(
index: Int,
retries: Int = 3,
buffer: ByteArray,
callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit)
): Boolean {
var start = chuckStartByte.getOrNull(index) ?: return false
@ -861,7 +862,7 @@ object VideoDownloadManager {
for (i in 0 until retries) {
try {
// in case
start = resolve(start, end, callback)
start = resolve(start, end, buffer, callback)
// no end defined, so we don't care exactly where it ended
if (end == null) return true
// we have download more or exactly what we needed
@ -1158,7 +1159,10 @@ object VideoDownloadManager {
}
}
// this will take up the first available job and resolve
// Reuse a download buffer to decrease unnecessary alloc
val buffer = ByteArray(items.bufferSize)
// This will take up the first available job and resolve
while (true) {
if (!isActive) return@launch
@ -1188,7 +1192,7 @@ object VideoDownloadManager {
// in case something has gone wrong set to failed if the fail is not caused by
// user cancellation
if (!items.resolveSafe(index, callback = callback)) {
if (!items.resolveSafe(index, buffer = buffer, callback = callback)) {
fileMutex.withLock {
if (metadata.type != DownloadType.IsStopped) {
metadata.type = DownloadType.IsFailed

View file

@ -0,0 +1,40 @@
package com.lagradost.cloudstream3.utils.serializers
import android.net.Uri
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Custom KSerializer for Android's [Uri] type.
*
* Uri is an Android platform type and cannot be annotated with @Serializable directly.
* Registering it in a SerializersModule globally would require a custom module passed to
* every Json instance, which adds hidden coupling. This serializer is also used sparingly
* across the codebase, so the overhead of a global registration isn't justified.
* Instead, we keep it explicit so that each usage site opts in intentionally and the
* serialization behavior remains visible.
*
* Usage:
*
* @Serializable
* data class MyData(
* @Serializable(with = UriSerializer::class)
* val uri: Uri,
* )
*/
object UriSerializer : KSerializer<Uri> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uri) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Uri {
return Uri.parse(decoder.decodeString())
}
}

View file

@ -11,6 +11,7 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -12,6 +12,7 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="680dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -11,6 +11,7 @@
android:id="@+id/player_metadata_scrim"
android:layout_width="640dp"
android:layout_height="match_parent"
android:layout_marginTop="-10dp"
android:background="@drawable/bg_player_metadata_scrim_netflix"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"

View file

@ -753,4 +753,7 @@
<item quantity="other">%d تنزيل قيد الانتظار</item>
</plurals>
<string name="show_player_metadata_overlay">عرض واجهة منبثقة للبيانات الوصفية للمشغِّل</string>
<string name="video_singular">مقطع</string>
<string name="skip_type_preview">استعراض</string>
<string name="player_is_live">البث قائم</string>
</resources>

View file

@ -252,7 +252,7 @@
<string name="update">Update</string>
<string name="watch_quality_pref">Bevorzugte Videoqualität (WLAN)</string>
<string name="limit_title">Videoplayertitel max. Zeichen</string>
<string name="limit_title_rez">Playerinformationen anzeigen</string>
<string name="limit_title_rez">Zeige Playerinformationen</string>
<string name="video_buffer_size_settings">Videopuffergröße</string>
<string name="video_buffer_length_settings">Videopufferlänge</string>
<string name="video_buffer_disk_settings">Video-Cache in Speicher</string>
@ -587,7 +587,7 @@
<string name="pref_category_security">Sicherheit</string>
<string name="pref_category_accounts">Konten</string>
<string name="open_downloaded_repo">Repository öffnen</string>
<string name="device_pin_url_message">Besuche<b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
<string name="device_pin_url_message">Besuche <b>%s</b> auf dem Smartphone oder Computer und gebe den obenstehenden Code ein</string>
<string name="device_pin_error_message">PIN-Code vom Gerät nicht abrufbar, versuche lokale Authentifizierung</string>
<string name="downloads_empty">Zur Zeit sind keine Downloads verfügbar.</string>
<string name="open_local_video">Lokales Video öffnen</string>
@ -712,8 +712,8 @@
<string name="extra_brightness_settings">Zusätzliche Helligkeit</string>
<string name="extra_brightness_settings_des">Aktiviere Helligkeitsfilter, wenn 100% Bildschirmhelligkeit überschritten ist</string>
<string name="extra_brightness_key">Erhöhte Helligkeit aktiviert</string>
<string name="show_cast_in_details">Cast-Panel zeigen</string>
<string name="video_info">Medieninfo</string>
<string name="show_cast_in_details">Zeige Cast-Panel</string>
<string name="video_info">Mediainfo</string>
<string name="source_name">Quellname</string>
<string name="download_all">Alle herunterladen</string>
<string name="download_episode_range">Möchtest du Episode %s herunter laden?</string>
@ -731,4 +731,8 @@
<string name="queue_empty_message">Es befinden sich keine Downloads in der Warteschlange.</string>
<string name="source_priority">Quellpriorität</string>
<string name="source_priority_help">Entscheide, wie Videoquellen im Player sortiert werden sollen</string>
<string name="show_player_metadata_overlay">Zeige Player-Metadaten</string>
<string name="video_singular">Video</string>
<string name="skip_type_preview">Vorschau</string>
<string name="player_is_live">Live</string>
</resources>

View file

@ -244,7 +244,7 @@
<string name="quality_tc">TC</string>
<string name="subscription_new">Претплатен на %s</string>
<string name="pref_category_subtitles">Преводи</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream 3 не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="download_all_plugins_from_repo">Предупредување: CloudStream не презема никаква одговорност за користење на екстензии од трети страни и не обезбедува никаква поддршка за нив!</string>
<string name="backup_failed">Недостасуваат дозволи за складирање. Обиди се повторно.</string>
<string name="sort_save">Зачувај</string>
<string name="player_load_subtitles">Вчитај од датотека</string>
@ -445,7 +445,7 @@
<string name="backup_failed_error_format">Грешка при правење резервна копија на %s</string>
<string name="pref_filter_search_quality">Сокриј го избраниот квалитет на видеото во резултатите од пребарувањето</string>
<string name="apk_installer_settings_des">Некои уреди не го поддржуваат новиот инсталатор на пакети. Испробај ја легаси(старата) опција, ако ажурирањата не се инсталираат.</string>
<string name="limit_title_rez">Резолуција на видео плеер</string>
<string name="limit_title_rez">Прикажи информации за плеерот</string>
<string name="video_buffer_size_settings">Големина на видео баферот</string>
<string name="pref_category_player_layout">Распоред</string>
<string name="pref_category_defaults">Стандардно</string>
@ -705,4 +705,37 @@
<string name="top_center">Горе во центар</string>
<string name="top_right">Горе на десно</string>
<string name="play_full_series_button">Пушти ја целата серија</string>
<string name="download_queue">Редица за преземање</string>
<string name="queue_empty_message">Моментално нема преземања во редицата.</string>
<string name="extra_brightness_settings">Дополнителна осветленост</string>
<string name="extra_brightness_settings_des">Овозможи филтер за осветленост кога ќе се надмине 100% осветленост на екранот</string>
<string name="extra_brightness_key">овозможенаополнителна_осветленост</string>
<string name="search_suggestions">Предлози за пребарување</string>
<string name="search_suggestions_des">Прикажувај предлози за пребарување додека пишуваш</string>
<string name="clear_suggestions">Исчисти предлози</string>
<string name="show_player_metadata_overlay">Прикажи преклоп со метаподатоци на плеерот</string>
<string name="show_cast_in_details">Прикажи панел за емитување</string>
<string name="install_prerelease">Инсталирај предиздавачка верзија</string>
<string name="prerelease_already_installed">Предиздавачката верзија е веќе инсталирана.</string>
<string name="prerelease_install_failed">Неуспешна инсталација на предиздавачката верзија.</string>
<string name="video_singular">Видео</string>
<string name="show_episode_text">Текст на епизода</string>
<string name="video_info">Информации за медиумот</string>
<string name="skip_type_preview">Преглед</string>
<string name="source_priority">Приоритет на извор</string>
<string name="source_priority_help">Одреди како ќе се подредуваат видео изворите во плеерот</string>
<string name="source_name">Име на изворот</string>
<string name="download_all">Преземи сѐ</string>
<string name="cancel_all">Откажи сѐ</string>
<string name="download_episode_range">Дали сакате да ја преземете епизодата %s?</string>
<string name="cancel_queue_message">Дали сакате да ги откажете сите преземања во редицата?</string>
<plurals name="downloads_active">
<item quantity="one">%d активно преземање</item>
<item quantity="other">%d активни преземања</item>
</plurals>
<plurals name="downloads_queued">
<item quantity="one">%d преземање во редицата</item>
<item quantity="other">%d преземања во редицата</item>
</plurals>
<string name="player_is_live">Во живо</string>
</resources>

View file

@ -25,8 +25,8 @@
<string name="next_episode_format" formatted="true">Episod %d akan disiarkan dalam</string>
<string name="cast_format" formatted="true">Pelakon:%s</string>
<string name="safe_mode_title">Mod Selamat Hidup</string>
<string name="next_episode_time_day_format" formatted="true">%1$dd %2$dh %3$dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dh %2$dm</string>
<string name="next_episode_time_day_format" formatted="true">%1$dh %2$dj %3$dm</string>
<string name="next_episode_time_hour_format" formatted="true">%1$dj %2$dm</string>
<string name="next_episode_time_min_format" formatted="true">%dm</string>
<string name="episode_poster_img_des">Poster Episod</string>
<string name="home_main_poster_img_des">Poster Utama</string>
@ -485,7 +485,7 @@
<string name="category_updates">Kemaskini dan sandaran</string>
<string name="double_tap_to_seek_settings">Ketik dua kali untuk mencari</string>
<string name="use_system_brightness_settings_des">Gunakan kecerahan sistem dalam pemain apl dan bukannya tindanan gelap</string>
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$dh %2$dm %3$ds</string>
<string name="download_time_left_hour_min_sec_format" formatted="true">%1$dj %2$dm %3$ds</string>
<string name="download_time_left_min_sec_format" formatted="true">%1$dm %2$ds</string>
<string name="download_time_left_sec_format" formatted="true">%1$ds</string>
<string name="speech_recognition_unavailable">Pengecaman pertuturan tidak tersedia</string>

View file

@ -661,4 +661,20 @@
<string name="clipboard_permission_error">Fout bij toegang tot het Klembord, Probeer het opnieuw.</string>
<string name="clipboard_unknown_error">Fout bij het kopiëren. Kopieer alsjeblieft de logcat en neem contact op met de app-ondersteuning.</string>
<string name="dismiss">Afwijzen</string>
<string name="video_singular">Video</string>
<string name="skip_type_preview">Voorbeeld</string>
<string name="source_priority">Bron Prioriteit</string>
<string name="source_priority_help">Bepaal hoe de videobronnen worden gesorteerd in de speler</string>
<string name="biometric_authentication_title">Ontgrendel CloudStream</string>
<string name="biometric_setting">Versleutel met Biometrie</string>
<string name="reset_btn">Reset</string>
<string name="sort_release_date_new">verschijningsdatum (Nieuw naar Oud)</string>
<string name="sort_release_date_old">verschijningsdatum (Oud naar Nieuw)</string>
<string name="hide_player_control_names">Verberg de namen van de besturingselementen van de speler</string>
<string name="no_subtitles_loaded">Ondertiteling nog niet geladen</string>
<string name="backup_path_title">Back-up folder locatie</string>
<string name="custom">Aangepast</string>
<string name="confirm_before_exiting_title">Bevestig voor afsluiten</string>
<string name="confirm_before_exiting_desc">Toon dialoogvenster voordat de app wordt afgesloten</string>
<string name="subs_edge_size">Randgrote</string>
</resources>

View file

@ -188,4 +188,14 @@
<string name="picture_in_picture">Bilde i bilde</string>
<string name="continue_watching">Fortsett å sjå</string>
<string name="reload_error">Prøv tilkopling på nytt…</string>
<string name="next_season_episode_format" formatted="true">Sesong %1$d Episode %2$d blir sleppt om</string>
<string name="play_from_beginning_img_des">Spel av frå start</string>
<string name="download_queue">Nedlastingskø</string>
<string name="speech_recognition_unavailable">Semmegjenkjenning er ikkje tilgjengeleg</string>
<string name="begin_speaking">Snakk no…</string>
<string name="browser">Nettlesar</string>
<string name="type_dropped">Fjerna</string>
<string name="play_torrent_button">Strøm Torrent</string>
<string name="play_full_series_button">Spel heile serien</string>
<string name="torrent_info">Denne filmen er ein Torrent, som betyr at bruken din kan bli spora\nSett deg inn i bruk av Torrent-resursar før du fortsetter.</string>
</resources>

View file

@ -616,7 +616,7 @@
<string name="play_from_beginning_img_des">Reproduzir do começo</string>
<string name="test_warning">Reprovou alguns testes</string>
<string name="delete_plugin">Excluir plugin</string>
<string name="downloads_empty">Você não baixou nada :/</string>
<string name="downloads_empty">Atualmente não há downloads disponíveis.</string>
<string name="hide_player_control_names">Ocultar os nomes dos controles do player</string>
<string name="open_local_video">Abrir arquivo de vídeo</string>
<string name="sort_release_date_new">Data de lançamento (do novo ao antigo)</string>
@ -736,7 +736,7 @@
<string name="source_name">Nome da fonte</string>
<string name="download_all">Baixar tudo</string>
<string name="cancel_all">Cancelar tudo</string>
<string name="download_episode_range">Você deseja baixar o episódio%s</string>
<string name="download_episode_range">Você deseja baixar o episódio%s?</string>
<string name="cancel_queue_message">Você gostaria de cancelar todos os downloads da fila?</string>
<plurals name="downloads_active">
<item quantity="one">%ddownload ativo</item>
@ -748,7 +748,7 @@
<item quantity="many">%d downloads na sequência</item>
<item quantity="other">%d downloads na sequência</item>
</plurals>
<string name="show_player_metadata_overlay">Exibir sobreposição de metadados do player</string>
<string name="show_player_metadata_overlay">Mostrar sobreposição de metadados do reprodutor</string>
<string name="video_singular">Vídeo</string>
<string name="skip_type_preview">Visualização</string>
<string name="player_is_live">Ao vivo</string>

View file

@ -61,7 +61,7 @@
<string name="stream">Transmitir</string>
<string name="error_loading_links_toast">Erro a Carregar Links</string>
<string name="download_storage_text">Armazenamento Interno</string>
<string name="app_dubbed_text">Dob</string>
<string name="app_dubbed_text">Dub</string>
<string name="app_subbed_text">Leg</string>
<string name="popup_delete_file">Eliminar Ficheiro</string>
<string name="popup_play_file">Reproduzir Ficheiro</string>
@ -100,7 +100,7 @@
<string name="subs_import_text" formatted="true">Importar fontes colocando em %s</string>
<string name="continue_watching">Continuar a Assistir</string>
<string name="action_remove_watching">Remover</string>
<string name="action_open_watching">Mais info</string>
<string name="action_open_watching">Mais informações</string>
<string name="vpn_might_be_needed">Uma VPN pode ser necessária para que este fornecedor funcione corretamente</string>
<string name="vpn_torrent">Este fornecedor é um torrent, uma VPN é recomendada</string>
<string name="provider_info_meta">Metadados não são oferecidos pelo site, o carregamento do vídeo irá falhar se ele não existir no site.</string>
@ -142,7 +142,7 @@
<string name="search">Procurar</string>
<string name="category_account">Contas e segurança</string>
<string name="category_updates">Atualizações e cópias de segurança</string>
<string name="settings_info">Info</string>
<string name="settings_info">Informações</string>
<string name="advanced_search">Procura Avançada</string>
<string name="advanced_search_des">Mostra resultados separados por fornecedor</string>
<string name="show_fillers_settings">Mostrar episódios de enchimento para anime</string>
@ -318,9 +318,9 @@
<string name="player_load_subtitles">Carregar de arquivo</string>
<string name="player_load_subtitles_online">Carregar da Internet</string>
<string name="downloaded_file">Arquivo baixado</string>
<string name="actor_main">Protagonista</string>
<string name="actor_supporting">Coadjuvante</string>
<string name="actor_background">Figurante</string>
<string name="actor_main">Principal</string>
<string name="actor_supporting">Suporte</string>
<string name="actor_background">Plano de fundo</string>
<string name="home_random">Aleatório</string>
<string name="coming_soon">Em breve…</string>
<string name="poster_image">Imagem de Poster</string>
@ -523,7 +523,7 @@
<string name="no_repository_found_error">Repositório não encontrado, verifique o URL e tente a VPN</string>
<string name="already_voted">Você já votou</string>
<string name="action_unsubscribe">Cancelar Inscrição</string>
<string name="action_subscribe">Subscrever</string>
<string name="action_subscribe">Inscrever-se</string>
<string name="favorites_list_name">Favoritos</string>
<string name="links_reloaded_toast">A recarregar links</string>
<string name="backup_frequency">Frequência de Backup</string>
@ -686,7 +686,7 @@
<string name="edit_profile_image_success">Imagem Atualizada com Sucesso</string>
<string name="action_mark_watched_up_to_this_episode">Marcar como assistido o episódio</string>
<string name="action_remove_mark_watched_up_to_this_episode">Removar marcação de assistido até esse episódio</string>
<string name="action_reload">Recarregado</string>
<string name="action_reload">Recarregar</string>
<string name="reload_provider">Provedor de Recarregamento</string>
<string name="episode_action_play_mirror">Reproduzir do servidor alternativo</string>"
<string name="name">Nome</string>
@ -733,4 +733,8 @@
<item quantity="many">%d downloads na fila</item>
<item quantity="other">%d downloads na fila</item>
</plurals>
<string name="show_player_metadata_overlay">Mostrar sobreposição de metadados do player</string>
<string name="video_singular">Vídeo</string>
<string name="skip_type_preview">Pré-visualização</string>
<string name="player_is_live">Ao Vivo</string>
</resources>

View file

@ -735,4 +735,8 @@
<string name="cancel_all">Отменить всё</string>
<string name="download_episode_range">Вы хотите загрузить эпизод %s?</string>
<string name="cancel_queue_message">Вы хотите отменить всё запланированные загрузки?</string>
<string name="show_player_metadata_overlay">Показывать наложения метаданных проигрывателя</string>
<string name="video_singular">Видео</string>
<string name="skip_type_preview">Предпросмотр</string>
<string name="player_is_live">Прямой эфир</string>
</resources>

View file

@ -166,7 +166,7 @@
<string name="app_language">Ngôn ngữ ứng dụng</string>
<string name="no_chromecast_support_toast">Nguồn phim này chưa hỗ trợ Chromecast</string>
<string name="no_links_found_toast">Không tìm thấy liên kết</string>
<string name="copy_link_toast">Đã sao chép liên kết vào b nhớ tạm</string>
<string name="copy_link_toast">Đã sao chép liên kết vào bảng nhớ tạm</string>
<string name="play_episode_toast">Phát Tập phim</string>
<string name="subs_default_reset_toast">Đặt lại giá trị mặc định</string>
<string name="season">Mùa</string>
@ -254,7 +254,7 @@
<string name="update">Cập nhật</string>
<string name="watch_quality_pref">Chất lượng xem ưu tiên (WiFi)</string>
<string name="limit_title">Số ký tự tối đa tiêu đề trình phát</string>
<string name="limit_title_rez">Hiện thông tin trình phát</string>
<string name="limit_title_rez">Hiển thị thông tin trình phát</string>
<string name="video_buffer_size_settings">Kích thước bộ nhớ đệm video</string>
<string name="video_buffer_length_settings">Thời lượng bộ nhớ đệm</string>
<string name="video_buffer_disk_settings">Bộ nhớ đệm video trên thiết bị</string>
@ -417,7 +417,7 @@
<string name="tracks">Âm thanh &amp; video</string>
<string name="audio_tracks">Âm thanh</string>
<string name="video_tracks">Video</string>
<string name="apply_on_restart">Khởi động lại ứng dụng để thấy câc thay đổi.</string>
<string name="apply_on_restart">Khởi động lại ứng dụng để thấy các thay đổi.</string>
<string name="safe_mode_title">Chế độ an toàn được bật</string>
<string name="safe_mode_description">Tất cả tiện ích mở rộng đã được tắt do ứng dụng bị ngừng bất thường để giúp bạn tìm ra vấn đề gây lỗi.</string>
<string name="safe_mode_crash_info">Xem thông tin sự cố</string>
@ -469,8 +469,8 @@
<string name="skip_type_credits">Danh đề</string>
<string name="skip_type_intro">Giới thiệu</string>
<string name="clear_history">Xoá lịch sử</string>
<string name="enable_skip_op_from_database_des">Hiện các popup bỏ qua cho mở đầu/kết thúc</string>
<string name="clipboard_too_large">Văn bản quá dài. Không thể lưu vào b nhớ tạm.</string>
<string name="enable_skip_op_from_database_des">Hiển thị cửa sổ bật lên của bỏ qua giới thiệu cho mở đầu/kết thúc</string>
<string name="clipboard_too_large">Văn bản quá dài. Không thể lưu vào bảng nhớ tạm.</string>
<string name="action_remove_from_watched">Xoá khỏi đã xem</string>
<string name="confirm_exit_dialog">Bạn có chắc muốn thoát?</string>
<string name="yes"></string>
@ -575,7 +575,7 @@
<string name="auto_rotate_video_desc">Bật tự động xoay màn hình theo hướng của video</string>
<string name="auto_rotate_video">Tự động xoay</string>
<string name="toast_copied">đã sao chép!</string>
<string name="clipboard_permission_error">Lỗi truy cập B nhớ tạm, Vui lòng thử lại.</string>
<string name="clipboard_permission_error">Lỗi truy cập Bảng nhớ tạm, Vui lòng thử lại.</string>
<string name="clipboard_unknown_error">Lỗi sao chép, Vui lòng sao chép logcat và liên hệ hỗ trợ ứng dụng.</string>
<string name="favorite">Yêu thích</string>
<string name="ok">OK</string>
@ -635,7 +635,7 @@
<string name="preview_seekbar">Xem trước trên thanh tua</string>
<string name="no_subtitles_loaded">Chưa tải phụ đề nào</string>
<string name="confirm_before_exiting_title">Xác nhận trước khi thoát</string>
<string name="confirm_before_exiting_desc">Hiện hộp thoại xác nhận trước khi thoát ứng dụng</string>
<string name="confirm_before_exiting_desc">Hiển thị hộp thoại xác nhận trước khi thoát ứng dụng</string>
<string name="dont_show">Không hiển thị</string>
<string name="show">Hiển thị</string>
<string name="backup_path_title">Vị trí thư mục sao lưu</string>
@ -644,7 +644,7 @@
<string name="torrent_info">Video này là Torrent, điều này có nghĩa là hoạt động video của bạn có thể được theo dõi.\nHãy đảm bảo rằng bạn hiểu về Torrent trước khi tiếp tục.</string>
<string name="encoding_error">Lỗi mã hóa</string>
<string name="software_decoding_desc">Giải mã phần mềm cho phép phát các tệp video không được thiết bị của bạn hỗ trợ, nhưng có thể gây ra phản hồi chậm hoặc phát lại không ổn định ở độ phân giải cao.</string>
<string name="software_decoding">Bộ giải mã ứng dụng</string>
<string name="software_decoding">Giải mã phần mềm</string>
<string name="torrent_not_accepted">Khởi động lại ứng dụng và chấp nhận cửa sổ bật lên của Stream Torrent để tiếp tục.</string>
<string name="torrent_preferred_media">Kích hoạt torrent trong Cài đặt/Nguồn phim/Thể loại ưu tiên</string>
<string name="player_load_one_subtitle_online">Tải phụ đề đầu tiên có sẵn</string>
@ -731,7 +731,7 @@
<string name="source_name">Tên nguồn</string>
<string name="download_queue">Hàng đợi tải xuống</string>
<string name="queue_empty_message">Không có tải xuống đang chờ nào.</string>
<string name="source_priority_help">Quyết định cách sắp xếp các nguồn video trong trình phát.</string>
<string name="source_priority_help">Quyết định cách sắp xếp các nguồn video trong trình phát</string>
<string name="source_priority">Ưu tiên nguồn</string>
<string name="download_all">Tải xuống tất cả</string>
<string name="cancel_all">Hủy tất cả</string>

View file

@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.dokka) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.serialization) apply false
}
allprojects {

View file

@ -1 +1 @@
بث وتحميل الأفلام, الأنمي, والمسلسلات التلفزيونية.
بث وتحميل الأفلام, المسلسلات التلفزيونية والأنمي.

View file

@ -1 +1 @@
Transmita e transfira filmes, séries de TV e anime.
Transmita e descarga filmes, séries de TV e anime.

View file

@ -6,8 +6,8 @@ androidGradlePlugin = "9.1.1"
animeDb = "1.0.2"
annotation = "1.10.0"
appcompat = "1.7.1"
biometric = "1.4.0-alpha06"
buildkonfigGradlePlugin = "0.18.0"
biometric = "1.4.0-alpha07"
buildkonfigGradlePlugin = "0.21.2"
coil = { strictly = "3.3.0" } # Later versions require jvmTarget 11 or later
colorpicker = "6b46b49"
conscryptAndroid = { strictly = "2.5.2" } # 2.5.3 crashes everything
@ -17,22 +17,26 @@ desugar_jdk_libs_nio = "2.1.5"
dokkaGradlePlugin = "2.2.0"
espressoCore = "3.7.0"
fragmentKtx = "1.8.9"
fuzzywuzzy = "1.4.0"
instancioCore = "5.6.0"
jacksonModuleKotlin = { strictly = "2.13.1" } # Later versions don't support minSdk <26 (Crashes on Android TV's and FireSticks)
json = "20251224"
json = "20260522"
jsoup = "1.22.1"
junit = "4.13.2"
junitKtx = "1.3.0"
junitVersion = "1.3.0"
juniversalchardet = "2.5.0"
kotlinGradlePlugin = "2.3.20"
kotlinxAtomicfu = "0.33.0"
kotlinxCollectionsImmutable = "0.4.0"
kotlinxCoroutinesCore = "1.10.2"
kotlinxCoroutinesCore = "1.11.0"
kotlinxDatetime = "0.8.0"
kotlinxSerializationJson = "1.11.0"
ktor = "3.5.0"
lifecycleKtx = "2.10.0"
material = "1.14.0-beta01"
material = "1.14.0"
media3 = "1.9.3"
navigationKtx = "2.9.7"
newpipeextractor = "v0.26.0"
navigationKtx = "2.9.8"
newpipeextractor = "v0.26.3"
nextlibMedia3 = "1.9.3-0.12.0"
nicehttp = "0.4.18"
overlappingpanels = "0.1.5"
@ -56,6 +60,9 @@ minSdk = "23"
compileSdk = "36"
targetSdk = "36"
versionCode = "68"
versionName = "4.7.0"
[libraries]
activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
anime-db = { module = "com.github.recloudstream:anime-db", version.ref = "animeDb" }
@ -74,15 +81,20 @@ desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", vers
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
ext-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" }
fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
fuzzywuzzy = { module = "me.xdrop:fuzzywuzzy", version.ref = "fuzzywuzzy" }
instancio-core = { group = "org.instancio", name = "instancio-core", version.ref = "instancioCore" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
json = { module = "org.json:json", version.ref = "json" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { module = "junit:junit", version.ref = "junit" }
junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitKtx" }
juniversalchardet = { module = "com.github.albfernandez:juniversalchardet", version.ref = "juniversalchardet" }
kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlinGradlePlugin" }
kotlinx-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "kotlinxAtomicfu" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-http = { module = "io.ktor:ktor-http", version.ref = "ktor" }
lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycleKtx" }
lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleKtx" }
material = { module = "com.google.android.material:material", version.ref = "material" }
@ -125,6 +137,7 @@ buildkonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildkonfigG
dokka = { id = "org.jetbrains.dokka", version.ref = "dokkaGradlePlugin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm" , version.ref = "kotlinGradlePlugin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlinGradlePlugin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinGradlePlugin" }
[bundles]
coil = ["coil", "coil-network-okhttp"]

View file

@ -12,6 +12,7 @@ plugins {
alias(libs.plugins.android.multiplatform.library)
alias(libs.plugins.buildkonfig)
alias(libs.plugins.dokka)
alias(libs.plugins.kotlin.serialization)
}
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
@ -56,12 +57,31 @@ kotlin {
implementation(libs.annotation) // Annotations
implementation(libs.nicehttp) // HTTP Lib
implementation(libs.jackson.module.kotlin) // JSON Parser
implementation(libs.kotlinx.atomicfu)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.fuzzywuzzy) // Match Extractors
implementation(libs.kotlinx.datetime)
implementation(libs.kotlinx.serialization.json) // JSON Parser
implementation(libs.ktor.http)
implementation(libs.jsoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript
implementation(libs.newpipeextractor)
implementation(libs.tmdb.java) // TMDB API v3 Wrapper Made with RetroFit
// Deprecated; will be removed once extensions have time to migrate from using it
implementation("me.xdrop:fuzzywuzzy:1.4.0")
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
// We will eventually add a new jvmCommonMain source set
// for things shared between Android and JVM.
androidMain.dependencies {
implementation(libs.newpipeextractor)
}
jvmMain.dependencies {
implementation(libs.newpipeextractor)
}
}
}
@ -84,6 +104,11 @@ buildkonfig {
"MDL_API_KEY",
(System.getenv("MDL_API_KEY") ?: localProperties["mdl.key"]).toString()
)
buildConfigField(
FieldSpec.Type.STRING,
"TRAKT_CLIENT_ID", (System.getenv("TRAKT_CLIENT_ID") ?: localProperties["trakt.id"]).toString()
)
}
}

View file

@ -0,0 +1,105 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.newAudioFile
import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.newExtractorLink
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamType
actual open class YoutubeExtractor actual constructor() : ExtractorApi() {
actual override val mainUrl = "https://www.youtube.com"
actual override val name = "YouTube"
actual override val requiresReferer = false
actual override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
) {
val videoId = extractYouTubeId(url)
val watchUrl = "$mainUrl/watch?v=$videoId"
val info = StreamInfo.getInfo(watchUrl)
val isLive = info.streamType == StreamType.LIVE_STREAM
|| info.streamType == StreamType.AUDIO_LIVE_STREAM
|| info.streamType == StreamType.POST_LIVE_STREAM
|| info.streamType == StreamType.POST_LIVE_AUDIO_STREAM
if (isLive && info.hlsUrl != null) {
callback(
newExtractorLink(
source = name,
name = "YouTube Live",
url = info.hlsUrl
) {
type = ExtractorLinkType.M3U8
}
)
} else {
processVideo(info, subtitleCallback, callback)
}
}
private suspend fun processVideo(
info: StreamInfo,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
): Boolean {
val videoStreams = info.videoOnlyStreams.orEmpty()
if (videoStreams.isEmpty()) return false
val audioStreams = info.audioStreams.orEmpty()
videoStreams.forEach { video ->
callback(
newExtractorLink(
source = name,
name = "YouTube ${normalizeCodec(video.codec)}",
url = video.content
) {
quality = video.height
audioTracks = audioStreams.map { newAudioFile(it.content) }
}
)
}
info.subtitles.forEach { subtitle ->
subtitleCallback(
newSubtitleFile(
lang = subtitle.displayLanguageName
?: subtitle.languageTag
?: "Unknown",
url = subtitle.content
)
)
}
return true
}
private fun extractYouTubeId(url: String): String {
val regex = Regex(
"(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})"
)
return regex.find(url)?.groupValues?.get(1)
?: throw IllegalArgumentException("Invalid YouTube URL: $url")
}
private fun normalizeCodec(codec: String?): String {
if (codec.isNullOrBlank()) return ""
val c = codec.lowercase()
return when {
c.startsWith("av01") -> "AV1"
c.startsWith("vp9") -> "VP9"
c.startsWith("avc1") || c.startsWith("h264") -> "H264"
c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265"
else -> codec.substringBefore('.').uppercase()
}
}
}

View file

@ -10,17 +10,18 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.nicehttp.requestCreator
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import java.net.URI
/**
* When used as Interceptor additionalUrls cannot be returned, use WebViewResolver(...).resolveUsingWebView(...)
@ -119,7 +120,7 @@ actual class WebViewResolver actual constructor(
}
var fixedRequest: Request? = null
val extraRequestList = threadSafeListOf<Request>()
val extraRequestList = atomicListOf<Request>()
main {
try {
@ -211,7 +212,7 @@ actual class WebViewResolver actual constructor(
* */
return@runBlocking try {
when {
blacklistedFiles.any { URI(webViewUrl).path.contains(it) } || webViewUrl.endsWith(
blacklistedFiles.any { Url(webViewUrl).encodedPath.decodeURLPart().contains(it) } || webViewUrl.endsWith(
"/favicon.ico"
) -> WebResourceResponse(
"image/png",

View file

@ -17,21 +17,34 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF
import com.lagradost.nicehttp.RequestBodyTypes
import io.ktor.http.Url
import io.ktor.http.URLBuilder
import io.ktor.http.encodedPath
import io.ktor.http.takeFrom
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import java.net.URI
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.format.DateTimeComponents
import kotlinx.datetime.format.FormatStringsInDatetimeFormats
import kotlinx.datetime.format.byUnicodePattern
import kotlinx.datetime.format.char
import kotlinx.datetime.format.parse
import kotlinx.datetime.toInstant
import kotlinx.serialization.json.Json
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.absoluteValue
import kotlin.math.roundToInt
import kotlin.time.Clock
import kotlin.time.Instant
/**
* API available only on prerelease builds.
@ -74,20 +87,27 @@ const val USER_AGENT =
class ErrorLoadingException(message: String? = null) : Exception(message)
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
@Prerelease
val json = Json {
encodeDefaults = true
explicitNulls = false
ignoreUnknownKeys = true
}
val mapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
object APIHolder {
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
val unixTimeMS: Long
get() = System.currentTimeMillis()
get() = Clock.System.now().toEpochMilliseconds()
val unixTime: Long
get() = unixTimeMS / 1000L
// ConcurrentModificationException is possible!!!
val allProviders = threadSafeListOf<MainAPI>()
val allProviders = atomicListOf<MainAPI>()
fun initAll() {
synchronized(allProviders) {
allProviders.withLock {
for (api in allProviders) {
api.init()
}
@ -97,28 +117,28 @@ object APIHolder {
/** String extension function to Capitalize first char of string.*/
fun String.capitalize(): String {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
}
var apis: List<MainAPI> = threadSafeListOf()
var apis: AtomicList<MainAPI> = atomicListOf()
var apiMap: Map<String, Int>? = null
fun addPluginMapping(plugin: MainAPI) {
synchronized(apis) {
apis.withLock {
apis = apis + plugin
}
initMap(true)
}
fun removePluginMapping(plugin: MainAPI) {
synchronized(apis) {
apis.withLock {
apis = apis.filter { it != plugin }
}
initMap(true)
}
private fun initMap(forcedUpdate: Boolean = false) {
synchronized(apis) {
apis.withLock {
if (apiMap == null || forcedUpdate)
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
}
@ -126,24 +146,21 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null
synchronized(allProviders) {
return allProviders.withLock {
initMap()
synchronized(apis) {
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
apis.withLock {
apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName }
?: allProviders.firstOrNull { it.name == apiName }
}
}
}
fun getApiFromUrlNull(url: String?): MainAPI? {
if (url == null) return null
synchronized(allProviders) {
allProviders.forEach { api ->
if (url.startsWith(api.mainUrl)) return api
}
return allProviders.withLock {
allProviders.firstOrNull { url.startsWith(it.mainUrl) }
}
return null
}
/**
@ -161,9 +178,9 @@ object APIHolder {
// To get the key
suspend fun getCaptchaToken(url: String, key: String, referer: String? = null): String? {
try {
val uri = URI.create(url)
val _url = Url(url)
val domain = base64Encode(
(uri.scheme + "://" + uri.host + ":443").encodeToByteArray(),
(_url.protocol.name + "://" + _url.host + ":443").encodeToByteArray(),
).replace("\n", "").replace("=", ".")
val vToken =
@ -472,7 +489,7 @@ abstract class MainAPI {
}
fun init() {
overrideData?.get(this.javaClass.simpleName)?.let { data ->
overrideData?.get(this::class.simpleName)?.let { data ->
overrideWithNewData(data)
}
}
@ -686,17 +703,22 @@ abstract class MainAPI {
}
}
/** Might need a different implementation for desktop*/
fun base64Decode(string: String): String {
return String(base64DecodeArray(string), Charsets.ISO_8859_1)
// ISO-8859-1 decoding: each byte maps directly to its Unicode code point (0-255),
// so we mask each byte to unsigned and convert to the corresponding Char manually.
// decodeToString() can't be used here as it assumes UTF-8.
val bytes = base64DecodeArray(string)
return buildString(bytes.size) {
for (b in bytes) {
append((b.toInt() and 0xFF).toChar())
}
}
}
@OptIn(ExperimentalEncodingApi::class)
fun base64DecodeArray(string: String): ByteArray {
return Base64.decode(string)
}
@OptIn(ExperimentalEncodingApi::class)
fun base64Encode(array: ByteArray): String {
return Base64.encode(array)
}
@ -1308,23 +1330,23 @@ fun getQualityFromString(string: String?): SearchQuality? {
* ```
*/
fun MainAPI.updateUrl(url: String): String {
try {
val original = URI(url)
val updated = URI(mainUrl)
return try {
val original = Url(url)
val updated = Url(mainUrl)
// URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment)
return URI(
updated.scheme,
original.userInfo,
updated.host,
updated.port,
original.path,
original.query,
original.fragment
).toString()
URLBuilder().apply {
takeFrom(updated)
user = original.user
password = original.password
encodedPath = original.encodedPath
fragment = original.fragment
parameters.clear()
parameters.appendAll(original.parameters)
}.buildString()
} catch (t: Throwable) {
logError(t)
return url
url
}
}
@ -1488,7 +1510,7 @@ constructor(
override var posterUrl: String? = null,
var year: Int? = null,
var dubStatus: EnumSet<DubStatus>? = null,
var dubStatus: MutableSet<DubStatus>? = null,
var otherName: String? = null,
var episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
@ -1497,46 +1519,10 @@ constructor(
override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null,
override var score: Score? = null,
) : SearchResponse {
@Suppress("DEPRECATION_ERROR")
@Deprecated(
"Use newAnimeSearchResponse",
level = DeprecationLevel.ERROR
)
constructor(
name: String,
url: String,
apiName: String,
type: TvType? = null,
posterUrl: String? = null,
year: Int? = null,
dubStatus: EnumSet<DubStatus>? = null,
otherName: String? = null,
episodes: MutableMap<DubStatus, Int> = mutableMapOf(),
id: Int? = null,
quality: SearchQuality? = null,
posterHeaders: Map<String, String>? = null,
) : this(
name,
url,
apiName,
type,
posterUrl,
year,
dubStatus,
otherName,
episodes,
id,
quality,
posterHeaders, null
)
}
) : SearchResponse
fun AnimeSearchResponse.addDubStatus(status: DubStatus, episodes: Int? = null) {
this.dubStatus = dubStatus?.also { it.add(status) } ?: EnumSet.of(status)
this.dubStatus = dubStatus?.also { it.add(status) } ?: mutableSetOf(status)
if (this.type?.isMovieType() != true)
if (episodes != null && episodes > 0)
this.episodes[status] = episodes
@ -2535,15 +2521,45 @@ constructor(
get() = score?.toInt(100)
}
@OptIn(FormatStringsInDatetimeFormats::class)
fun Episode.addDate(date: String?, format: String = "yyyy-MM-dd") {
try {
this.date = SimpleDateFormat(format, Locale.getDefault()).parse(date ?: return)?.time
} catch (e: Exception) {
logError(e)
}
if (date == null) return
this.date = runCatching {
// First try standard ISO 8601 (e.g. "2026-01-01T12:30:00.000Z", "2026-05-17T14:35+02:00")
runCatching { Instant.parse(date).toEpochMilliseconds() }
.getOrElse {
val fmt = DateTimeComponents.Format { byUnicodePattern(format) }
val components = DateTimeComponents.parse(date, fmt)
/**
* Try multiple conversions in order of precision for non-ISO-8601 formats,
* since the date string may or may not include time and/or timezone offset:
* 1. If the custom format produced a UTC offset (e.g. "2026-05-17 14:35+02:00"), use it directly
* 2. If it has time but no offset (e.g. "2026-05-17 14:35"), fall back to device timezone
* 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
*/
runCatching { components.toInstantUsingOffset().toEpochMilliseconds() }
.recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
.getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds() }
}
}.onFailure { logError(it) }.getOrNull()
}
fun Episode.addDate(date: Date?) {
@Prerelease
fun Episode.addDate(date: LocalDate?) {
this.date = date?.atStartOfDayIn(TimeZone.currentSystemDefault())?.toEpochMilliseconds()
}
@Prerelease
fun Episode.addDate(date: Instant?) {
this.date = date?.toEpochMilliseconds()
}
// Deprecate after next stable
/* @Deprecated(
message = "Use addDate with LocalDate, Instant, or String instead.",
level = DeprecationLevel.WARNING,
) */
fun Episode.addDate(date: java.util.Date?) {
this.date = date?.time
}
@ -2680,6 +2696,27 @@ fun fetchUrls(text: String?): List<String> {
return linkRegex.findAll(text).map { it.value.trim().removeSurrounding("\"") }.toList()
}
@Prerelease
fun isUpcoming(dateString: String?): Boolean {
return runCatching {
val fmt = DateTimeComponents.Format {
year(); char('-'); monthNumber(); char('-'); day()
}
val components = DateTimeComponents.parse(dateString ?: return false, fmt)
/**
* Try multiple conversions in order of precision, since the date string format
* may or may not include time and/or timezone offset information:
* 1. If the string has a UTC offset (e.g. "2026-05-17T14:35+02:00"), use it directly
* 2. If it has time but no offset (e.g. "2026-05-17T14:35"), fall back to device timezone
* 3. If it's date-only (e.g. "2026-05-17"), use start of day in device timezone
*/
val instant = runCatching { components.toInstantUsingOffset() }
.recoverCatching { components.toLocalDateTime().toInstant(TimeZone.currentSystemDefault()) }
.getOrElse { components.toLocalDate().atStartOfDayIn(TimeZone.currentSystemDefault()) }
Clock.System.now() < instant
}.onFailure { logError(it) }.getOrElse { false }
}
@Deprecated(
"toRatingInt() is deprecated. Use new score API instead.",
level = DeprecationLevel.ERROR

View file

@ -1,39 +1,33 @@
package com.lagradost.cloudstream3
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import kotlin.reflect.KClass
// Short name for requests client to make it nicer to use
private val jacksonResponseParser = object : ResponseParser {
val mapper: ObjectMapper = jacksonObjectMapper().configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false
)
private val jsonResponseParser = object : ResponseParser {
override fun <T : Any> parse(text: String, kClass: KClass<T>): T {
return mapper.readValue(text, kClass.java)
return parseJson(text, kClass)
}
override fun <T : Any> parseSafe(text: String, kClass: KClass<T>): T? {
return try {
mapper.readValue(text, kClass.java)
} catch (e: Exception) {
parse(text, kClass)
} catch (_: Exception) {
null
}
}
override fun writeValueAsString(obj: Any): String {
return mapper.writeValueAsString(obj)
return obj.toJson()
}
}
/** The default networking helper. This helper performs SSL checks.
* If you need to make requests to websites with invalid SSL certificates use insecureApp instead. */
var app = Requests(responseParser = jacksonResponseParser).apply {
var app = Requests(responseParser = jsonResponseParser).apply {
defaultHeaders = mapOf("user-agent" to USER_AGENT)
}
@ -41,6 +35,6 @@ var app = Requests(responseParser = jacksonResponseParser).apply {
* This should NEVER be used for sensitive networking operations such as logins. Only use this when required. */
@Prerelease
@UnsafeSSL
var insecureApp = Requests(responseParser = jacksonResponseParser).apply {
var insecureApp = Requests(responseParser = jsonResponseParser).apply {
defaultHeaders = mapOf("user-agent" to USER_AGENT)
}
}

View file

@ -8,8 +8,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import java.net.URI
import java.nio.charset.StandardCharsets
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -46,11 +46,11 @@ open class ByseSX : ExtractorApi() {
}
private fun getBaseUrl(url: String): String {
return URI(url).let { "${it.scheme}://${it.host}" }
return Url(url).let { "${it.protocol.name}://${it.host}" }
}
private fun getCodeFromUrl(url: String): String {
val path = URI(url).path ?: ""
val path = Url(url).encodedPath.decodeURLPart()
return path.trimEnd('/').substringAfterLast('/')
}
@ -94,7 +94,7 @@ open class ByseSX : ExtractorApi() {
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
val plainBytes = cipher.doFinal(cipherBytes)
var jsonStr = String(plainBytes, StandardCharsets.UTF_8)
var jsonStr = plainBytes.decodeToString()
if (jsonStr.startsWith("\uFEFF")) jsonStr = jsonStr.substring(1)

View file

@ -6,15 +6,14 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URLDecoder
open class Cda : ExtractorApi() {
override var mainUrl = "https://ebd.cda.pl"
override var name = "Cda"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val mediaId = url
.split("/").last()
@ -65,10 +64,10 @@ open class Cda : ExtractorApi() {
.replace("_QWE", "")
.replace("_Q5", "")
.replace("_IKSDE", "")
a = URLDecoder.decode(a, "UTF-8")
a = a.decodeUrl()
a = a.map { char ->
if (char.code in 33..126) {
return@map String.format("%c", 33 + (char.code + 14) % 94)
return@map (33 + (char.code + 14) % 94).toChar().toString()
} else {
return@map char
}

View file

@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.HttpUrl.Companion.toHttpUrl
import io.ktor.http.Url
// deobfuscated from https://hglink.to/main.js?v=1.1.3 using https://deobfuscate.io/
private val mirrors = arrayOf(
@ -90,7 +90,7 @@ abstract class CineMMRedirect : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val videoId = url.toHttpUrl().encodedPath
val videoId = Url(url).encodedPath
val mirror = mirrors.random()
// re-use existing extractors by calling the ExtractorApi
@ -98,4 +98,4 @@ abstract class CineMMRedirect : ExtractorApi() {
val mirrorUrlWithVideoId = "https://$mirror$videoId"
loadExtractor(mirrorUrlWithVideoId, referer, subtitleCallback, callback)
}
}
}

View file

@ -27,7 +27,7 @@ open class CloudMailRu : ExtractorApi() {
"Origin" to mainUrl,
"User-Agent" to USER_AGENT,
)
val vidId = url.substringAfter("public/").toByteArray()
val vidId = url.substringAfter("public/").encodeToByteArray()
val vidIdEnc = base64Encode(vidId)
val videoReq = app.get(url, headers=headers).text
val regex = Regex(pattern = "videowl_view\":\\{\"count\":\"1\",\"url\":\"([^\"]*)\"\\}", options = setOf(RegexOption.IGNORE_CASE))

View file

@ -7,9 +7,8 @@ import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import java.net.URI
import io.ktor.http.Url
import io.ktor.http.decodeURLPart
class Geodailymotion : Dailymotion() {
override val name = "GeoDailymotion"
@ -57,7 +56,6 @@ open class Dailymotion : ExtractorApi() {
}
}
private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/") || url.contains("/video/")) return url
if (url.contains("geo.dailymotion.com")) {
@ -67,9 +65,8 @@ open class Dailymotion : ExtractorApi() {
return null
}
private fun getVideoId(url: String): String? {
val path = URI(url).path
val path = Url(url).encodedPath.decodeURLPart()
val id = path.substringAfter("/video/")
return if (id.matches(videoIdRegex)) id else null
}
@ -82,7 +79,6 @@ open class Dailymotion : ExtractorApi() {
return generateM3u8(name, streamLink, "").forEach(callback)
}
data class MetaData(
val qualities: Map<String, List<Quality>>?,
val subtitles: SubtitlesWrapper?
@ -102,5 +98,4 @@ open class Dailymotion : ExtractorApi() {
val label: String,
val urls: List<String>
)
}

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URI
import io.ktor.http.Url
class Doodspro : DoodLaExtractor() {
override var mainUrl = "https://doods.pro"
@ -138,8 +138,6 @@ open class DoodLaExtractor : ExtractorApi() {
}
private fun getBaseUrl(url: String): String {
return URI(url).let {
"${it.scheme}://${it.host}"
}
return Url(url).let { "${it.protocol.name}://${it.host}" }
}
}

View file

@ -0,0 +1,48 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.Prerelease
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.ExtractorLinkType
import com.lagradost.cloudstream3.utils.newExtractorLink
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Prerelease
open class Flyfile : ExtractorApi() {
override val name: String = "FlyFile"
override val mainUrl: String = "https://flyfile.app"
open val apiUrl: String = "https://api.flyfile.app"
override val requiresReferer: Boolean = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val videoId = url.substringAfterLast("/")
val videoInfo = app.get("$apiUrl/api/streaming/assign/$videoId")
.parsed<StreamInfo>()
val streamUrl = "${videoInfo.url}/hls/${videoInfo.token}/master.m3u8"
callback.invoke(
newExtractorLink(
source = name,
name = name,
url = streamUrl,
type = ExtractorLinkType.M3U8
)
)
}
@Serializable
private data class StreamInfo(
@SerialName("url")
val url: String,
@SerialName("token")
val token: String
)
}

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor
import java.net.URI
import io.ktor.http.Url
class Techinmind: GDMirrorbot() {
override var name = "Techinmind Cloud AIO"
@ -103,7 +103,7 @@ open class GDMirrorbot : ExtractorApi() {
}
private fun getBaseUrl(url: String): String {
return URI(url).let { "${it.scheme}://${it.host}" }
return Url(url).let { "${it.protocol.name}://${it.host}" }
}
}

View file

@ -82,7 +82,7 @@ open class Gdriveplayer : ExtractorApi() {
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+"))
?.joinToString("") {
Char(it.toInt()).toString()
it.toInt().toChar().toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password")
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
@ -125,4 +125,4 @@ open class Gdriveplayer : ExtractorApi() {
@JsonProperty("label") val label: String
)
}
}

View file

@ -8,6 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.newExtractorLink
import kotlin.math.round
open class Gofile : ExtractorApi() {
override val name = "Gofile"
@ -67,10 +68,19 @@ open class Gofile : ExtractorApi() {
?: Qualities.Unknown.value
}
private fun roundTo2Decimals(value: Double): String {
val rounded = round(value * 100) / 100.0
val intPart = rounded.toLong()
val decPart = round((rounded - intPart) * 100).toLong()
return "$intPart.${decPart.toString().padStart(2, '0')}"
}
private fun formatBytes(bytes: Long): String {
val mb = 1024L * 1024
val gb = mb * 1024
return when {
bytes < 1024L * 1024 * 1024 -> "%.2f MB".format(bytes.toDouble() / (1024 * 1024))
else -> "%.2f GB".format(bytes.toDouble() / (1024 * 1024 * 1024))
bytes < gb -> "${roundTo2Decimals(bytes.toDouble() / mb)} MB"
else -> "${roundTo2Decimals(bytes.toDouble() / gb)} GB"
}
}

View file

@ -2,13 +2,12 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.api.Log
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.extractors.helper.AesHelper
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
open class HDMomPlayer : ExtractorApi() {
override val name = "HDMomPlayer"
@ -24,7 +23,7 @@ open class HDMomPlayer : ExtractorApi() {
if (bePlayer != null) {
val bePlayerPass = bePlayer.get(1)
val bePlayerData = bePlayer.get(2)
val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
val encrypted = AesHelper.cryptoAESHandler(bePlayerData, bePlayerPass.encodeToByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
m3uLink = Regex("""video_location\":\"([^\"]+)""").find(encrypted)?.groupValues?.get(1)
} else {
@ -32,7 +31,7 @@ open class HDMomPlayer : ExtractorApi() {
val trackStr = Regex("""tracks:\[([^\]]+)""").find(iSource)?.groupValues?.get(1)
if (trackStr != null) {
val tracks:List<Track> = jacksonObjectMapper().readValue("[${trackStr}]")
val tracks:List<Track> = parseJson<List<Track>>("[${trackStr}]")
for (track in tracks) {
if (track.file == null || track.label == null) continue
@ -68,4 +67,4 @@ open class HDMomPlayer : ExtractorApi() {
@JsonProperty("language") val language: String?,
@JsonProperty("default") val default: String?
)
}
}

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.loadExtractor
import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URI
import io.ktor.http.Url
class HubCloud : ExtractorApi() {
override val name = "Hub-Cloud"
@ -24,7 +24,7 @@ class HubCloud : ExtractorApi() {
) {
val tag = "HubCloud"
val realUrl = url.takeIf {
try { URI(it).toURL(); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false }
try { Url(it); true } catch (e: Exception) { Log.e(tag, "Invalid URL: ${e.message}"); false }
} ?: return
val baseUrl=getBaseUrl(realUrl)
@ -161,7 +161,7 @@ class HubCloud : ExtractorApi() {
private fun getBaseUrl(url: String): String {
return try {
URI(url).let { "${it.scheme}://${it.host}" }
Url(url).let { "${it.protocol.name}://${it.host}" }
} catch (_: Exception) {
""
}

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.StringUtils.decodeUri
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
import com.lagradost.cloudstream3.utils.newExtractorLink
import org.jsoup.nodes.Document
@ -96,7 +96,7 @@ open class InternetArchive : ExtractorApi() {
if (mediaUrl.isNotEmpty()) {
val name = if (mediaUrl.count() > 1) {
val fileExtension = mediaUrl.substringAfterLast(".")
val fileNameCleaned = fileName.decodeUri().substringBeforeLast('.')
val fileNameCleaned = fileName.decodeUrl().substringBeforeLast('.')
"$fileNameCleaned ($fileExtension)"
} else this.name
callback(

View file

@ -14,11 +14,10 @@ open class Mvidoo : ExtractorApi() {
private fun String.decodeHex(): String {
require(length % 2 == 0) { "Must have an even length" }
return String(
chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
)
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
.decodeToString()
}
override suspend fun getUrl(

View file

@ -32,7 +32,7 @@ open class Odnoklassniki : ExtractorApi() {
val embedUrl = url.replace("/video/","/videoembed/")
val videoReq = app.get(embedUrl, headers=headers).text.replace("\\&quot;", "\"").replace("\\\\", "\\")
.replace(Regex("\\\\u([0-9A-Fa-f]{4})")) { matchResult ->
Integer.parseInt(matchResult.groupValues[1], 16).toChar().toString()
matchResult.groupValues[1].toInt(16).toChar().toString()
}
val videosStr = Regex(""""videos":(\[[^]]*])""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("Video not found")
val videos = AppUtils.tryParseJson<List<OkRuVideo>>(videosStr) ?: throw ErrorLoadingException("Video not found")

View file

@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
@ -171,7 +170,7 @@ open class Rabbitstream : ExtractorApi() {
IvParameterSpec(decryptionKey.copyOfRange(32, decryptionKey.size))
)
val decryptedData = aesCBC?.doFinal(encrypted) ?: throw ErrorLoadingException("Cipher not found")
return String(decryptedData, StandardCharsets.UTF_8)
return decryptedData.decodeToString()
}
data class Tracks(

View file

@ -35,14 +35,14 @@ open class RapidVid : ExtractorApi() {
if (extractedValue != null) {
val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
decoded = String(bytes, Charsets.UTF_8)
decoded = bytes.decodeToString()
} else {
val evalJWSsetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
val JWSsetup = getAndUnpack(getAndUnpack(evalJWSsetup)).replace("\\\\", "\\")
extractedValue = Regex("""file":"(.*)","label""").find(JWSsetup)?.groupValues?.get(1)?.replace("\\\\x", "")
val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray()
decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found")
decoded = bytes?.decodeToString() ?: throw ErrorLoadingException("File not found")
}
callback.invoke(

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.api.Log
import com.lagradost.cloudstream3.Prerelease
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
@ -12,7 +13,6 @@ import com.lagradost.cloudstream3.utils.getAndUnpack
import com.lagradost.cloudstream3.utils.getPacked
import com.lagradost.cloudstream3.network.WebViewResolver
class Mwish : StreamWishExtractor() {
override val name = "Mwish"
override val mainUrl = "https://mwish.pro"
@ -28,6 +28,12 @@ class Ewish : StreamWishExtractor() {
override val mainUrl = "https://embedwish.com"
}
@Prerelease
class Hgcloudto : StreamWishExtractor() {
override val name = "Hgcloud"
override val mainUrl = "https://Hgcloud.to"
}
class WishembedPro : StreamWishExtractor() {
override val name = "Wishembed"
override val mainUrl = "https://wishembed.pro"

View file

@ -8,7 +8,6 @@ import com.lagradost.cloudstream3.utils.INFER_TYPE
import com.lagradost.cloudstream3.utils.JsUnpacker
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URI
open class Streamhub : ExtractorApi() {
override var mainUrl = "https://streamhub.to"

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import java.net.URI
import io.ktor.http.Url
open class Streamplay : ExtractorApi() {
override val name = "Streamplay"
@ -22,9 +22,7 @@ open class Streamplay : ExtractorApi() {
) {
val request = app.get(url, referer = referer)
val redirectUrl = request.url
val mainServer = URI(redirectUrl).let {
"${it.scheme}://${it.host}"
}
val mainServer = Url(redirectUrl).let { "${it.protocol.name}://${it.host}" }
val key = redirectUrl.substringAfter("embed-").substringBefore(".html")
val token =
request.document.select("script").find { it.data().contains("sitekey:") }?.data()
@ -79,4 +77,4 @@ open class Streamplay : ExtractorApi() {
@JsonProperty("label") val label: String? = null,
)
}
}

View file

@ -6,8 +6,6 @@ import com.lagradost.cloudstream3.utils.*
import org.mozilla.javascript.Context
import org.mozilla.javascript.EvaluatorException
import org.mozilla.javascript.Scriptable
import java.util.*
open class Userload : ExtractorApi() {
override var name = "Userload"
@ -16,7 +14,7 @@ open class Userload : ExtractorApi() {
private fun splitInput(input: String): List<String> {
var counter = 0
val array = ArrayList<String>()
val array = mutableListOf<String>()
var buffer = ""
for (c in input) {
when (c) {
@ -71,7 +69,7 @@ open class Userload : ExtractorApi() {
}
var txtresult = ""
subchar.forEach{
txtresult = txtresult.plus(Char(it.toInt(8)))
txtresult = txtresult.plus(it.toInt(8).toChar())
}
val val1 = Regex(""""morocco="((.|\\n)*?)"&mycountry="""").find(txtresult)?.groups?.get(1)?.value.toString().drop(1).dropLast(1)
val val2 = txtresult.substringAfter("""&mycountry="+""").substringBefore(")")

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
@ -21,7 +22,7 @@ open class Vicloud : ExtractorApi() {
) {
val id = Regex("\"apiQuery\":\"(.*?)\"").find(app.get(url).text)?.groupValues?.getOrNull(1)
app.get(
"$mainUrl/api/?$id=&_=${System.currentTimeMillis()}",
"$mainUrl/api/?$id=&_=$unixTimeMS",
headers = mapOf(
"X-Requested-With" to "XMLHttpRequest"
),

View file

@ -35,14 +35,14 @@ open class VidMoxy : ExtractorApi() {
if (extractedValue != null) {
val bytes = extractedValue.split("\\x").filter { it.isNotEmpty() }.map { it.toInt(16).toByte() }.toByteArray()
decoded = String(bytes, Charsets.UTF_8)
decoded = bytes.decodeToString()
} else {
val evaljwSetup = Regex("""\};\s*(eval\(function[\s\S]*?)var played = \d+;""").find(videoReq)?.groupValues?.get(1) ?: throw ErrorLoadingException("File not found")
val jwSetup = getAndUnpack(getAndUnpack(evaljwSetup)).replace("\\\\", "\\")
extractedValue = Regex("""file":"(.*)","label""").find(jwSetup)?.groupValues?.get(1)?.replace("\\\\x", "")
val bytes = extractedValue?.chunked(2)?.map { it.toInt(16).toByte() }?.toByteArray()
decoded = bytes?.toString(Charsets.UTF_8) ?: throw ErrorLoadingException("File not found")
decoded = bytes?.decodeToString() ?: throw ErrorLoadingException("File not found")
}
callback.invoke(

View file

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.fixUrl
import com.lagradost.cloudstream3.utils.newExtractorLink
import java.net.URI
import io.ktor.http.Url
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@ -84,7 +84,7 @@ open class VidStack : ExtractorApi() {
private fun getBaseUrl(url: String): String {
return try {
URI(url).let { "${it.scheme}://${it.host}" }
Url(url).let { "${it.protocol.name}://${it.host}" }
} catch (e: Exception) {
Log.e("Vidstack", "getBaseUrl fallback: ${e.message}")
mainUrl

View file

@ -47,7 +47,7 @@ class Videa : ExtractorApi() {
rawBytes[4] == 0x6C.toByte() // 'l'
val videaXml = if (isXml) {
String(rawBytes, Charsets.UTF_8)
rawBytes.decodeToString()
} else {
// Handle encrypted XML response
val xsHeader = response.headers["X-Videa-Xs"] ?: return
@ -179,7 +179,7 @@ class Videa : ExtractorApi() {
}
val actualEncryptedBytes = if (isBase64) {
val base64String = String(encryptedBytes, Charsets.UTF_8)
val base64String = encryptedBytes.decodeToString()
.replace("\r", "")
.replace("\n", "")
.replace(" ", "")
@ -189,7 +189,7 @@ class Videa : ExtractorApi() {
encryptedBytes
}
val keyBytes = key.toByteArray(Charsets.UTF_8)
val keyBytes = key.encodeToByteArray()
// RC4 key-scheduling algorithm (KSA)
val s = IntArray(256) { it }
@ -211,6 +211,6 @@ class Videa : ExtractorApi() {
result[k] = ((actualEncryptedBytes[k].toInt() and 0xFF) xor keyStreamByte).toByte()
}
return String(result, Charsets.UTF_8)
return result.decodeToString()
}
}

View file

@ -2,12 +2,11 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.api.Log
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.*
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
open class VideoSeyred : ExtractorApi() {
override val name = "VideoSeyred"
@ -20,7 +19,7 @@ open class VideoSeyred : ExtractorApi() {
val videoUrl = "${mainUrl}/playlist/${videoId}.json"
val responseRaw = app.get(videoUrl)
val responseList:List<VideoSeyredSource> = jacksonObjectMapper().readValue(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred")
val responseList: List<VideoSeyredSource> = tryParseJson<List<VideoSeyredSource>>(responseRaw.text) ?: throw ErrorLoadingException("VideoSeyred")
val response = responseList[0]
for (track in response.tracks) {
@ -68,4 +67,4 @@ open class VideoSeyred : ExtractorApi() {
@JsonProperty("label") val label: String? = null,
@JsonProperty("default") val default: String? = null
)
}
}

View file

@ -38,13 +38,13 @@ class Vidsonic() : ExtractorApi() {
.substringBefore(";")
.replace("'", "")
// (improved) Java implementation of the JavaScript code from above
// (improved) Kotlin implementation of the JavaScript code from above
val streamUrl = encodedStreamUrl
.replace("|", "")
// always two base16 digits together build one ASCII char
.chunked(2)
.map {
Integer.parseInt(it, 16).toChar()
it.toInt(16).toChar()
}
.joinToString("")
.reversed()

View file

@ -1,14 +1,20 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.newAudioFile
import com.lagradost.cloudstream3.newSubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.newExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamType
expect open class YoutubeExtractor() : ExtractorApi {
override val mainUrl: String
override val name: String
override val requiresReferer: Boolean
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit,
)
}
class YoutubeShortLinkExtractor : YoutubeExtractor() {
override val mainUrl = "https://youtu.be"
@ -21,107 +27,3 @@ class YoutubeMobileExtractor : YoutubeExtractor() {
class YoutubeNoCookieExtractor : YoutubeExtractor() {
override val mainUrl = "https://www.youtube-nocookie.com"
}
open class YoutubeExtractor : ExtractorApi() {
override val mainUrl = "https://www.youtube.com"
override val name = "YouTube"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val videoId = extractYouTubeId(url)
val watchUrl = "$mainUrl/watch?v=$videoId"
val info = StreamInfo.getInfo(watchUrl)
val isLive =
info.streamType == StreamType.LIVE_STREAM
|| info.streamType == StreamType.AUDIO_LIVE_STREAM
|| info.streamType == StreamType.POST_LIVE_STREAM
|| info.streamType == StreamType.POST_LIVE_AUDIO_STREAM
if (isLive && info.hlsUrl != null) {
callback(
newExtractorLink(
source = name,
name = "YouTube Live",
url = info.hlsUrl
) {
type = ExtractorLinkType.M3U8
}
)
} else {
processVideo(info, subtitleCallback, callback)
}
}
private suspend fun processVideo(
info: StreamInfo,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
val videoStreams = info.videoOnlyStreams.orEmpty()
if (videoStreams.isEmpty()) return false
val audioStreams = info.audioStreams.orEmpty()
videoStreams.forEach { video ->
callback(
newExtractorLink(
source = name,
name = "YouTube ${normalizeCodec(video.codec)}",
url = video.content
) {
quality = video.height
audioTracks = audioStreams.map { newAudioFile(it.content) }
}
)
}
info.subtitles.forEach { subtitle ->
subtitleCallback(
newSubtitleFile(
lang = subtitle.displayLanguageName
?: subtitle.languageTag
?: "Unknown",
url = subtitle.content
)
)
}
return true
}
// ---------------- HELPERS ----------------
private fun extractYouTubeId(url: String): String {
val regex = Regex(
"(?:youtu\\.be/|youtube(?:-nocookie)?\\.com/(?:.*v=|v/|u/\\w/|embed/|shorts/|live/))([\\w-]{11})"
)
return regex.find(url)?.groupValues?.get(1)
?: throw IllegalArgumentException("Invalid YouTube URL: $url")
}
private fun normalizeCodec(codec: String?): String {
if (codec.isNullOrBlank()) return ""
val c = codec.lowercase()
return when {
c.startsWith("av01") -> "AV1"
c.startsWith("vp9") -> "VP9"
c.startsWith("avc1") || c.startsWith("h264") -> "H264"
c.startsWith("hev1") || c.startsWith("hvc1") || c.startsWith("hevc") -> "H265"
else -> codec.substringBefore('.').uppercase()
}
}
}

View file

@ -2,13 +2,11 @@ package com.lagradost.cloudstream3.extractors.helper
import com.lagradost.cloudstream3.base64DecodeArray
import com.lagradost.cloudstream3.base64Encode
import java.util.Arrays
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec
import javax.crypto.spec.IvParameterSpec
import java.nio.charset.StandardCharsets
import kotlin.math.min
/**
@ -48,9 +46,9 @@ object CryptoJS {
// Create CryptoJS-like encrypted!
val sBytes = APPEND.toByteArray()
val b = ByteArray(sBytes.size + saltBytes.size + cipherText.size)
System.arraycopy(sBytes, 0, b, 0, sBytes.size)
System.arraycopy(saltBytes, 0, b, sBytes.size, saltBytes.size)
System.arraycopy(cipherText, 0, b, sBytes.size + saltBytes.size, cipherText.size)
sBytes.copyInto(destination = b, destinationOffset = 0)
saltBytes.copyInto(destination = b, destinationOffset = sBytes.size)
cipherText.copyInto(destination = b, destinationOffset = sBytes.size + saltBytes.size)
return base64Encode(b)
}
@ -63,8 +61,8 @@ object CryptoJS {
*/
fun decrypt(password: String, cipherText: String): String {
val ctBytes = base64DecodeArray(cipherText)
val saltBytes = Arrays.copyOfRange(ctBytes, 8, 16)
val cipherTextBytes = Arrays.copyOfRange(ctBytes, 16, ctBytes.size)
val saltBytes = ctBytes.copyOfRange(8, 16)
val cipherTextBytes = ctBytes.copyOfRange(16, ctBytes.size)
val key = ByteArray(KEY_SIZE / 8)
val iv = ByteArray(IV_SIZE / 8)
@ -107,16 +105,18 @@ object CryptoJS {
hash.reset()
}
System.arraycopy(
block!!, 0, derivedBytes, numberOfDerivedWords * 4,
min(block.size, (targetKeySize - numberOfDerivedWords) * 4)
block!!.copyInto(
destination = derivedBytes,
destinationOffset = numberOfDerivedWords * 4,
startIndex = 0,
endIndex = min(block.size, (targetKeySize - numberOfDerivedWords) * 4)
)
numberOfDerivedWords += block.size / 4
}
System.arraycopy(derivedBytes, 0, resultKey, 0, keySize * 4)
System.arraycopy(derivedBytes, keySize * 4, resultIv, 0, ivSize * 4)
derivedBytes.copyInto(destination = resultKey, destinationOffset = 0, startIndex = 0, endIndex = keySize * 4)
derivedBytes.copyInto(destination = resultIv, destinationOffset = 0, startIndex = keySize * 4, endIndex = (keySize * 4) + (ivSize * 4))
return derivedBytes // key + iv
}
@ -126,4 +126,4 @@ object CryptoJS {
SecureRandom().nextBytes(this)
}
}
}
}

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