Compare commits

..

1,043 commits

Author SHA1 Message Date
firelight
a72f9e6c3f
Translated using Weblate (Albanian) (#3102)
Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Albanian)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (735 of 735 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Spanish)

Currently translated at 99.7% (733 of 735 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (734 of 735 strings)

Translated using Weblate (Swedish)

Currently translated at 99.8% (734 of 735 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (733 of 735 strings)

Translated using Weblate (English)

Currently translated at 99.8% (734 of 735 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (733 of 733 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (733 of 733 strings)













Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/sq/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane

Co-authored-by: 007 <juri.malaj@gmail.com>
Co-authored-by: Aziz Hadidi <azizhadidi@proton.me>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate user 54392 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: win7guru <winsevn@users.noreply.hosted.weblate.org>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
2026-08-05 22:54:24 +00:00
Hosted Weblate
5a67740643
Merge remote-tracking branch 'origin/master' 2026-08-06 00:54:04 +02:00
Bnyro
2207215a82
feat(extractors): add playmate extractor (#3103) 2026-08-05 22:53:51 +00:00
Hosted Weblate
9d070417ed
Translated using Weblate (Albanian)
Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Albanian)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (735 of 735 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Spanish)

Currently translated at 99.7% (733 of 735 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (734 of 735 strings)

Translated using Weblate (Swedish)

Currently translated at 99.8% (734 of 735 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Arabic)

Currently translated at 99.7% (733 of 735 strings)

Translated using Weblate (English)

Currently translated at 99.8% (734 of 735 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (735 of 735 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (733 of 733 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (733 of 733 strings)

Co-authored-by: 007 <juri.malaj@gmail.com>
Co-authored-by: Aziz Hadidi <azizhadidi@proton.me>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hosted Weblate user 54392 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: win7guru <winsevn@users.noreply.hosted.weblate.org>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/sq/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-08-05 22:53:21 +00:00
Bnyro
65269fb3b1
feat(extractors): add vidara mirrors and fix links with filecodes (#3104) [skip ci] 2026-08-05 22:52:36 +00:00
Jelle Thijs
caf0db3dc1
Feat: Add optional clock to TV player 2026-08-01 01:14:02 +00:00
firelight
1cf7425093
Merge pull request #3067 from recloudstream/weblate
Translations update from Hosted Weblate
2026-07-31 17:06:42 +00:00
Hosted Weblate
d7a9675bd3
Merge remote-tracking branch 'origin/master' 2026-07-31 17:00:23 +00:00
Hosted Weblate
e80fa1b30d
Translated using Weblate (Albanian)
Currently translated at 100.0% (733 of 733 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (733 of 733 strings)

Translated using Weblate (Bengali)

Currently translated at 52.6% (384 of 729 strings)

Translated using Weblate (English)

Currently translated at 100.0% (729 of 729 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Bengali)

Currently translated at 51.0% (372 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'

Translated using Weblate (Dutch)

Currently translated at 92.4% (674 of 729 strings)

Translated using Weblate (Vietnamese)

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 (Bengali)

Currently translated at 75.0% (3 of 4 strings)

Translated using Weblate (Bengali)

Currently translated at 49.9% (364 of 729 strings)

Co-authored-by: 007 <juri.malaj@gmail.com>
Co-authored-by: Aishik Mukherjee <aishikm2002@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: caviaz <sverrebesteman@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/bn/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-07-31 17:00:10 +00:00
recloudstream[bot]
7f750964f3 chore(locales): fix locale issues 2026-07-31 17:00:05 +00:00
firelight
ff701cb220
Add Belarusian, Catalan and Albanian 2026-07-31 16:59:46 +00:00
firelight
a2adb6caaa
[skip ci] Swallow exception when device does not support channels 2026-07-31 15:35:02 +00:00
CranberrySoup
f09435dc18
Add source hiding (#2570) 2026-07-31 15:25:00 +00:00
Luna712
2eb05e8f35
DataStore: deprecate toKotlinObject (#2964)
1. It can sometimes cause type erasure
2. It is less clear what it is doing
3. It is easy to replace
4. It is very rarely used, almost all app usage has already been replaced, and I haven't found any extensions actually using it but still following normal deprecation cycle as some still might be using it.
2026-07-31 15:01:45 +00:00
firelight
1e33e42277
fix subtitle clipping (#3099) 2026-07-31 14:59:31 +00:00
Kraptor123
93d3b36575
feat: add settings button to provider selection in home (#3060) 2026-07-25 22:43:11 +00:00
danzybae
2cd65aad4f
Fix posterHeaders not passed in TV detail menu (#3088) 2026-07-25 19:53:45 +00:00
danzybae
115be075d4
Fix posterHeaders not passed in home preview carousel (#3085) 2026-07-19 18:30:04 +00:00
Luna712
e48c97363e
[skip ci] Add actual declarations for webMain/webTest (#3072) 2026-07-15 22:27:22 +00:00
Luna712
6ca7507146
HlsPlaylistParser: replace usage of Java APIs with kotlin equivalents (#2828)
This does add kotlinx-io as a dependency, it is more light weight then okio, and when we migrate to ktor for example it will become an implicit dependency anyway so it should be fine to just add. Allows for more reliable and readable usage then doing some of this manually.
2026-07-15 13:34:43 +00:00
Luna712
91e533686e
[skip ci] Rewrite TmdbProvider to remove external lib dependency (#2800) 2026-07-15 13:31:41 +00:00
Luna712
fef17079a8
[skip ci] Coroutines KMP API (#2836) 2026-07-15 13:05:44 +00:00
firelight
fb8bcb4ac7
more robust date parsing (#3068) 2026-07-14 17:23:52 +00:00
Leandre Chamberland-Dozois
c09c06820d
fix(download): remove deleted/cancelled items from the Downloads list in real time (#1227) (#2598) 2026-07-14 15:19:39 +00:00
Kraptor123
3650221334
[skip ci] ui: Hide pinned NSFW-only extensions from non-NSFW categories (#2923) 2026-07-14 15:05:15 +00:00
firelight
c66b0eadd6
Fix failed cast in ContextHelper 2026-07-14 13:29:29 +00:00
Luna712
804b3c279c
extractors: migrate to kotlinx serialization (#2963) 2026-07-14 13:04:51 +00:00
Luna712
feddcccf0d
[skip ci] Update Kotlin to 2.4.0 (#2868) 2026-07-14 12:17:47 +00:00
Luna712
15fea0c7ca
Bump compilesdk to 37 (#2670) 2026-07-13 18:37:33 +00:00
Luna712
d778befe19
[skip ci] Add KMP locale API for SubtitleHelper (#2833) 2026-07-13 18:26:21 +00:00
Luna712
076e7a5116
[skip ci] test: add an IgnoreOnWeb annotation (#3034) 2026-07-13 18:00:32 +00:00
firelight
ecf5650bbd
fix build 2026-07-13 14:46:09 +00:00
Phisher98
b245a2d9d7
Fix VidHidePro M3U8 type detection in JWPlayerHelper (#3064) 2026-07-13 14:28:26 +00:00
Luna712
b1ce67dbf3
ControllerActivity: migrate to kotlinx serialization (#2958) 2026-07-13 13:20:02 +00:00
Luna712
eef67fa699
Replace usage of gson in extractors (#2983) 2026-07-13 13:14:51 +00:00
Luna712
86d713c33c
[skip ci] Improve ContextHelper API for KMP (#2834) 2026-07-13 12:57:43 +00:00
firelight
45a795f25f
[skip ci] Translated using Weblate (Latvian) (#2943)
Co-authored-by: Aishik Mukherjee <aishikm2002@gmail.com>
Co-authored-by: Kie Takaba <takabakie@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Nori Masomi (Mystical) <mysticalkeyall@users.noreply.hosted.weblate.org>
Co-authored-by: Saleh ALHarbi <alfraidigamerofficial@gmail.com>
Co-authored-by: caviaz <sverrebesteman@gmail.com>
Co-authored-by: gabix <gabi1xperson@gmail.com>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: trodiz <neptune32@protonmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
2026-07-13 12:48:43 +00:00
Hosted Weblate
e2d49567b6
Merge remote-tracking branch 'origin/master' 2026-07-13 14:42:20 +02:00
Luna712
105f168b2d
[skip ci] Add new splitUrlParameters API accepting Ktor Url or string (#2931) 2026-07-13 12:42:15 +00:00
Hosted Weblate
0859285e43
Merge remote-tracking branch 'origin/master' 2026-07-13 12:40:54 +00:00
Luna712
2a75b8cb08
[skip ci] Mark UriSerializer InternalAPI (#3062)
We shouldn't really be encouraging extensions to use it and use ktor Url instead which has a serializer for it built into ktor. Eventually I think we should factor out the two usages of an actual Uri in serialization and remove this serializer.
2026-07-13 12:40:47 +00:00
Hosted Weblate
737135fcb7
Merge remote-tracking branch 'origin/master' 2026-07-13 00:38:24 +02:00
Luna712
7b3c938ba7
Add more custom serializers for kotlinx serialization (#2978)
This adds numerous other serializers to replicate Jackson mapper configuration behavior, that some extensions will need.
2026-07-12 22:38:08 +00:00
Hosted Weblate
07dbc848ee
Merge remote-tracking branch 'origin/master' 2026-07-13 00:18:32 +02:00
Luna712
15632b864a
[skip ci] TraktProvider: migrate to kotlinx serialization (#2872) 2026-07-12 22:18:22 +00:00
Hosted Weblate
8f1317c82c
Merge remote-tracking branch 'origin/master' 2026-07-13 00:09:48 +02:00
Luna712
505722c1b5
[skip ci] Torrent: migrate to kotlinx serialization (#2882) 2026-07-12 22:09:36 +00:00
Hosted Weblate
024d2a5c7f
Merge remote-tracking branch 'origin/master' 2026-07-12 23:44:05 +02:00
firelight
70469f55db
[skip ci] Migration guide (#3058) 2026-07-12 21:43:59 +00:00
Hosted Weblate
9f64b6da29
Merge remote-tracking branch 'origin/master' 2026-07-11 19:21:51 +00:00
Hosted Weblate
c265d630ac
Translated using Weblate (Latvian)
Currently translated at 81.4% (594 of 729 strings)

Translated using Weblate (Bengali)

Currently translated at 48.1% (351 of 729 strings)

Translated using Weblate (Lithuanian)

Currently translated at 35.6% (260 of 729 strings)

Translated using Weblate (Dutch)

Currently translated at 91.0% (664 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'

Translated using Weblate (Arabic (Saudi Arabia))

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Japanese)

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'

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 (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.8% (276 of 729 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (726 of 729 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Korean)

Currently translated at 100.0% (729 of 729 strings)

Co-authored-by: Aishik Mukherjee <aishikm2002@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kie Takaba <takabakie@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Nori Masomi (Mystical) <mysticalkeyall@users.noreply.hosted.weblate.org>
Co-authored-by: Saleh ALHarbi <alfraidigamerofficial@gmail.com>
Co-authored-by: caviaz <sverrebesteman@gmail.com>
Co-authored-by: gabix <gabi1xperson@gmail.com>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: trodiz <neptune32@protonmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar_SA/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-07-11 21:21:44 +02:00
Luna712
11792dd65c
Add type for tryParseJson (#3057) 2026-07-11 21:21:42 +02:00
Luna712
4a79ca1c9f
Add missing types to getKey calls (#3056) 2026-07-11 21:21:11 +02:00
BlipBlob
3496e5f8d2
[skip ci] Use abiValidation to prevent breaking changes in extension library (#2835) 2026-07-10 22:59:31 +00:00
Luna712
5cd1cb3433
SimklApi: migrate to kotlinx serialization (#2998) 2026-07-10 18:26:09 +00:00
Luna712
f03b18ff95
[skip ci] Enable some deprecations (#3048) 2026-07-10 18:19:33 +00:00
Luna712
0af9b7f569
DataStoreHelper: migrate to kotlinx serialization (#3027) 2026-07-10 16:59:59 +00:00
firelight
1463cf40cd
remove prerelease annotations (#3046) 2026-07-10 16:57:27 +00:00
Luna712
3e17b3a5a0
Make some private cryptography methods suspend (#3040) 2026-07-10 16:55:35 +00:00
Luna712
8c3dbdc5b6
Remove fuzzywuzzy from library (#2999)
It doesn't use api(), which means extensions don't inherently have access, and they must already have the dependency explicit to use it. It being in app prevents runtime errors if they use it, but it being an implimentation in the library should be completely unused and unnecessary.
2026-07-10 16:54:15 +00:00
Luna712
5b0c5afeba
EmturbovidExtractor: replace selectXpath (#2977)
This will be necessary when we finally fully migrate to ksoup, which currently doesn't have selectXpath, but for our usages that is not necessary. This way is also more consistent with how we do it everywhere else.
2026-07-10 16:53:36 +00:00
Kraptor123
e75b5e64a7
shortcode: Add py.md as alternative to cutt.ly (#3001) 2026-07-10 16:52:23 +00:00
Luna712
c36652d265
[skip ci] DownloadObjects: migrate to kotlinx serialization (#3026) 2026-07-10 16:40:25 +00:00
Luna712
b681d443e4
[skip ci] Make some classes serializable (#2874) 2026-07-10 16:18:30 +00:00
Luna712
189162fac9
AesHelper: make the new cryptoAESHandler suspend (#3039) 2026-07-10 16:12:06 +00:00
Osten
3796406e63
Bump to 4.8.0 2026-07-10 01:46:50 +02:00
Luna712
8bdc599996
test: some cleanup to JsInterpreterTest (#3036) 2026-07-09 17:43:49 +00:00
Luna712
0e3191e28b
[skip ci] Update commented deprecation message for getRhinoContext (#3037) 2026-07-09 17:41:14 +00:00
Luna712
11202d269d
[skip ci] SyncUtil: fix type for tryParseJson (#3038) 2026-07-09 17:40:00 +00:00
firelight
07b4d2d576
Remove AM/PM when on 24h settings 2026-07-09 15:51:16 +00:00
firelight
66afc3b1ee
Feat: Animation to actor adapter (#3035) 2026-07-09 15:50:05 +00:00
firelight
e428d422eb
Fix: Remove uiReset for better UX for errors (#3023) 2026-07-09 11:12:37 +00:00
firelight
59eaed0c82
Fix: Subsouce and cache issue (#3033) 2026-07-09 11:08:53 +00:00
Luna712
694c9c4413
Add JsInterpreter to replace Rhino usage (#2812) 2026-07-09 01:41:54 +02:00
firelight
480d48bea4
Fix: Switch focus on skip chapter (#3024) 2026-07-09 01:26:42 +02:00
firelight
a123c5935c
Fix slow startup in downloader 2026-07-08 17:30:23 +00:00
firelight
5cb939df69
Fix: Corrupted HSL files due to 404 (#3013) 2026-07-08 17:29:06 +00:00
firelight
60cfeba735
Fix: Sync delay, Progress on sync button and runOnMainThreadNative fix (#3032) 2026-07-08 17:13:50 +00:00
Luna712
678726e9ee
GogoHelper: use more clear name for private method (#3028) 2026-07-08 16:17:17 +02:00
firelight
894a18a831
Feat: Loading apply button (#3025) 2026-07-07 18:34:00 +00:00
firelight
5bae7d35d8
Fix: Subtitle ordering on names (#3012) 2026-07-07 14:17:39 +00:00
firelight
94ce0d4385
Fix: Repo dismiss (#3020) 2026-07-07 14:09:34 +00:00
firelight
55b5017068
[skip ci] Fix: Sync episode setting, Closes #3014 (#3021) 2026-07-07 14:06:47 +00:00
firelight
e8c005dfeb
[skip ci] Fix: Leading space in Vids (#3022) 2026-07-07 14:05:07 +00:00
Luna712
bfb85ca5c9
[skip ci] SerializationClassTester: skip abstract classes (#2960)
Necessary for some such as LibrarySearchResponse and DownloadCached. Using the skip annotation causes it to also skip child classes which we don't want so just skip abstract classes since Instancio can't create an instance of an abstract class: "It is an abstract class and no subtype was provided".
2026-07-06 20:17:29 +00:00
Bnyro
8828ee1718
feat(extractors): add vids.st extractor (#3008) 2026-07-06 20:05:23 +00:00
Bnyro
5bcac072e1
fix(extractors) fix gupload extractor (#3007) 2026-07-06 20:04:38 +00:00
danzybae
771a201d76
Fix regex to match quality case insensitively (#3002) 2026-07-06 18:24:48 +00:00
Bnyro
2cde74de27
feat(extractors): fix vidara and update list of mirrors (#2992) 2026-07-03 18:47:31 +00:00
Bnyro
adb57c86b3
feat(extractors): add firestream.to extractor (#2993) 2026-07-03 18:43:40 +00:00
Bnyro
bc270d538c
feat(extractors): add streamcash.to extractor (#2994) 2026-07-03 18:41:43 +00:00
Luna712
fd078d75be
[skip ci] Add expect/actual for WorkerThread (#2997)
This will later be necessary for other targets as it only exists on JVM platforms. Other targets would just define it as a base annoation that doesn't do anything, allows us to keep thread safety on JVM platforms but still be able to compile for others that don't have it.
2026-07-03 18:31:18 +00:00
Luna712
4b6bfa8bbd
Promote AcraApplication deprecations to error level (#2630) 2026-07-02 23:48:49 +00:00
firelight
b5e2a0eff4
Update user agent
Fixes #2951
2026-07-02 23:43:39 +00:00
Luna712
5e85c80fd5
Replace some JVM specific stuff in JsHunter and JsUnpacker (#2990) 2026-07-02 22:14:40 +00:00
Luna712
982189cdf0
AniListApi: migrate to kotlinx serialization (#2952) 2026-07-02 22:12:28 +00:00
Luna712
faacc1a893
[skip ci] Remove failOnNoDiscoveredTests = false (#2985)
Not needed anymore since we actually have tests now.
2026-07-02 21:43:53 +00:00
Luna712
d164c36123
[skip ci] Remove completely commented and out of date ProviderTests file (#2973) 2026-07-02 21:42:32 +00:00
Luna712
b1022805bb
Add kotlin-reflect to jvmCommonMain in library (#2989) 2026-07-02 21:41:12 +00:00
Luna712
60da132d3f
[skip ci] MyDramaList: migrate to kotlinx serialization (#2870) 2026-07-02 21:40:57 +00:00
Luna712
aaa87c1ccc
LibraryFragment: migrate to kotlinx serialization (#2972) 2026-07-02 21:31:41 +00:00
Luna712
56c6400fd9
[skip ci] Remove unnecessary ?. (#2982)
It is a build warning
2026-07-02 21:27:46 +00:00
Luna712
971027d2d1
[skip ci] Add JvmName import to MainAPI (#2988)
Necessary for non JVM platforms or it won't resolve. On non JVM platforms the annotation is simply erased at compile time but still compiled as long as the import is present.
2026-07-02 21:26:54 +00:00
KingLucius
870d514bb7
Focus "Add Repo" by Right click on Extensions page (#2986) 2026-07-02 15:29:35 +00:00
firelight
b425d39ef5
Fix(TV): Crash issue due to missing xml, Closes #2980 (#2981) 2026-06-30 20:40:58 +02:00
Luna712
9ab4150c4f
[skip ci] Support running instrumented tests with a comment in pull requests (#2792) 2026-06-30 17:31:05 +00:00
Luna712
6a7a31cefb
Replace javaClass with Kotlin ::class for consistency (#2976)
I used Jackson's JsonAlias and Kotlinx Serialization's JsonNames annotations for CustomSite to maintain backwards compatibility for deserialization but future serialization would use the new name, so `parentClassName` rather than `parentJavaClass`.
2026-06-30 17:16:04 +00:00
Luna712
959970cfd1
fcast: migrate to kotlinx serialization (#2970) 2026-06-30 18:40:37 +02:00
Luna712
53b0150245
Remove broken providersExist test (#2974) 2026-06-30 18:39:14 +02:00
Luna712
a074b96d48
VotingApi: add missing type (#2971) 2026-06-30 18:38:15 +02:00
firelight
609834cf03
refactor plugin logic and add repository name (#2975) 2026-06-30 18:37:38 +02:00
firelight
f1578919b6
Add button to search extensions (#2966) 2026-06-27 14:11:10 +00:00
Luna712
d2af4ef123
BasePlugin: migrate Manifest to kotlinx serialization (#2873) 2026-06-26 16:14:50 +00:00
Luna712
aa60bd708a
SyncUtil: migrate to kotlinx serialization (#2957) 2026-06-26 16:07:03 +00:00
Luna712
49270b43c8
InAppUpdater: migrate to kotlinx serialization (#2877) 2026-06-26 16:05:05 +00:00
Luna712
99ac37c6d7
extractors/helper: migrate to kotlinx serialization (#2959) 2026-06-26 16:00:53 +00:00
Luna712
10d726fd0c
BackupUtils: migrate to kotlinx serialization (#2955) 2026-06-26 15:57:36 +00:00
KingLucius
b97426b85e
Clean Player Animation code (#2939) 2026-06-26 15:42:47 +00:00
Luna712
2b9848aaac
MALApi: migrate to kotlinx serialization (#2953) 2026-06-26 15:34:49 +00:00
Nivin
6e7c054df4
minor fix dailymotion extractor (#2962) 2026-06-26 15:27:39 +00:00
Luna712
15a9f7cc7a
Migrate crypto from javax to cryptography-kotlin in library (#2813) 2026-06-25 13:54:19 +00:00
Luna712
0099e30dfc
[skip ci] Add a new jvmCommonMain source set to library (#2863) 2026-06-25 12:30:14 +00:00
Luna712
9b343eaa0c
[skip ci] plugins: migrate to kotlinx serialization (#2880) 2026-06-25 12:21:04 +00:00
Luna712
3d7d0fe131
CloudStreamPackage: migrate to kotlinx serialization (#2878) 2026-06-24 22:52:21 +00:00
Luna712
ac1ea57454
FillerEpisodeCheck: migrate to kotlinx serialization (#2886) 2026-06-24 22:48:22 +00:00
Luna712
ba2d71bc19
CrossTmdbProvider: migrate to kotlinx serialization (#2871) 2026-06-24 22:46:20 +00:00
Luna712
7dd68710cf
[skip ci] settings: migrate to kotlinx serialization (#2884) 2026-06-24 22:44:04 +00:00
Luna712
1990e62a1c
[skip ci] AuthAPI: migrate to kotlinx serialization (#2888) 2026-06-24 22:42:27 +00:00
Luna712
f5e8b3331c
[skip ci] KitsuApi: migrate to kotlinx serialization (#2892) 2026-06-24 22:40:38 +00:00
Luna712
04ba9f1c06
[skip ci] OpenSubtitlesApi: migrate to kotlinx serialization (#2889) 2026-06-24 22:30:11 +00:00
Luna712
4f1abad514
[skip ci] SerializationClassTester: show all failures at once (#2949)
This is helpful for batch testing rather than testing and fixing one at a time.
2026-06-24 22:26:33 +00:00
Luna712
28786fab50
[skip ci] Add a temporary SkipSerializationTest annotation (#2947) 2026-06-24 22:12:39 +00:00
Luna712
1f0e20c16e
[skip ci] StringUtils: add DeprecationLevel.WARNING to not yet enabled deprecations (#2948) 2026-06-24 22:12:21 +00:00
firelight
ffa2e18d4d
Limit tags in the player overlay 2026-06-23 19:02:08 +00:00
firelight
cf9b2909a7
Fix overlay description sometimes showing HTML 2026-06-23 18:52:13 +00:00
Luna712
eee9fc6fe2
Replace direct usage of Jsoup.parse() with Ksoup (#2919) 2026-06-23 18:39:56 +00:00
firelight
683da9605f
Remove flickering from bottom dialog sheets
Now also dims the background
2026-06-23 15:01:47 +00:00
Luna712
deebac0bb2
SearchHistoryAdaptor: migrate to kotlinx serialization (#2883) 2026-06-23 00:14:43 +00:00
Luna712
8b8891c59b
SubSource: migrate to kotlinx serialization (#2891) 2026-06-23 00:11:04 +00:00
recloudstream[bot]
54618fab99 chore(locales): fix locale issues 2026-06-23 00:07:39 +00:00
Weblate (bot)
dc13c28b7f
Translated using Weblate (Vietnamese) (#2910)
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.8% (276 of 729 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (726 of 729 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Korean)

Currently translated at 100.0% (729 of 729 strings)






Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translation: Cloudstream/App

Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Nori Masomi (Mystical) <mysticalkeyall@users.noreply.hosted.weblate.org>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: trodiz <neptune32@protonmail.com>
2026-06-23 00:07:24 +00:00
Luna712
c1fc6c1300
SearchSuggestionApi: migrate to kotlinx serialization (#2879) 2026-06-23 00:05:53 +00:00
Luna712
acff04e04f
videoskip: migrate to kotlinx serialization (#2881) 2026-06-23 00:02:34 +00:00
Luna712
41a9f25cbe
subtitles: migrate to kotlinx serialization (#2885) 2026-06-22 23:49:41 +00:00
firelight
be8e9437e9
Fix regression in subtitle offset animation (#2942) 2026-06-22 23:32:29 +00:00
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
Asheesh Sahu
03eb6ccd45
fix: implement memory-based throttle to prevent OOM during downloads (#2786) 2026-05-12 15:58:48 +00:00
Asheesh Sahu
7425d283cd
fix: external player visibility on Android 11+ (#2785) 2026-05-12 15:30:17 +00:00
fire-light43
6eb833130d
Fix rare bug in the download queue 2026-05-11 15:59:00 +00:00
recloudstream[bot]
3d1852ba04 chore(locales): fix locale issues 2026-05-11 13:02:22 +00:00
fire-light43
59ae579b7c
Merge pull request #2754 from recloudstream/weblate
Translations update from Hosted Weblate
2026-05-11 13:02:02 +00:00
Hosted Weblate
aa6e702b59
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Chinese (Traditional Han script))

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 (Indonesian)

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'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (726 of 729 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (French)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Albanian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Arabic)

Currently translated at 99.5% (726 of 729 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Albanian)

Currently translated at 77.2% (563 of 729 strings)

Translated using Weblate (Italian)

Currently translated at 99.8% (728 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (729 of 729 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (729 of 729 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Czech)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 25.2% (183 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.6% (172 of 726 strings)

Translated using Weblate (Assamese)

Currently translated at 86.9% (631 of 726 strings)

Translated using Weblate (Assamese)

Currently translated at 86.9% (631 of 726 strings)

Translated using Weblate (Assamese)

Currently translated at 86.9% (631 of 726 strings)

Translated using Weblate (Arabic (Najdi))

Currently translated at 45.0% (327 of 726 strings)

Translated using Weblate (Arabic (Najdi))

Currently translated at 45.0% (327 of 726 strings)

Translated using Weblate (Arabic (Najdi))

Currently translated at 45.0% (327 of 726 strings)

Translated using Weblate (Arabic (Najdi))

Currently translated at 45.0% (327 of 726 strings)

Translated using Weblate (Arabic (Najdi))

Currently translated at 45.0% (327 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.0% (719 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.0% (719 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.0% (719 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.0% (719 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.0% (719 of 726 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.0% (719 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Persian)

Currently translated at 46.5% (338 of 726 strings)

Translated using Weblate (Galician)

Currently translated at 38.9% (283 of 726 strings)

Translated using Weblate (Galician)

Currently translated at 38.9% (283 of 726 strings)

Translated using Weblate (Galician)

Currently translated at 38.9% (283 of 726 strings)

Translated using Weblate (Galician)

Currently translated at 38.9% (283 of 726 strings)

Translated using Weblate (Galician)

Currently translated at 38.9% (283 of 726 strings)

Translated using Weblate (Galician)

Currently translated at 38.9% (283 of 726 strings)

Translated using Weblate (Catalan)

Currently translated at 45.0% (327 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 88.9% (646 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 88.9% (646 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 88.9% (646 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 88.9% (646 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 88.9% (646 of 726 strings)

Translated using Weblate (Dutch)

Currently translated at 88.9% (646 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Amharic)

Currently translated at 14.4% (105 of 726 strings)

Translated using Weblate (Albanian)

Currently translated at 73.1% (531 of 726 strings)

Translated using Weblate (Albanian)

Currently translated at 73.1% (531 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Kannada)

Currently translated at 17.4% (127 of 726 strings)

Translated using Weblate (Nepali)

Currently translated at 17.0% (124 of 726 strings)

Translated using Weblate (Nepali)

Currently translated at 17.0% (124 of 726 strings)

Translated using Weblate (Nepali)

Currently translated at 17.0% (124 of 726 strings)

Translated using Weblate (Nepali)

Currently translated at 17.0% (124 of 726 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 19.8% (144 of 726 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 19.8% (144 of 726 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 65.7% (477 of 726 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 65.7% (477 of 726 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 65.7% (477 of 726 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 65.7% (477 of 726 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 65.7% (477 of 726 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 65.7% (477 of 726 strings)

Translated using Weblate (Romanian)

Currently translated at 84.8% (616 of 726 strings)

Translated using Weblate (Romanian)

Currently translated at 84.8% (616 of 726 strings)

Translated using Weblate (Romanian)

Currently translated at 84.8% (616 of 726 strings)

Translated using Weblate (Romanian)

Currently translated at 84.8% (616 of 726 strings)

Translated using Weblate (Romanian)

Currently translated at 84.8% (616 of 726 strings)

Translated using Weblate (Romanian)

Currently translated at 84.8% (616 of 726 strings)

Translated using Weblate (Odia)

Currently translated at 21.4% (156 of 726 strings)

Translated using Weblate (Odia)

Currently translated at 21.4% (156 of 726 strings)

Translated using Weblate (Odia)

Currently translated at 21.4% (156 of 726 strings)

Translated using Weblate (Odia)

Currently translated at 21.4% (156 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Croatian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Croatian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Croatian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Maltese)

Currently translated at 16.6% (121 of 726 strings)

Translated using Weblate (Maltese)

Currently translated at 16.6% (121 of 726 strings)

Translated using Weblate (Maltese)

Currently translated at 16.6% (121 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 90.4% (657 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 90.4% (657 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 90.4% (657 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 90.4% (657 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 90.4% (657 of 726 strings)

Translated using Weblate (Greek)

Currently translated at 90.4% (657 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hebrew)

Currently translated at 72.5% (527 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Hungarian)

Currently translated at 81.9% (595 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Slovak)

Currently translated at 62.5% (454 of 726 strings)

Translated using Weblate (Afrikaans)

Currently translated at 16.2% (118 of 726 strings)

Translated using Weblate (Afrikaans)

Currently translated at 16.2% (118 of 726 strings)

Translated using Weblate (Afrikaans)

Currently translated at 16.2% (118 of 726 strings)

Translated using Weblate (Afrikaans)

Currently translated at 16.2% (118 of 726 strings)

Translated using Weblate (Afrikaans)

Currently translated at 16.2% (118 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Bengali)

Currently translated at 48.2% (350 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Macedonian)

Currently translated at 96.4% (700 of 726 strings)

Translated using Weblate (Macedonian)

Currently translated at 96.4% (700 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Malay)

Currently translated at 65.8% (478 of 726 strings)

Translated using Weblate (Lithuanian)

Currently translated at 29.7% (216 of 726 strings)

Translated using Weblate (Lithuanian)

Currently translated at 29.7% (216 of 726 strings)

Translated using Weblate (Lithuanian)

Currently translated at 29.7% (216 of 726 strings)

Translated using Weblate (Lithuanian)

Currently translated at 29.7% (216 of 726 strings)

Translated using Weblate (Lithuanian)

Currently translated at 29.7% (216 of 726 strings)

Translated using Weblate (Arabic (Egyptian))

Currently translated at 0.0% (0 of 726 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 726 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 726 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 726 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Tigrinya)

Currently translated at 0.4% (3 of 726 strings)

Translated using Weblate (Tigrinya)

Currently translated at 0.4% (3 of 726 strings)

Translated using Weblate (Tigrinya)

Currently translated at 0.4% (3 of 726 strings)

Translated using Weblate (Tigrinya)

Currently translated at 0.4% (3 of 726 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Tamil)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Tamil)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 11.2% (82 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Tagalog)

Currently translated at 32.5% (236 of 726 strings)

Translated using Weblate (Burmese)

Currently translated at 69.1% (502 of 726 strings)

Translated using Weblate (Burmese)

Currently translated at 69.1% (502 of 726 strings)

Translated using Weblate (Burmese)

Currently translated at 69.1% (502 of 726 strings)

Translated using Weblate (Burmese)

Currently translated at 69.1% (502 of 726 strings)

Translated using Weblate (Burmese)

Currently translated at 69.1% (502 of 726 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Urdu)

Currently translated at 81.8% (594 of 726 strings)

Translated using Weblate (Urdu)

Currently translated at 81.8% (594 of 726 strings)

Translated using Weblate (Urdu)

Currently translated at 81.8% (594 of 726 strings)

Translated using Weblate (Urdu)

Currently translated at 81.8% (594 of 726 strings)

Translated using Weblate (Urdu)

Currently translated at 81.8% (594 of 726 strings)

Translated using Weblate (Urdu)

Currently translated at 81.8% (594 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (German)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 96.9% (704 of 726 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 96.9% (704 of 726 strings)

Translated using Weblate (Latvian)

Currently translated at 80.8% (587 of 726 strings)

Translated using Weblate (Latvian)

Currently translated at 80.8% (587 of 726 strings)

Translated using Weblate (Latvian)

Currently translated at 80.8% (587 of 726 strings)

Translated using Weblate (Latvian)

Currently translated at 80.8% (587 of 726 strings)

Translated using Weblate (Latvian)

Currently translated at 80.8% (587 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Somali)

Currently translated at 60.4% (439 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Malayalam)

Currently translated at 31.2% (227 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 85.3% (620 of 726 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 85.3% (620 of 726 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 85.3% (620 of 726 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 85.3% (620 of 726 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 85.3% (620 of 726 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 85.3% (620 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Co-authored-by: 007 <juri.malaj@gmail.com>
Co-authored-by: Aayush Shah <shahaayush999@gmail.com>
Co-authored-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
Co-authored-by: Ahmed Abd El-Fattah <a.aelfattah5@gmail.com>
Co-authored-by: Aitor Salaberria <amento@ni.eus>
Co-authored-by: Akhlak Ur Rahman <akhlak.pro.red@gmail.com>
Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Co-authored-by: Alexander Svärd <genc.demiri@hotmail.com>
Co-authored-by: Alexandru <negrualexandru52@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Andre Costa <andrecaeu@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Apostol Penkov <apostol.penkov@gmail.com>
Co-authored-by: Argo Carpathians <chrisarabagas@gmail.com>
Co-authored-by: Azgar <azgaresncf@gmail.com>
Co-authored-by: Balmunk <reilria77@gmail.com>
Co-authored-by: Bananenbrot <keram2810@outlook.de>
Co-authored-by: Beabfekad Zikie <beabfekadz@gmail.com>
Co-authored-by: Bhupesh Yadav <thebhupeshyadav@gmail.com>
Co-authored-by: Bitpaint <bitpaintclub@gmail.com>
Co-authored-by: BluTiger <beqirstafa@gmail.com>
Co-authored-by: ButterflyOfFire <boffire@users.noreply.hosted.weblate.org>
Co-authored-by: Bálint László <blaszlobors@gmail.com>
Co-authored-by: Carlos Luiz <ecarlos-luiz@hotmail.com>
Co-authored-by: Chaya Endot <cheyenne.xyn@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Clxff H3r4ld0 <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: DarkOrbFX <darkorbfx@gmail.com>
Co-authored-by: Davide Marcoli <davide.marcoli13@gmail.com>
Co-authored-by: Deleted User <Skrripy@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <inavleb@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <jsdelvani@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+48943@weblate.org>
Co-authored-by: Deleted User <noreply+53776@weblate.org>
Co-authored-by: Deleted User <noreply+68493@weblate.org>
Co-authored-by: Doctorredits_here <182783629+doctorreditshere@users.noreply.github.com>
Co-authored-by: Emmanuel HEMERIT <emmanuel@hemer.it>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: FastAct <alex.rijckaert@gmail.com>
Co-authored-by: Felicity <kjev666+weblate@protonmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Gabriel <bloxgabriel18@gmail.com>
Co-authored-by: Gabriel Cnudde <gabriel.cnudde59@gmail.com>
Co-authored-by: Giuseppe Terrana <terranagiuseppe03@gmail.com>
Co-authored-by: Gnkalk <github.fngyb@slmail.me>
Co-authored-by: H Tamás <hovanszki@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Imprevisible <imprevisible@duck.com>
Co-authored-by: Itsmechinmoy <167056923+itsmechinmoy@users.noreply.github.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: JL Pilgram <twich_89@hotmail.it>
Co-authored-by: Jean-Michel <arsene_lupin_57@hotmail.fr>
Co-authored-by: Jen Xie <aulaul825879@gmail.com>
Co-authored-by: Joana Trashlieva <j.trashlieva@gmail.com>
Co-authored-by: John Titor <utkin2007@gmail.com>
Co-authored-by: Joshua Joseph <joshuasaju2@gmail.com>
Co-authored-by: Julia Sugawara <jm.sugawara@gmail.com>
Co-authored-by: Kardi Demha <kardi.demha@gmail.com>
Co-authored-by: Konstantinos Tranoudis <kontranpro@gmail.com>
Co-authored-by: Kraptor123 <kraptor121@gmail.com>
Co-authored-by: LagradOst <46196380+Blatzar@users.noreply.github.com>
Co-authored-by: Leon de Klerk <deklerkleon5@gmail.com>
Co-authored-by: Levi Klippel <leviklippel@gmail.com>
Co-authored-by: LiJu09 <lisojuraj@gmail.com>
Co-authored-by: LucasMZ <git@lucasmz.dev>
Co-authored-by: Ludovic Pagès <nanucq@proton.me>
Co-authored-by: Luna712 <142361265+Luna712@users.noreply.github.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Marian Turba <puki247@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Michael John Scerri <michaeljscerri@gmail.com>
Co-authored-by: Mika <akimivanov43@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Muhammad Fahad Khan <itxmfahadkhan@gmail.com>
Co-authored-by: Muhammet <zumruduanka0013@gmail.com>
Co-authored-by: Murat Han <murathancw@gmail.com>
Co-authored-by: Márkó <gost1336@gmail.com>
Co-authored-by: Naga <yz2000.pro@gmail.com>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Nicoara Alex <alex.nicoara@yahoo.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: PaneradFisk <andre.bergvall@icloud.com>
Co-authored-by: PiterDev <piterzdev@gmail.com>
Co-authored-by: Pixelcode <pixelcode@dismail.de>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Posemartonis <weblate.drainage895@passmail.net>
Co-authored-by: Qareen Skoll <qareen101@protonmail.com>
Co-authored-by: Radoslav Lelchev <rlelchev@abv.bg>
Co-authored-by: Radoslav Vasilev Vasilev <fifata@gmail.com>
Co-authored-by: Ranforingus <ranforingus@hotmail.com>
Co-authored-by: Red Star Over Earth <rs0vere@outlook.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Roi Gabay <roigby@gmail.com>
Co-authored-by: SBS1313 <simonsaade005@gmail.com>
Co-authored-by: Sam Cooper <samcooper838@gmail.com>
Co-authored-by: Samuel Gadiel <samuelgadiel@gmail.com>
Co-authored-by: Sandyran <sandyran@protonmail.com>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: SehrGuterCode <philemonpfeiffer@gmail.com>
Co-authored-by: Shafici Isxariifshe <mega12xhaphiee@gmail.com>
Co-authored-by: ShareASmile <ShareASmile@users.noreply.hosted.weblate.org>
Co-authored-by: Sintayew Gashaw <sintayewgashaw4@gmail.com>
Co-authored-by: SleepyOwl <artem726artem@gmail.com>
Co-authored-by: Sufyan Zahoor Jutt <sufyanpahore@gmail.com>
Co-authored-by: Synertry <jonny.somrak@gmail.com>
Co-authored-by: T1z3n <info@njbraun.de>
Co-authored-by: TZVS <akyasan@tuta.io>
Co-authored-by: The Initiator <eithansten@gmail.com>
Co-authored-by: Tiago Lucas <tiago.slucas@gmail.com>
Co-authored-by: Tomas Andersson <jotakswe@users.noreply.hosted.weblate.org>
Co-authored-by: TubaApollo <86665265+TubaApollo@users.noreply.github.com>
Co-authored-by: Vrwi <jurgisbums@gmail.com>
Co-authored-by: Walter H. <walter75@gmail.com>
Co-authored-by: Wei-Cheng Yeh (IID) <iid@ccns.ncku.edu.tw>
Co-authored-by: almost gamer <almost.gamer01+language@gmail.com>
Co-authored-by: arcopnt <arcopnt@posteo.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: dabao1955 <dabao1955@163.com>
Co-authored-by: edgolron <edgolron@tutanota.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: enly sure <enlysure@gmail.com>
Co-authored-by: esfzzddfse <esfzzddfse@proton.me>
Co-authored-by: george kitsoukakis <norhorn@gmail.com>
Co-authored-by: helpless_helper <dis.hd.eif.y.bk.ao.up@gmail.com>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: htet <htetoh2006@outlook.com>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: kerklangsi <arifilah.01@gmail.com>
Co-authored-by: khalidbelk <khalid.belkassmi-el-hafi@epitech.eu>
Co-authored-by: l <thisuserooo@gmail.com>
Co-authored-by: liva <livinja@proton.me>
Co-authored-by: mojtaba piri <Mpcuteangel@gmail.com>
Co-authored-by: ngocanhtve <ngocanh.tve@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: programutox <programutox@disroot.org>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: therry47 <soulietherry@gmail.com>
Co-authored-by: tuan041 <tuananh163025ttt@gmail.com>
Co-authored-by: william piti <loolyowo@gmail.com>
Co-authored-by: Ícaro Rodrigo Ferreira Da Fonseca Bezerra <icaro.bezerra@sptech.school>
Co-authored-by: Ömer Faruk Sancak <keyiflerolsun@gmail.com>
Co-authored-by: ΣΤΑΥΡΟΣ ΔΑΛΙΑΚΟΠΟΥΛΟΣ <stavros.daliakopoulos@gmail.com>
Co-authored-by: Влад Николаев <vladnic1990@gmail.com>
Co-authored-by: Сергей (MrSabin) <sabin.21011986@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: 电棍老板 <qwertyuiop9296@outlook.com>
Co-authored-by: 구병우 <dodamby@ajou.ac.kr>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/af/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/am/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ars/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/arz/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/az/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ca/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ckb/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/eo/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/gl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/kn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/my/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ne/
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/or/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/qt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/so/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ti/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translation: Cloudstream/App
2026-05-11 13:12:30 +02:00
firelight
0d16a636e2
Fix: playerVideoTitleRez visibility (#2767) 2026-05-06 20:13:29 +02:00
firelight
bfc926814c
Fixobserveorder (#2766) 2026-05-06 20:13:13 +02:00
firelight
a45f1d9ab1
Refactor: Player, Generator and ViewModel (#2764) 2026-05-06 02:29:47 +02:00
Luna712
948a2c1725
Always go to next source in player and add thread safety (#2733) 2026-05-04 21:57:27 +02:00
Luna712
4e24aa5db1
Minor fixes to player (#2756) 2026-05-04 18:43:01 +02:00
Osten
7476d24db3
Added exhaustive keyCode check, fixed #2757 2026-05-04 14:40:13 +02:00
firelight
c82fec0862
Refactor: Move all key logic into the player, and added toggleEpisodesOverlay keybind (#2745) 2026-05-03 22:46:09 +02:00
Luna712
e36e9e8d24
Use Looper.getMainLooper().isCurrentThread for simplicity (#2753) 2026-05-03 18:06:09 +02:00
Luna712
e64136db8a
Use runOnMainThread for simplicity (#2749) 2026-05-02 20:15:34 +02:00
Luna712
104ab26790
Use ioWorkSafe for updateFillers for simplification (#2743) 2026-05-01 18:53:30 +02:00
Osten
2400e6ab45
fixed observe, aka #2567 (#2736) 2026-04-30 17:23:49 +02:00
firelight
4cc76ee6c5
Fix(TV): Color on "Skip Chapter" button (#2731) 2026-04-30 17:23:08 +02:00
Luna712
8523a4bd90
Fix DownloadedPlayerActivity not loading new files when activity is already running (#2738) 2026-04-30 17:22:32 +02:00
Luna712
58f45c7bda
Remove unnecessary ?: 0 (#2734) 2026-04-30 00:46:12 +02:00
firelight
d30d71f39e
Fix(TV): Metadata hide episode selector #2715 (#2730) 2026-04-28 23:35:37 +02:00
Luna712
4610d6aae7
Revert "Improve subtitle selection UX: Move "No Subtitles" option to bottom (…" (#2720)
This reverts commit ee6a9af217.
2026-04-28 23:22:30 +02:00
firelight
9257d31090
Fix: Cancel popup dialog if dismissed (#2726) 2026-04-28 23:21:45 +02:00
firelight
2755385fa6
Feat: Ask for path before download from UI (#2699) 2026-04-28 23:21:32 +02:00
Luna712
18d9f5c317
Major rework to player (#2689) 2026-04-28 23:20:43 +02:00
Luna712
53345a804f
Enable abortOnError in lint (#2681) 2026-04-25 22:32:09 +00:00
Luna712
71306d4e98
[skip ci] Delete empty files (#2723) 2026-04-25 22:21:36 +00:00
firelight
6d79b0e5d0
Fix: Remove default headers from downloads, like player (#2722) 2026-04-25 01:17:32 +02:00
firelight
3bdda7d380
Feat: Anime Skip (#2710) 2026-04-25 00:29:37 +02:00
Luna712
659f639acd
Add new TVType for generic Video (#2712) 2026-04-25 00:26:07 +02:00
Luna712
1d750340a0
Fix some typos in singular (#2721) 2026-04-25 00:22:17 +02:00
Bnyro
d4899536d3
refactor(extractors): simplify and combine jwplayer extraction (#2398) 2026-04-22 00:45:04 +00:00
firelight
18ee71664f
Feat: Offline filler database (#2704) 2026-04-20 23:24:37 +00:00
Luna712
f7494f20e1
Support resuming fragmented MP4s (#2690) 2026-04-19 21:42:46 +00:00
Luna712
590a94e318
Fix typo in credits (#2703) 2026-04-19 21:40:12 +00:00
Luna712
2264b90396
Bump nicehttp (#2697) 2026-04-19 21:39:22 +00:00
Luna712
0ed6fd8fef
Bump jsoup and zipline libs (#2517) 2026-04-19 21:39:12 +00:00
Luna712
e3e995b222
Add lint ignore (#2669)
We only care about the source language with this, not translations which would mostly be false positives.
2026-04-19 17:01:50 +00:00
Luna712
7c1554a479
AGP 9! (#2604) 2026-04-19 17:00:47 +00:00
firelight
7926e60fb0
Add plugin hash validation (#2644) 2026-04-19 13:29:37 +00:00
Luna712
68a1d0856c
Fix STATE_IDLE issues in player (#2691) 2026-04-19 13:09:19 +00:00
hrisabhy
ee6a9af217
Improve subtitle selection UX: Move "No Subtitles" option to bottom (#2523) 2026-04-19 12:59:44 +00:00
Luna712
c1eef1de1d
Add new URL for Voe (#2701) 2026-04-19 12:41:05 +00:00
firelight
f175beb51b
Fix concurrent plugin loading (#2700) 2026-04-19 12:05:24 +00:00
Luna712
e55794c200
Bump buildkonfig lib (#2643) 2026-04-18 13:52:14 +00:00
Luna712
c67ba2b485
Add explicit permission checks for notifications in downloader (#2667) 2026-04-18 13:45:55 +00:00
Luna712
6336837903
Revert media3 to 1.9.3 (#2693) 2026-04-18 13:40:05 +00:00
Luna712
636d2507f7
Add missing OptIn (#2668)
This an error level opt in introduced in media3 1.10.0.
2026-04-16 23:28:16 +00:00
Luna712
cd03392364
Remove setup-android action from Dokka action (#2666)
It shouldn't be necessary with setup-gradle.
2026-04-16 23:26:52 +00:00
Luna712
c31c5764ea
Bump nextlibMedia3 (#2658) 2026-04-14 22:22:38 +00:00
Luna712
7925e714e7
Fix editing accounts from MainActivity (#2663) 2026-04-14 21:39:31 +00:00
Luna712
8d416fa2fc
Remove commented android.enableJetifier from gradle.properties (#2662)
It is now deprecated anyway. We will never use it now, so we can just fully remove it.
2026-04-14 20:48:23 +00:00
Luna712
0bb9322276
Don't explicitly enable WebContentsDebugging (#2657)
"this is enabled automatically if the app is declared as `android:debuggable="true"` in its manifest; otherwise, the default is false." - which we set on CloudStream Debug but not release flavors.

"Enabling web contents debugging allows the state of any WebView in the app to be inspected and modified by the user via adb. This is a security liability and should not be enabled in production builds of apps unless this is an explicitly intended use of the app."
2026-04-14 20:47:38 +00:00
Luna712
cfce80e93e
Bump DGP and KGP libs (#2582)
Final compatibility with AGP 9
2026-04-13 22:27:27 +00:00
firelight
fb54d02979
Fix SSL issues (#2655) 2026-04-13 22:21:52 +00:00
Luna712
788189c80c
Bump github-script action (#2642) 2026-04-13 21:52:57 +00:00
Luna712
1b0fdb57a8
Add permissions to workflows (#2654)
https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions
2026-04-13 21:50:57 +00:00
firelight
2eb63dc334
Change default installer to legacy (#2653)
Switching the default to the more reliable legacy installer until we fix the new installer.
2026-04-13 21:47:12 +00:00
Luna712
bd7db6c20a
Upgrade media3 to 1.10.0 (#2608) 2026-04-12 22:46:11 +02:00
Phisher98
14d56de61e
Adding a subtle shadow and minor adjustments to make the description stand out more on a white background. (#2648) 2026-04-12 22:40:05 +02:00
CranberrySoup
adf2ed6df3
Fix livestreams (#2627) 2026-04-12 22:36:31 +02:00
Luna712
b89f36c9bc
Bump material to 1.14.0-beta01 (#2636) 2026-04-09 22:01:20 +02:00
Luna712
0f1cb3a773
Add strictly for coil lib (#2635) 2026-04-09 21:59:53 +02:00
Phisher98
c304e8556e
Minor Fix IntroDbSkip (#2634) 2026-04-09 21:59:31 +02:00
firelight
a7f5f9a35a
Feat: TheIntroDBSkip + Bugfix (#2631) 2026-04-09 21:58:50 +02:00
Luna712
d7b030e7ef
Update gradle to 9.4.1 (#2610) 2026-04-09 17:19:57 +02:00
Luna712
fe0829ff64
Bump material (#2609) 2026-04-09 17:11:17 +02:00
Luna712
bb4e5da5c9
Bump androidx libraries (#2607) 2026-04-09 17:07:10 +02:00
Luna712
c9a24e198c
Add true configuration cache support for git commit hash (#2285)
Co-authored-by: firelight <147925818+fire-light42@users.noreply.github.com>
2026-04-09 10:32:41 +00:00
Luna712
ca96aa6891
Bump actions (#2588)
* Keep gradle/actions/setup-gradle@v5 for now
2026-04-09 10:16:24 +00:00
Luna712
8bdc1a83d7
Use InternalAPI rather than permanent deprecations in PluginManager (#2615) 2026-04-09 10:12:53 +00:00
firelight
be69ec938e
Merge pull request #2633 from recloudstream/weblate
Translations update from Hosted Weblate
2026-04-09 10:11:32 +00:00
firelight
04b22ba4df
Small backup fix 2026-04-09 10:11:15 +00:00
Hosted Weblate
e22a596d0c
Translated using Weblate (Arabic)
Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Albanian)

Currently translated at 68.0% (494 of 726 strings)

Co-authored-by: 007 <juri.malaj@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: hollow04 <ichigo0404@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/
Translation: Cloudstream/App
2026-04-09 07:09:57 +00:00
Onur Civanoğlu
f6920fb05d
feat: Force landscape orientation and pillarbox portrait videos on TV and emulator devices. (#2560) 2026-04-09 01:22:57 +02:00
CranberrySoup
f28924f704
Fix intent launches (#2554) 2026-04-09 01:17:21 +02:00
Luna712
b510942027
Bump newpipeextractor to v0.26.0 (#2624) 2026-04-09 01:11:40 +02:00
firelight
1d03b05a7c
Refactor: New SkipAPI for SkipStamp (#2601) 2026-04-09 01:04:39 +02:00
Luna712
f51885fb6e
Fix MotionEvent gestures getting stuck in player (#2629) 2026-04-09 01:00:53 +02:00
firelight
31165a87c1
Merge pull request #2625 from Luna712/short-commit
Use short commit hashes for libs
2026-04-08 22:40:58 +00:00
firelight
9f792f5b1a
Merge pull request #2616 from recloudstream/weblate
Translations update from Hosted Weblate
2026-04-08 22:40:01 +00:00
Hosted Weblate
418cf08ad4
Translated using Weblate (Albanian)
Currently translated at 57.0% (414 of 726 strings)

Translated using Weblate (Albanian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Added translation using Weblate (Albanian)

Translated using Weblate (Hindi)

Currently translated at 56.6% (411 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Co-authored-by: 007 <juri.malaj@gmail.com>
Co-authored-by: ByAyzen <Ayzenxyz@proton.me>
Co-authored-by: Hariom Jha <hariom.jha5499@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: muhaco <cemusa10@gmail.com>
Co-authored-by: xinshoutw <me@xinshou.tw>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sq/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/sq/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-04-08 17:10:18 +00:00
Luna712
d495cbe32d
Use short commit hashes for libs 2026-04-06 19:01:57 -06:00
Bnyro
08b1d97152
feat(extractors): add playmogo (doodstream mirror) (#2620) 2026-04-06 21:45:29 +00:00
Luna712
62e6895d8e
Replace deprecated viewBinding { enable = true } in Gradle (#2623) 2026-04-06 21:39:27 +00:00
Luna712
ae9a374a83
Don't keep screen on when ended (#2619) 2026-04-06 21:36:49 +00:00
Luna712
154cd7500b
Remove @prerelease annotations and enable some deprecations (#2614) 2026-04-06 21:32:58 +00:00
firelight
76e30d2e75
Merge pull request #2591 from recloudstream/weblate
Translations update from Hosted Weblate
2026-04-04 18:32:47 +00:00
Hosted Weblate
b2cd9612ea
Merge remote-tracking branch 'origin/master' 2026-04-04 18:27:01 +00:00
Luna712
562a5d8192
Fix CustomSubripParser to match assertion usage upstream (#2612)
Also suppresses a deprecation to fix build warnings. Fixes all other warnings in assertions by changing them because that is what it uses upstream also.
2026-04-04 18:26:55 +00:00
Hosted Weblate
bb295ded09
Translated using Weblate (Vietnamese)
Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (723 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (French)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Latvian)

Currently translated at 80.8% (587 of 726 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (726 of 726 strings)

Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Posemartonis <weblate.drainage895@passmail.net>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: blueocean2308 <bluewhale2308@gmail.com>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translation: Cloudstream/App
2026-04-04 18:26:35 +00:00
Luna712
ba9413e972
Change param name in interface to match everywhere else (#2611) 2026-04-04 18:26:17 +00:00
Phisher98
db154a8cd2
Adding IntroDB (#2599) 2026-04-03 16:36:42 +02:00
Nguyen Van Nam
736c6374a6
Fix: thread-safe HashMap for image bitmap cache 2026-03-30 21:23:02 +00:00
firelight
9fe7662f95
Merge pull request #2573 from recloudstream/weblate
Translations update from Hosted Weblate
2026-03-30 23:21:45 +02:00
Hosted Weblate
d23fb0ac4c
Translated using Weblate (Swedish)
Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (725 of 726 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (726 of 726 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Slovak)

Currently translated at 62.6% (454 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.7% (172 of 725 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.1% (719 of 725 strings)

Translated using Weblate (Latvian)

Currently translated at 81.2% (589 of 725 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'

Translated using Weblate (Esperanto)

Currently translated at 17.5% (127 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 82.0% (595 of 725 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (722 of 725 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'

Translated using Weblate (Belarusian)

Currently translated at 99.5% (722 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 77.3% (561 of 725 strings)

Translated using Weblate (Dutch)

Currently translated at 89.1% (646 of 725 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 75.4% (547 of 725 strings)

Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Aron Folkerts <aronfolkerts@gmail.com>
Co-authored-by: Daniel Konstantinov <bgshadow2010@gmail.com>
Co-authored-by: David Hermann <theumis@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jen Xie <aulaul825879@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Romhányi-Kakucska Viktor <viktor@romhanyi.dev>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: Wacky Wars <wackywars21@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: clearstripe <sakurasan000510@gmail.com>
Co-authored-by: hollow <ichigo0404@hotmail.com>
Co-authored-by: hou1234 <gjqmgjsl3@gmail.com>
Co-authored-by: jpkaster 77 <jpkaster81@gmail.com>
Co-authored-by: programutox <programutox@disroot.org>
Co-authored-by: tomas293 <tomaskopodstreleny@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/eo/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ar/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-03-30 15:10:01 +00:00
firelight
c26f236202
Fix: Minor UX bugs with #2461 2026-03-30 00:29:27 +02:00
recloudstream[bot]
673569a747 chore(locales): fix locale issues 2026-03-29 22:12:16 +00:00
Phisher98
7a2222b252
Adding Metdata on Player (Initial Draft) (TV) (#2461) 2026-03-30 00:11:58 +02:00
PiterDev
76a2feb79c
Add fallback url for kitsu sync (#2552) 2026-03-29 20:33:36 +02:00
Nguyen Van Nam
81c7d90a5f
Fix: Subtitle deletion matches on substring extension, can delete non-subtitle files (#2584) 2026-03-29 18:15:34 +00:00
Osten
235863f9d2
Bump to 4.7.0 2026-03-29 14:06:17 +02:00
Osten
07eb9973f8
Increased DOWNLOAD_PARTIAL_MIN_SIZE to 50MB 2026-03-29 13:54:51 +02:00
Osten
a23c136d81
Fixed partial downloads + resume bugs 2026-03-26 01:13:13 +01:00
Osten
89400be5e5
Remove google dependenciesInfo + bumb nicehttp 2026-03-25 18:31:57 +01:00
Phisher98
d06afa32fd
Tracks naming fix and minor UI improvements (#2480) 2026-03-23 22:14:48 +00:00
Luna712
f674b427ac
Use assemblePrereleaseRelease for building prerelease (#2365) 2026-03-23 21:48:48 +00:00
Luna712
f5b46949ec
Add support for configuration cache with keystore (#2328) 2026-03-22 23:16:39 +00:00
firelight
dce70ac229
Merge pull request #2565 from recloudstream/weblate
Translations update from Hosted Weblate
2026-03-23 00:16:30 +01:00
Osten
45699b72a8
Added .close to m3u8 hslLazy 2026-03-22 13:41:12 +01:00
Osten
a74a0840d6
Fixed BackPressedCallbackHelper activity leak 2026-03-22 13:04:22 +01:00
Hosted Weblate
73e19212cc
Merge remote-tracking branch 'origin/master' 2026-03-22 08:34:50 +01:00
Osten
ee1e90e0f4
Emergency fix for OOM and leaks 2026-03-22 08:34:43 +01:00
Hosted Weblate
51bd1c4a6c
Translated using Weblate (Slovak)
Currently translated at 62.6% (454 of 725 strings)

Translated using Weblate (Esperanto)

Currently translated at 23.7% (172 of 725 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.1% (719 of 725 strings)

Translated using Weblate (Latvian)

Currently translated at 81.2% (589 of 725 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'

Translated using Weblate (Esperanto)

Currently translated at 17.5% (127 of 725 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 82.0% (595 of 725 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (722 of 725 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'

Translated using Weblate (Belarusian)

Currently translated at 99.5% (722 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 77.3% (561 of 725 strings)

Translated using Weblate (Dutch)

Currently translated at 89.1% (646 of 725 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 75.4% (547 of 725 strings)

Co-authored-by: Aron Folkerts <aronfolkerts@gmail.com>
Co-authored-by: Daniel Konstantinov <bgshadow2010@gmail.com>
Co-authored-by: David Hermann <theumis@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jen Xie <aulaul825879@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Romhányi-Kakucska Viktor <viktor@romhanyi.dev>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: Wacky Wars <wackywars21@gmail.com>
Co-authored-by: clearstripe <sakurasan000510@gmail.com>
Co-authored-by: jpkaster 77 <jpkaster81@gmail.com>
Co-authored-by: programutox <programutox@disroot.org>
Co-authored-by: tomas293 <tomaskopodstreleny@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/eo/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translation: Cloudstream/App
2026-03-20 09:09:57 +00:00
Luna712
19efb1ffc3
Fix BuildConfig import (#2566) 2026-03-13 23:21:12 +00:00
Luna712
86cca03dd7
Use a new method to pass if debug to fix debug logging in library (#2330)
* Use a new method to pass if debug to fix debug logging in library
2026-03-13 22:39:34 +00:00
Nivin
ef07f761d7
Prefer player default live position for HLS/DASH (#2547) 2026-03-13 22:17:53 +00:00
Bnyro
904dda0c60
feat(extractors): add new extractor for vidsonic.net (#2557) 2026-03-13 22:03:27 +00:00
Bnyro
8d3846d2a3
feat(extractors): add support for vidara.to (#2556)
* feat(extractors): add support for vidara.to

* Allow soft subtitle failure in Streamup
2026-03-13 21:59:59 +00:00
Osten
ccc0a45065
Fixed shared pool, closes #2082 (#2553) 2026-03-13 21:54:11 +00:00
firelight
06907bed05
Translated using Weblate (Vietnamese) (#2530)
Co-authored-by: Aron Folkerts <aronfolkerts@gmail.com>
Co-authored-by: David Hermann <theumis@users.noreply.hosted.weblate.org>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Romhányi-Kakucska Viktor <viktor@romhanyi.dev>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: Wacky Wars <wackywars21@gmail.com>
Co-authored-by: clearstripe <sakurasan000510@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
2026-03-13 22:54:03 +01:00
Bnyro
b0d3731faa
feat(extractors): add vide0 doodstream mirror (#2558) 2026-03-11 14:19:57 +00:00
Hosted Weblate
618f9cde65
Translated using Weblate (Vietnamese)
Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 82.0% (595 of 725 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (722 of 725 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'

Translated using Weblate (Belarusian)

Currently translated at 99.5% (722 of 725 strings)

Translated using Weblate (Filipino)

Currently translated at 21.2% (154 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 77.3% (561 of 725 strings)

Translated using Weblate (Dutch)

Currently translated at 89.1% (646 of 725 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (German)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Hungarian)

Currently translated at 75.4% (547 of 725 strings)

Co-authored-by: Aron Folkerts <aronfolkerts@gmail.com>
Co-authored-by: David Hermann <theumis@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Romhányi-Kakucska Viktor <viktor@romhanyi.dev>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: Wacky Wars <wackywars21@gmail.com>
Co-authored-by: clearstripe <sakurasan000510@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translation: Cloudstream/App
2026-03-09 19:09:55 +01:00
Luna712
1fb6ce310d
Bump material (#2519) 2026-02-28 23:34:24 +01:00
Luna712
2ca21051d5
Bump androidx libraries (#2518) 2026-02-28 23:29:54 +01:00
firelight
a8f6ef0ea5
Fix: Nextlib textrenderer from #2510 2026-02-28 23:23:24 +01:00
Luna712
809b66af81
Revert "Revert "Update media3 to 1.9.2"" (#2510) 2026-02-28 23:02:49 +01:00
CranberrySoup
ecc3e506f9
Backup fix (#2542)
* Fix subtitle selection

* Move logic to getLanguageDataFromName

* Update BackupUtils.kt
2026-02-28 22:59:10 +01:00
saimuel
ef9e49d955
Remove dead Extractors (#2511) 2026-02-28 22:58:33 +01:00
DieGon7771
a65828e2b0
Fix trailer zoom not resetting after fullscreen exit (#2512) 2026-02-28 22:57:01 +01:00
Saurabh Kaperwan
9f2067bbff
fix gofile extractor (#2525)
* fix gofile extractor

* minor fix
2026-02-28 22:54:34 +01:00
Phisher98
514a808218
Removing default headers that caused some streams to return 2004. (#2533) 2026-02-28 22:25:44 +01:00
Phisher98
8e71baeb84
Make video info slightly dimmer (#2508) 2026-02-20 23:10:41 +00:00
firelight
543d1b4478
Translated using Weblate (#2482)
Co-authored-by: Broo Mohamed <broo91398@gmail.com>
Co-authored-by: Camila Sciocca <hellocamilatranslation@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Daniel Wiik <d.wiik@outlook.com>
Co-authored-by: Deepak C <deepakchatgpt19@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Kehribar <103407696+dpentx@users.noreply.github.com>
Co-authored-by: Kerim Demirkaynak <aschannel111@gmail.com>
Co-authored-by: Luiz Felipe Sudorio dos Santos <luizfelipe@post.com>
Co-authored-by: MD Sakibur Rahman <msr.official01@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oliver <oliver.puff@gmx.de>
Co-authored-by: Posemartonis <weblate.drainage895@passmail.net>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: ShowhyT <showhy@proton.me>
Co-authored-by: Takeru Mikenu <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Timo Panda <pandoroo@users.noreply.hosted.weblate.org>
Co-authored-by: XC3 <darkofficial110@gmail.com>
Co-authored-by: kokolo <ivansto314@gmail.com>
Co-authored-by: lamrichiasmaa <lamrichiasma@gmail.com>
Co-authored-by: lizamimiku-wq <lizamimiku@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: tjy122 <tning0134@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2026-02-20 23:08:25 +00:00
Saurabh Kaperwan
6e423ba24e
Add multiple extractors new domains (#2516) 2026-02-20 23:02:55 +00:00
Hosted Weblate
46f9c95376
Translated using Weblate (French)
Currently translated at 100.0% (725 of 725 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (German)

Currently translated at 98.8% (717 of 725 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (English)

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.6% (715 of 725 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (724 of 725 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (725 of 725 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (725 of 725 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'

Translated using Weblate (Croatian)

Currently translated at 99.3% (718 of 723 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (723 of 723 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (723 of 723 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (723 of 723 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (723 of 723 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (722 of 723 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (723 of 723 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (723 of 723 strings)

Merge remote-tracking branch 'origin/master'

Added translation using Weblate (Arabic (Egyptian))

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 99.5% (712 of 715 strings)

Translated using Weblate (Belarusian)

Currently translated at 99.5% (712 of 715 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (714 of 715 strings)

Translated using Weblate (Belarusian)

Currently translated at 90.4% (647 of 715 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Belarusian)

Currently translated at 85.5% (612 of 715 strings)

Translated using Weblate (Bengali)

Currently translated at 48.9% (350 of 715 strings)

Translated using Weblate (German)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Belarusian)

Currently translated at 65.5% (469 of 715 strings)

Translated using Weblate (French)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Croatian)

Currently translated at 99.8% (714 of 715 strings)

Translated using Weblate (Belarusian)

Currently translated at 58.0% (415 of 715 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (715 of 715 strings)

Co-authored-by: Broo Mohamed <broo91398@gmail.com>
Co-authored-by: Camila Sciocca <hellocamilatranslation@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Daniel Wiik <d.wiik@outlook.com>
Co-authored-by: Deepak C <deepakchatgpt19@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kehribar <103407696+dpentx@users.noreply.github.com>
Co-authored-by: Kerim Demirkaynak <aschannel111@gmail.com>
Co-authored-by: Luiz Felipe Sudorio dos Santos <luizfelipe@post.com>
Co-authored-by: MD Sakibur Rahman <msr.official01@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Oliver <oliver.puff@gmx.de>
Co-authored-by: Posemartonis <weblate.drainage895@passmail.net>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: Sasha Glazko <lenify@users.noreply.hosted.weblate.org>
Co-authored-by: ShowhyT <showhy@proton.me>
Co-authored-by: Takeru Mikenu <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Timo Panda <pandoroo@users.noreply.hosted.weblate.org>
Co-authored-by: XC3 <darkofficial110@gmail.com>
Co-authored-by: kokolo <ivansto314@gmail.com>
Co-authored-by: lamrichiasmaa <lamrichiasma@gmail.com>
Co-authored-by: lizamimiku-wq <lizamimiku@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: tjy122 <tning0134@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2026-02-19 10:09:57 +01:00
Mioki
1d1a7fb6fe
Improved TV Back Button UX (extension content -> extension selection Bttn -> home nav Bttn -> exit dialog) (#2468) 2026-02-18 17:58:48 +01:00
saimuel
eaf2b7bd0d
Add new ByseSX mainUrl (#2506) 2026-02-18 17:41:21 +01:00
firelight
6806a4e2e6
Revert "Update media3 to 1.9.2 (#2342)" (#2509)
This reverts commit ea4ef5c2f3.
2026-02-18 17:31:01 +01:00
firelight
8baee7ee78
Fix(UI): Minor color issue and padding on top chip bar 2026-02-18 00:10:36 +00:00
Luna712
ea4ef5c2f3
Update media3 to 1.9.2 (#2342) 2026-02-18 00:52:58 +01:00
Luna712
ad2168c5bc
Upgrade to minSdk 23 (#2078) 2026-02-18 00:31:01 +01:00
Luna712
76728d858f
Use withStyledAttributes (#2305) 2026-02-17 22:55:30 +01:00
Nivin
88d42613d3
Replace Google suggest API with TheMovieDB multi-search endpoint (#2500) 2026-02-17 21:53:07 +01:00
Mohd Kaif Shaikh
c862d119fb
add Videa.hu extractor (#2491)
* add Videa.hu extractor

* replace import android.util.Base64

* Refactor Videa extractor for improved URL handling
2026-02-17 21:48:29 +01:00
firelight
443c1c81c9
Refactor: Minor code cleanup for #2501 2026-02-17 20:45:39 +00:00
Swapnil Kuwar
8796a73f06
Search result provider pining feature added. (#2501) 2026-02-17 21:44:22 +01:00
Saurabh Kaperwan
960658df61
Remove hubcloud download link (#2502)
Sometimes it is making a get request to a video file
2026-02-17 21:28:03 +01:00
CranberrySoup
3da9b2ec7b
Better priority (#2496) 2026-02-13 19:59:29 +01:00
CranberrySoup
b58e0b893f
Fix resume watching (#2498) 2026-02-13 19:42:38 +01:00
firelight
f6339e44e1
Fix: YT Live + Subrip 2026-02-13 17:40:26 +00:00
Mohd Kaif Shaikh
32f1a2e6c3
Youtube Extractor Fix with NewPipe (#2489) 2026-02-13 18:39:29 +01:00
CranberrySoup
cf084ac2eb
Download rework (#2037) 2026-02-11 19:30:48 +01:00
Mioki
2c62f3fa46
Merge pull request #2478 from okibcn/0K_UqloadFixPR 2026-02-11 01:53:15 +01:00
firelight
8fcce6b5fd
Merge pull request #2488 from phisher98/Gdmirrorbot-improvement
GDMirrorbot Improvement
2026-02-11 00:43:58 +00:00
4530c00a71 Improvement 2026-02-09 19:28:54 +05:30
firelight
2766ac86a1
Merge pull request #2457 from recloudstream/weblate
Translations update from Hosted Weblate
2026-02-06 01:18:35 +00:00
Hosted Weblate
838989beaa
Merge remote-tracking branch 'origin/master' 2026-02-06 02:18:12 +01:00
Luna712
b370b5b9e7
Update gradle to 9.3.1 (#2477) 2026-02-06 01:18:05 +00:00
Hosted Weblate
30b5a4e649
Translated using Weblate (Belarusian)
Currently translated at 46.7% (334 of 715 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (German)

Currently translated at 99.1% (709 of 715 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (714 of 715 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (714 of 715 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (715 of 715 strings)

Translated using Weblate (Belarusian)

Currently translated at 40.1% (286 of 712 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (712 of 712 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (712 of 712 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (712 of 712 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (712 of 712 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (712 of 712 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (709 of 710 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (709 of 710 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (710 of 710 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (710 of 710 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (German)

Currently translated at 100.0% (709 of 709 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (706 of 709 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (French)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (709 of 709 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Polish)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (709 of 709 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (707 of 709 strings)

Co-authored-by: AlaxLima <thanhkhoidangngoc@gmail.com>
Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Artem <artemkozhin80@gmail.com>
Co-authored-by: BruttoDiego <bruttodiego6@gmail.com>
Co-authored-by: Bryan Tank <perso@bryantank.fr>
Co-authored-by: Christopher Allen <nsn90255@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Haru Ijima <haruijimakun@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kehribar <103407696+dpentx@users.noreply.github.com>
Co-authored-by: Kraptor123 <kraptor121@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mioki <okibcn@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: ShowhyT <showhy@proton.me>
Co-authored-by: Takeru Mikenu <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Theumis <me@david-hermann.de>
Co-authored-by: sam <cambridgeaccsamuel@gmail.com>
Co-authored-by: Дейв Рандом (MVboss1190) <m.v.boss123443211190@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2026-02-06 01:05:09 +01:00
Pawloland
a45593283d
Readd extra brightness feature as optional setting (#2469) 2026-02-03 20:19:25 +01:00
firelight
60244d86a4
Fix: Fixed stupid media3 parsing #2476 2026-02-03 19:07:56 +00:00
Phisher98
c25a9dc56b
Adding videoinfo optional in video Show player information (#2464)
* Adding videoinfo optional in video player resolution section
2026-02-02 18:28:56 +00:00
Luna712
6acc3d8f65
Run lint in pull requests (#2358) 2026-02-02 17:39:22 +00:00
Luna712
47b568c289
Update gradle to 9.3.0 (#2439)
* Update gradle to 9.3.0

* Update gradle-wrapper.properties
2026-02-02 17:34:40 +00:00
Mioki
045fc2770f
Fix track selection when same lang/id is used in 2+ audio tracks. Codec & channel configuration in track selection. Solves issue #2427 (#2447) 2026-02-01 18:13:48 +01:00
DieGon7771
bef80875b1
Add toggle for showing/hiding cast panel (#2466) 2026-02-01 17:58:19 +01:00
Mioki
c44d07b4e5
Vidmoly Fix and adds variants (#2470) 2026-02-01 17:49:48 +01:00
firelight
4e2bfd3d43
Fix: Logic bug in fixTitle, Closes #2465 2026-01-29 16:38:58 +00:00
firelight
06456bc548
Fix:(UI) Collapse sync to a single button, closes #2460 and closes #2458 2026-01-28 23:18:43 +00:00
firelight
af1e0757f4
Feat: Zoom (#2456)
* Feat: Zoom
2026-01-28 23:45:32 +01:00
firelight
4271b8104e
Fix(UI): Made outline consistent 2026-01-28 19:37:55 +00:00
CranberrySoup
5e039a80ba
Fix subtitle selection (#2449)
* Fix subtitle selection
2026-01-28 18:05:19 +01:00
firelight
c618e4e505
Fix: Minor fixes to #2454 2026-01-27 17:12:49 +00:00
Phisher98
cbad2cfdaf
Adding VideoInfo on Player (#2454)
Co-authored-by: Bnyro <bnyro@tutanota.com>
2026-01-27 18:06:27 +01:00
firelight
290283dc15
Chore: nicehttp -> 0.4.16 2026-01-26 22:17:31 +00:00
firelight
7ecb7785c2
Fix(UI): Move voice actor view behind actor view for better visibility 2026-01-26 22:11:12 +00:00
firelight
f82fe7b0ce
Bump to 4.6.2 2026-01-26 19:01:16 +00:00
Nivin
4b28140f8b
Add search suggestions to search UI (#2294) 2026-01-26 19:59:31 +01:00
Bnyro
6f1e4a959f
feat(extractors): add streamix extractor (streamup mirror) (#2455) 2026-01-26 19:49:37 +01:00
DieGon7771
0c25630f0b
Update VotingApi.kt (#2451) 2026-01-25 21:00:04 +01:00
firelight
f6f3e3ff73
Fix: Added backwards for subtitle+audio interceptor, Closes #2442 2026-01-25 19:53:39 +00:00
Osten
c1a2ae8704
Fixed #2448 and hdr by removing brightness filter 2026-01-24 20:06:54 +01:00
Yashas
fda9f0f8c0
feat: Add random play button to TV interface (#2430) 2026-01-24 15:15:21 +01:00
PiterDev
58ca69c284
Kitsu added as sync provider (#2440) 2026-01-24 15:04:37 +01:00
CranberrySoup
663c8a93cb
fix chapter skipping (#2444) 2026-01-24 14:44:10 +01:00
Osten
c28ee05bde
Added more software decoding options 2026-01-23 20:48:30 +01:00
firelight
58c84f0f33
Initial AI Policy (#2432) 2026-01-21 21:45:09 +01:00
Bnyro
7925aacf50
feat(extractors): add CineMM extractor (hglink, dhcplay, ...) (#2438) 2026-01-21 21:44:50 +01:00
Luna712
fb806b339f
Use wrap_content in trailer layout (#2437)
Consistent with other player layouts, and fixes error level lint.
2026-01-21 21:44:27 +01:00
Bnyro
2a60145314
feat(extractors): add vidoza/videzz extractor (#2436) 2026-01-21 21:43:37 +01:00
firelight
71b87d09e7
Fix: Video headers jank, Closes #2435 2026-01-21 17:50:41 +00:00
Cloudburst
8a4480dc42 Merge remote-tracking branch 'weblate/master' 2026-01-21 14:58:18 +01:00
Luna712
2abb7e2d3d
Merge pull request #2373 from Luna712/bump-json
Bump json lib
2026-01-19 23:18:03 +00:00
firelight
179f7e29eb
Merge pull request #2252 from Luna712/directories
Replace srcDirs with directories
2026-01-19 23:14:03 +00:00
firelight
f7a73bfb89
Merge pull request #2371 from recloudstream/weblate
Translations update from Hosted Weblate
2026-01-19 23:02:06 +00:00
Hosted Weblate
79801ec694
Translated using Weblate (Spanish)
Currently translated at 99.7% (704 of 706 strings)

Translated using Weblate (Dutch)

Currently translated at 87.8% (620 of 706 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'

Translated using Weblate (Russian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Romanian)

Currently translated at 87.3% (617 of 706 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 98.7% (697 of 706 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'

Translated using Weblate (Korean)

Currently translated at 96.8% (684 of 706 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (705 of 706 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'

Translated using Weblate (French)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Latvian)

Currently translated at 82.5% (583 of 706 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (705 of 706 strings)

Translated using Weblate (German)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Belarusian)

Currently translated at 25.7% (182 of 706 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (French)

Currently translated at 99.5% (703 of 706 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Belarusian)

Currently translated at 25.3% (179 of 706 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (French)

Currently translated at 99.0% (699 of 706 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (706 of 706 strings)

Co-authored-by: Daniel Navarro-Gomez <daniel.navarro.gomez@gmail.com>
Co-authored-by: Doen1el <weblate@mail.danielmuenstermann.de>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Friso de Boer <collorfrisie@hotmail.com>
Co-authored-by: Haru Ijima <haruijimakun@gmail.com>
Co-authored-by: Heeheon Ryu <heeheon.ryu001@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: João Pedro Nunes <nunes@disroot.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Pose marto <weblate.drainage895@passmail.net>
Co-authored-by: PrivateKeyy <rumaevvadim@gmail.com>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: Takeru Mikenu <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Tom G <luq6x@airsworld.net>
Co-authored-by: Wrapty <wrapty@gmail.com>
Co-authored-by: alex <contact@alexionut.ro>
Co-authored-by: avv-dev <vildan.abdullin@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: therry47 <soulietherry@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ro/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-01-19 23:02:05 +00:00
Luna712
fe9eedce39
Update Kotlin to 2.3.0 (#2341) 2026-01-19 23:01:46 +00:00
Hosted Weblate
317cb5dc1a
Translated using Weblate (Dutch)
Currently translated at 87.8% (620 of 706 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'

Translated using Weblate (Russian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Romanian)

Currently translated at 87.3% (617 of 706 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 98.7% (697 of 706 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'

Translated using Weblate (Korean)

Currently translated at 96.8% (684 of 706 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (705 of 706 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'

Translated using Weblate (French)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Latvian)

Currently translated at 82.5% (583 of 706 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (705 of 706 strings)

Translated using Weblate (German)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Belarusian)

Currently translated at 25.7% (182 of 706 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (French)

Currently translated at 99.5% (703 of 706 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Belarusian)

Currently translated at 25.3% (179 of 706 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (706 of 706 strings)

Translated using Weblate (French)

Currently translated at 99.0% (699 of 706 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (706 of 706 strings)

Co-authored-by: Daniel Navarro-Gomez <daniel.navarro.gomez@gmail.com>
Co-authored-by: Doen1el <weblate@mail.danielmuenstermann.de>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Friso de Boer <collorfrisie@hotmail.com>
Co-authored-by: Heeheon Ryu <heeheon.ryu001@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: João Pedro Nunes <nunes@disroot.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Pose marto <weblate.drainage895@passmail.net>
Co-authored-by: PrivateKeyy <rumaevvadim@gmail.com>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: Takeru Mikenu <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Tom G <luq6x@airsworld.net>
Co-authored-by: Wrapty <wrapty@gmail.com>
Co-authored-by: alex <contact@alexionut.ro>
Co-authored-by: avv-dev <vildan.abdullin@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: therry47 <soulietherry@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ro/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2026-01-19 09:01:52 +00:00
Luna712
ee1009a4e3
Don't suppress SimpleDateFormat lint in BackupUtils (#2372)
Just use `Locale.getDefault()` like we do everywhere else in the app.
2026-01-17 15:03:03 +01:00
gittinrep
dd3be24db1
Removes check for available devices before displaying chromecast button (#2412)
Once the button is selected, the chrome cast service will search for devices and notify the user if none have been found
2026-01-17 15:02:38 +01:00
firelight
7b00fce5c0
Fix: Remove duplicate string and unused code from #2416 2026-01-17 14:57:36 +01:00
Pawloland
ed759d6f50
Add extra brightness feature (#2416) 2026-01-17 14:55:54 +01:00
Phisher98
0431d879e3
Minor fix for HomeHead BG poster adding back shadow and minor fixes (#2422)
* Minor fix for HomeHead BG poster adding back shadow and minor fixes
2026-01-17 14:37:15 +01:00
Phisher98
66cf668c58
Minor fix for HomeHead BG poster dull #2409 (#2420) 2026-01-16 11:31:20 +01:00
firelight
ecd8f8b3f6
Fix: Styling fixes on white theme for hero banner #2409 (Part 2) 2026-01-15 20:17:20 +01:00
firelight
51ab4bfffb
Fix: Code cleanup and edgecases fix for hero card #2409 (Part 1) 2026-01-15 19:59:11 +01:00
Jaidev
3b28313fb5
Enhance Hero Card with Dynamic Rating Badge and Larger Layout (#2409) 2026-01-15 19:26:27 +01:00
Bnyro
91983b38ea
feat(extractors): add stream wish mirror streamhls.to (#2397)
- example url: https://streamhls.to/e/uszo5fi7zdda?33
2026-01-12 17:12:59 +01:00
rockhero1234
6090d0f219
Episode title in downloaded & offline logo (#2375) 2026-01-12 17:10:17 +01:00
Phisher98
124288c829
Add background posters on mobile (vertical) (#2408)
* Add background posters on mobile (vertical)
2026-01-12 16:54:43 +01:00
saimuel
1abb9d35ae
Add new StreamWish mainUrl (#2415) 2026-01-12 15:34:06 +01:00
Kraptor123
79d82b3150
fix: make YouTube subtitles nullable (#2405)
* fix: make YouTube subtitles nullable

* Add the comment back
2026-01-11 19:31:53 +01:00
firelight
73f258ca17
Fix: logoUrl final fix 2026-01-09 03:33:26 +01:00
firelight
bd7a90b064
Fix: Phone bindLogo + Prerelease annotation on logo 2026-01-09 03:31:40 +01:00
firelight
57c3d332ae
Fix: Race conditon fix, and code cleanup 2026-01-09 03:24:18 +01:00
Phisher98
fbc588b173
Add logo image (#2384) 2026-01-09 03:21:05 +01:00
firelight
2b7ff8b336
Fix: YT cleanup and subtitle fix 2026-01-09 02:23:37 +01:00
Kraptor123
2aaf99b3fd
Youtube Extractor now plays videos up to 1080p (#2386) 2026-01-09 02:22:47 +01:00
Bnyro
5c0f715973
feat(extractors): add gupload.xyz extractor (#2391) 2026-01-05 02:28:23 +01:00
saimuel
368ee2aca9
new mainUrl DoodExtractor (#2388) 2026-01-05 02:27:25 +01:00
Bnyro
2129c2f982
feat(extractors): add streamembed extractor (#2394) 2026-01-05 02:26:41 +01:00
Kraptor123
f84414dbf6
New Theme - Silent Blue (#2392) 2026-01-04 11:35:15 +01:00
rockhero1234
5e54552338
remove check icon in tvtype chips (#2363) 2026-01-04 11:29:04 +01:00
Bnyro
dc6b9f435d
feat(extractors): add up4stream extractor (#2389) 2026-01-04 10:09:50 +01:00
Bnyro
2795e9e0e2
feat(extractors): add vidnest extractor (#2390) 2026-01-04 10:08:15 +01:00
Luna712
81d9ecde67
Move untranslatable strings to seperate file (#2273)
This could cause crashes or poisoned data on some languages as some untranslatable strings were being translated, including keys and format strings that shouldn't be translatable. Also when translating the episodes key on weblate it caused a conflict between the plural version (which weblate does support) and the actual episodes key, meaning the episodes key was translating as the singular version of the plural episodes version in some cases. Moving to a separate resource file should hopefully prevent these issues.
2025-12-24 02:21:59 +00:00
firelight
b7e38ebc4e
Merge pull request #2361 from recloudstream/weblate
Translations update from Hosted Weblate
2025-12-24 02:07:55 +00:00
Hosted Weblate
ea194d9b48
Merge remote-tracking branch 'origin/master' 2025-12-24 02:07:14 +00:00
Luna712
3fe6a7853a
Replace QuickJS with Zipline (#2256)
QuickJS was renamed to Zipline all the way back in 2021. Unlike old QuickJS, newer Zipline versions are 16kb aligned. Current Zipline is also compatible back to minSdk 21.
2025-12-24 02:07:08 +00:00
Hosted Weblate
fb0df7991c
Merge remote-tracking branch 'origin/master' 2025-12-24 01:57:37 +00:00
Luna712
063d960c3a
Pin rhino version (#2369) 2025-12-24 01:57:28 +00:00
Hosted Weblate
7fd4902180
Translated using Weblate (Belarusian)
Currently translated at 33.4% (277 of 828 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Belarusian)

Currently translated at 27.5% (228 of 828 strings)

Translated using Weblate (Latvian)

Currently translated at 84.9% (703 of 828 strings)

Translated using Weblate (Belarusian)

Currently translated at 25.4% (211 of 828 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Belarusian)

Currently translated at 23.0% (191 of 828 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (828 of 828 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: soldado-do-wolfenstein <luigi.rebelato1234@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
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/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2025-12-23 20:00:29 +01:00
Luna712
6c2228b964
Improve caching system for actions (#2249) 2025-12-21 02:48:37 +00:00
Luna712
be78306c55
Minor order fix for lint plugin (#2355) 2025-12-21 02:22:07 +00:00
recloudstream[bot]
bc68b3d7c6 chore(locales): fix locale issues 2025-12-21 02:19:29 +00:00
firelight
8067742192
Merge pull request #2340 from recloudstream/weblate
Translations update from Hosted Weblate
2025-12-21 02:19:17 +00:00
firelight
bdcb9b4807
Merge branch 'master' into weblate 2025-12-21 02:17:33 +00:00
Luna712
0593cfbc01
Add linting to library (#2301) 2025-12-21 02:11:54 +00:00
Luna712
caf6704c54
Merge branch 'recloudstream:master' into directories 2025-12-20 18:36:50 -07:00
recloudstream[bot]
0d77f7b91a chore(locales): fix locale issues 2025-12-21 01:27:09 +00:00
Hosted Weblate
b3c44becc7
Merge remote-tracking branch 'origin/master' 2025-12-21 02:26:58 +01:00
Luna712
db2ef08b0a
Add lint.xml and add ignores to it (#2300)
It's more expandable later on this way.
2025-12-21 01:26:53 +00:00
Luna712
568223a496
Use new KMP plugin for library (#2251)
For compatibility with AGP 9, but this works on past AGP also, so we do it now to prepare for AGP 9 and make that simpler.
2025-12-21 01:22:16 +00:00
Hosted Weblate
fd11b5dd6a
Translated using Weblate (Czech)
Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Belarusian)

Currently translated at 20.8% (172 of 825 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Belarusian)

Currently translated at 14.7% (122 of 825 strings)

Added translation using Weblate (Belarusian)

Translated using Weblate (Hindi)

Currently translated at 64.1% (529 of 825 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (822 of 825 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (825 of 825 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Latvian)

Currently translated at 84.9% (701 of 825 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (825 of 825 strings)

Translated using Weblate (Spanish)

Currently translated at 98.4% (811 of 824 strings)

Translated using Weblate (German)

Currently translated at 99.8% (823 of 824 strings)

Co-authored-by: Adrian Rodriguez Rodriguez <arrodriguez1809@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Oliver <oliver.puff@gmx.de>
Co-authored-by: P Patel <pdvadalia2007@gmail.com>
Co-authored-by: Sasha Glazko <lenify@tutanota.com>
Co-authored-by: Takeru Mikenu <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Thiago Papageorgiou <tnpapa.simply253@aleeas.com>
Co-authored-by: korn3r <korn3r@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/be/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/be/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2025-12-21 02:01:18 +01:00
Luna712
9cd6e64120
Don't remove trailing newlines in fix locales (#2238)
It seems that weblate actually does add newlines so "to be consistent" we shouldn't remove them. I'm not 100% sure if this just changed at some point or if we do actually want to remove them still.
2025-12-21 01:00:51 +00:00
Luna712
ba74a9062f
Fix broken seeking on some fragmented MP4 files (#2352)
Seek doesn't work (only shows duration of a few seconds and seeking then restarts it) on some fragmented MP4 files if they have multiple sidx boxes. Setting this flag merges those and thus will allow seeking. This can be tested using https://github.com/androidx/media/blob/release/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_seekable_multiple_sidx.mp4
2025-12-20 22:29:23 +01:00
rockhero1234
c048426601
feat:separate episode text in continue watching (#2093) 2025-12-20 16:52:33 +01:00
Luna712
7ef57537ac
Bump material (#2348) 2025-12-20 16:07:48 +01:00
Luna712
bac6939444
Use VERSION_NAME from BuildConfig for app version (#2337) 2025-12-18 19:24:17 +01:00
firelight
ac6b20c178
Fix: Circular progress track color, Closes #2346 2025-12-18 18:20:30 +00:00
rockhero1234
47fac5dd4d
replaybtn (#2335) 2025-12-18 16:09:48 +01:00
Luna712
729ede5484
Fix scroll issue on bottom dialogs and add drag handle to UI (#2333) 2025-12-17 21:40:48 +01:00
rockhero1234
e97645c753
open in browser btn:original trailer url instead of extractedlink (#2332) 2025-12-17 21:34:11 +01:00
firelight
bd77965efb
Merge pull request #2307 from Luna712/minimum-duration-api
Remove "Too short playback" error
2025-12-16 22:16:29 +00:00
firelight
764201bbbd
Chore: Cleanup InAppUpdater 2025-12-16 21:56:26 +00:00
firelight
7cb8e7c846
Merge pull request #2326 from Luna712/remove-prerelease-setting
Replace the "Update to prereleases" setting with a new button
2025-12-16 21:50:48 +00:00
Luna712
8012339137
Merge branch 'recloudstream:master' into remove-prerelease-setting 2025-12-16 14:27:47 -07:00
firelight
1e9e9b4173
Fixed issue with selecting subtitles and source at the same time, Closes #2339 2025-12-16 21:19:34 +00:00
firelight
c555ec1375
Merge pull request #2338 from Luna712/no-install-backup
Don't create backup on first install
2025-12-16 17:31:36 +00:00
Luna712
fc1c0fc1c5
Don't create backup on first install
There is no reason to create a mostly empty backup when the app is first installed.
2025-12-15 15:06:56 -07:00
Luna712
5a3158ce6f
No change there 2025-12-15 11:39:58 -07:00
Luna712
ce944a9193
Remove entirely 2025-12-15 11:38:41 -07:00
Luna712
f684bd59b5
Merge branch 'master' into remove-prerelease-setting 2025-12-15 10:58:33 -07:00
recloudstream[bot]
3c9120f6fa chore(locales): fix locale issues 2025-12-15 17:55:11 +00:00
firelight
66ef8518b8
Merge pull request #2257 from recloudstream/weblate
Translations update from Hosted Weblate
2025-12-15 17:54:57 +00:00
Hosted Weblate
6c8c93958f
Merge remote-tracking branch 'origin/master' 2025-12-15 18:52:05 +01:00
rockhero1234
249b36e8b3
tv episodes layout minor fix (#2334) 2025-12-15 17:52:00 +00:00
Hosted Weblate
e88aa3be44
Merge remote-tracking branch 'origin/master' 2025-12-15 18:47:34 +01:00
Luna712
aa08fa126b
Bump AGP to 8.13.2 (#2318) 2025-12-15 17:47:30 +00:00
Luna712
604a9197d4
Add param doc 2025-12-15 10:18:13 -07:00
Hosted Weblate
97e1c1d3ff
Merge remote-tracking branch 'origin/master' 2025-12-15 00:41:10 +01:00
Nivin
029b72c17b
Add support for external audio tracks in player (#2288) 2025-12-15 00:41:02 +01:00
Hosted Weblate
c74f5dc61f
Merge remote-tracking branch 'origin/master' 2025-12-14 23:25:10 +00:00
Luna712
45cd0e6e3f
Add helper for default back press callback handling (#2243) 2025-12-15 00:25:04 +01:00
Luna712
42a1e58527
Remove native-lib.cpp (#2331)
It was first disabled in 8193e39 and eventually later on in 
f5d1f68 CMakeLists was removed completely so removing this should be fine too.
2025-12-13 23:45:06 +01:00
Hosted Weblate
362949bc23
Translated using Weblate (Latvian)
Currently translated at 85.0% (701 of 824 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Latvian)

Currently translated at 83.9% (692 of 824 strings)

Translated using Weblate (Hindi)

Currently translated at 54.2% (447 of 824 strings)

Translated using Weblate (Latvian)

Currently translated at 83.8% (691 of 824 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'

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Latvian)

Currently translated at 83.7% (690 of 824 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Latvian)

Currently translated at 82.7% (682 of 824 strings)

Translated using Weblate (Romanian)

Currently translated at 88.8% (732 of 824 strings)

Translated using Weblate (Latvian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Latvian)

Currently translated at 77.5% (639 of 824 strings)

Translated using Weblate (Latvian)

Currently translated at 76.0% (627 of 824 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 (French)

Currently translated at 99.7% (822 of 824 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'

Translated using Weblate (French)

Currently translated at 99.7% (822 of 824 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (822 of 824 strings)

Translated using Weblate (Tamil)

Currently translated at 96.3% (794 of 824 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (824 of 824 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (824 of 824 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'

Translated using Weblate (Turkish)

Currently translated at 100.0% (829 of 829 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Macedonian)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (829 of 829 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Italian)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (829 of 829 strings)

Co-authored-by: Anthony Cyndora <anthony270777@gmail.com>
Co-authored-by: Esat Tuna BECAN <esattunabecan4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jaidev Subramanian <tarunjai415@gmail.com>
Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Co-authored-by: Kerim Demirkaynak <aschannel111@gmail.com>
Co-authored-by: MagElwis <m.mhelheli@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Mateus Liberale Gomes <sergiogomes209403@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mikenu Takeru <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Pascariu Alex <pascariu610@gmail.com>
Co-authored-by: QSkill <QSkull@protonmail.com>
Co-authored-by: Sisitenr <sisiton2019@gmail.com>
Co-authored-by: VKing9 <vaibhavrathod2282@gmail.com>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: ℂ𝕠𝕠𝕠𝕝 (𝕘𝕚𝕥𝕙𝕦𝕓.𝕔𝕠𝕞/ℂ𝕠𝕠𝕠𝕝) <coool@mail.lv>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/lv/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2025-12-13 18:00:23 +00:00
Luna712
5ec30d64d2
Fix comment 2025-12-12 23:44:36 -07:00
Luna712
77e4ca32ec
Use install prerelease button preference 2025-12-12 23:17:08 -07:00
Luna712
5c396013b8
Update upload-artifact action to v6 (#2325) 2025-12-13 04:54:22 +01:00
Phisher98
15c70ded91
Adding ByseSX Extractor (Filemoon New Domain) (#2310) 2025-12-13 04:54:10 +01:00
Luna712
50173aaaf6
Remove "Update to prereleases" setting
This setting is very confusing on stable, when there is a seperate flavor for prerelease that is needed. Also causes other issues and conflicts with what you are updating on stable, and is annoying to always get update dialogs on debug versions when it doesn't work or do anything in debug version.
2025-12-12 17:17:49 -07:00
Luna712
6aa856e93c
Readd SearchAutoComplete check for now (#2322)
Seems to break focus on TV, not sure why, I can figure out why later, but for now just fixing like this as this is a critical bug, in my opinion.
2025-12-11 22:24:50 +01:00
Luna712
eaf2ac0792
Add some tools:targetApi to styles to appease lint (#2319)
Part of my work to fix all error level lint issues, in order to eventually enable `failOnError` and ensure better compatability with older API levels and a more consistent reporting of issues.
2025-12-11 21:04:11 +01:00
Luna712
5d2e432614
Make InAppUpdater an object (#2321)
Better than a class with only a companion object I think.
2025-12-11 21:03:37 +01:00
Luna712
70121f4548
Fix crash on Android 5 (#2320)
I just realized I hadn't done a PR to fix this issue yet but this issue is why I've been working on fixing all error level lint issues so that we can enable `failOnError` which would have prevented this.
2025-12-11 20:41:51 +01:00
Luna712
a836b26849
Cleanup InAppUpdater (#2298)
The only functional change here is that the commit in the updater dialog was normalized to what it is everywhere else, meaning it is 7 not 10 characters now.

I also have another patch prepared to convert this entire class to an actual object rather than just a class with only a companion object but since that touches every single line due to indentation changes, I decided to split it in order to make it easier to review.
2025-12-11 20:32:32 +01:00
Luna712
74ceaf9a3f
Add a lint suppression for RestrictedApi (#2312) 2025-12-11 20:27:36 +01:00
Luna712
350d19bd6b
Some minor miscellaneous cleanup (#2306)
* Some minor miscellaneous cleanup

* Remove classes
2025-12-11 18:27:54 +01:00
Luna712
7ded6a4fa1
Add tools:targetApi to appease lint (#2315)
Part of my work to fix all error level lint issues, in order to eventually enable `failOnError` and ensure better compatability with older API levels and a more consistent reporting of issues.
2025-12-11 17:56:34 +01:00
Luna712
9a9e71354c
Remove check for SearchAutoComplete (#2313) 2025-12-11 17:55:00 +01:00
rockhero1234
e0231520d5
added mpvex (#2309) 2025-12-11 17:37:19 +01:00
Luna712
ae5e25726d
Use String.toUri consistently (#2304) 2025-12-11 17:31:36 +01:00
Luna712
d5eba57bc0
Cleanup UnstableApi usage (#2314)
* Remove `@UnstableApi` from GeneratorPlayer and use OptIn instead.
* Remove `@OptIn` from WebviewFragment as it was unnecessary.
* Move `@OptIn` in SaveCaptionStyle to the actual single line we need to OptIn.
* Split `setCues` logic to a new method in ChromcastSubtitlesFragment and only add `@OptIn` to that method as it's only necessary there.
* Add some missing `@OptIn` annotations to fix all remaining `UnsafeOptInUsageError` lint errors.
2025-12-11 17:20:22 +01:00
Luna712
8fabb5c572
Suppress an UnspecifiedRegisterReceiverFlag lint issue (#2316)
Part of my work to fix all error level lint issues, in order to eventually enable `failOnError` and ensure better compatability with older API levels and a more consistent reporting of issues.
2025-12-11 17:17:25 +01:00
Osten
a46b0ac6e6
Download selection fix + sub del fix + Del dialog fix (#2308) 2025-12-08 22:35:11 +01:00
Luna712
e25847cb64
Add API for minimum media duration 2025-12-07 15:24:42 -07:00
Luna712
1a852f1f4c
Use SharedPreferences.edit extension function (#2299) 2025-12-06 15:35:14 +00:00
Luna712
fdad31c10e
Add backward compatibility for one more AcraApplication method (#2302)
`removeKeys()` only seems to be used by one single extension, but I suppose it doesn't hurt to still add back compat for it.
2025-12-06 15:29:57 +00:00
Luna712
f77df2f3bf
Use temurin distribution for setup-java action (#2297)
Per the note on the README for `actions/setup-java`: "AdoptOpenJDK got moved to Eclipse Temurin and won't be updated anymore. It is highly recommended to migrate workflows from `adopt` and `adopt-openj9`, to `temurin` and `semeru` respectively, to keep receiving software and security updates."
2025-12-05 01:52:24 +01:00
Luna712
f2a008922d
Bump rhino to 1.8.1 (#2295) 2025-12-05 01:51:12 +01:00
Luna712
472d0bab8b
Remove unused swiperefreshlayout dependency (#2296) 2025-12-05 01:48:29 +01:00
Luna712
93255dfc22
Add explicit dependency on fragment (#2233)
As with some of my other PRs, explicit dependencies allow for better version control.
2025-12-05 01:33:30 +01:00
Luna712
e47349af7a
Bump material (#2241) 2025-12-05 01:25:18 +01:00
Luna712
cd69597a54
Move app version to BuildConfig (#2291)
Also, the intent seems to be to be to set the version to `-PRE` when in pre release, which doesn't currently work, but this fixes that.
2025-12-05 01:20:08 +01:00
Luna712
b2e06c5966
Remove BuildConfig.BETA (#2290)
It's unused and can be accessed with `BuildConfig.FLAVOR == "prerelease"`
2025-12-05 01:13:38 +01:00
Luna712
2c0fa70101
Clear home page adapter pools when reloading (#2272) 2025-12-05 01:12:14 +01:00
Luna712
0b3aa24e66
Some cleanup/improvements to layouts (#2274) 2025-12-05 01:09:54 +01:00
firelight
d4d273f010
Fix: Configuration change view invalidation on AutofitRecyclerView popup 2025-12-04 23:50:56 +00:00
rockhero1234
81b2718129
horizontal poster in expanded list (#2286) 2025-12-05 00:34:52 +01:00
Luna712
2ac0698bd2
Handle new Android 16 biometrics error type (#2275)
Adds handling for `BiometricManager.BIOMETRIC_ERROR_NOT_ENABLED_FOR_APPS` which was added in API level 36.
2025-11-30 19:32:20 +00:00
Luna712
1dd477a965
Disable MissingTranslation lint (#2276)
Translations are handled by weblate, so we don't really care about missing translations here.
2025-11-30 19:21:09 +00:00
Luna712
110cf571bd
Fix no poster showing wrong poster (#2278) 2025-11-30 19:19:52 +00:00
Luna712
dad6b92ae3
Fix downloads loading background (#2279) 2025-11-30 19:15:11 +00:00
Luna712
d794f6182e
Add Prerelease annotation to extractors that are not in stable (#2281) 2025-11-30 19:11:05 +00:00
Osten
b68fadc956
Minor fixes to recycled DownloadAdapter cards 2025-11-30 00:24:08 +01:00
Osten
1aa6a6215d
Minor fix to ConsistentLiveData 2025-11-28 21:37:55 +01:00
Osten
38296bfb1a Fixed the atrocity of download selection along with some crash fixes and bugs. 2025-11-28 21:24:31 +01:00
Luna712
b05ccb2bc7
Merge branch 'recloudstream:master' into directories 2025-11-28 10:57:57 -07:00
Luna712
7fb6f3f535
Add explicit dependsOn for copyJar (#2261) 2025-11-27 18:37:47 +00:00
Luna712
d43a371b15
Better backward compatibility for AcraApplication (#2265) 2025-11-27 18:34:14 +00:00
Luna712
9d651f1f82
Remove work-runtime dependency (#2234)
We only really need to include the Kotlin version, work-runtime-ktx here.
2025-11-25 15:24:21 +01:00
Luna712
7f9f89cbf6
Use version catalog bundles for coil and lifecycle (#2237) 2025-11-25 15:16:37 +01:00
Luna712
009dcc2b89
Use version catalog for plugins (#2206) 2025-11-24 18:26:07 +01:00
Luna712
3be396216f
Remove acra and replace AcraApplication with CloudStreamApp (#2207) 2025-11-24 18:04:51 +01:00
Luna712
a95d8ddc78
Remove unnecessary overrideLibrary for torrServer (#2235) 2025-11-24 17:53:55 +01:00
rockhero1234
f2de69a1ee
UI improvement (#2209) 2025-11-24 17:50:32 +01:00
firelight
6332f1c344
Merge pull request #2239 from Luna712/panels-backpress
Close overlapping panels on back press
2025-11-24 16:33:53 +00:00
Luna712
2111049cec
Use unique callback ID 2025-11-24 08:50:40 -07:00
firelight
c310ee7ed0
Merge pull request #2250 from Luna712/resvalues
Enable resValues in buildFeatures
2025-11-24 14:31:14 +00:00
firelight
9a7654ebb7
Merge pull request #2258 from phisher98/HubCloud-Improve
Hub cloud improve
2025-11-24 14:29:41 +00:00
075c7440bf HubCloud Improvement 2025-11-24 12:49:37 +05:30
Phisher98
dc2fc15ae3
Merge branch 'recloudstream:master' into master 2025-11-24 12:41:00 +05:30
recloudstream[bot]
511536fda7 chore(locales): fix locale issues 2025-11-23 21:11:24 +00:00
firelight
e40cc27369
Translated using Weblate (Indonesian) (#2236)
Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (829 of 829 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Italian)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (829 of 829 strings)










Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App

Co-authored-by: Esat Tuna BECAN <esattunabecan4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mikenu Takeru <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-11-23 21:11:11 +00:00
Luna712
0caddd84d4
Minor cleanup to workflows (#2255) 2025-11-23 21:09:41 +00:00
Luna712
a9771d8a03
Replace srcDirs with directories
This is an error in AGP 9, but works in past AGP releases also. Just to prepare for AGP 9 we can just do this now.
2025-11-22 14:37:49 -07:00
Luna712
003544ad44
Enable resValues in buildFeatures
This will be required in AGP 9, but just to prepare for that, we can do it now. Only the default changed, this should have no effect on older AGP versions.
2025-11-22 13:34:32 -07:00
Hosted Weblate
9d20b9f322
Merge remote-tracking branch 'origin/master' 2025-11-22 20:18:44 +00:00
Luna712
f4b5acd401
Bump checkout action (#2242) 2025-11-22 20:18:37 +00:00
Hosted Weblate
a4ab7086a7
Merge remote-tracking branch 'origin/master' 2025-11-22 21:18:03 +01:00
Phisher98
3a5e29b77d
Vidsrc is Dead and Minor Fix for hubcloud (#2245) 2025-11-22 20:17:57 +00:00
Hosted Weblate
e2622b557c
Merge remote-tracking branch 'origin/master' 2025-11-22 21:13:16 +01:00
Luna712
36594604a8
Migrate freeCompilerArgs to new DSL (#2248)
`jvmDefault.set(JvmDefaultMode.ENABLE)` is equivalent to `-jvm-default=enable`, which replaces `-Xjvm-default=all-compatibility` in Kotlin 2.2, it doesn't yet warn, but does in Kotlin 2.3.

https://youtrack.jetbrains.com/issue/KT-61649
https://youtrack.jetbrains.com/issue/KT-74590
https://youtrack.jetbrains.com/issue/KT-76353
https://kotlinlang.org/docs/gradle-compiler-options.html#migrate-freecompilerargs
2025-11-22 20:13:11 +00:00
0b310ef87c Vidsrc is Dead and Minor Fix for hubcloud 2025-11-21 23:57:55 +05:30
6dabce4e93 Vidsrc is Dead and Minor Fix for hubcloud 2025-11-21 23:42:11 +05:30
90091eb305 Vidsrc is Dead and Minor Fix for hubcloud 2025-11-21 23:41:25 +05:30
Hosted Weblate
a4a2604962
Translated using Weblate (Indonesian)
Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (829 of 829 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Italian)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (829 of 829 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (829 of 829 strings)

Co-authored-by: Esat Tuna BECAN <esattunabecan4@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mikenu Takeru <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: opakholis <opakholis@users.noreply.hosted.weblate.org>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2025-11-21 07:51:24 +00:00
Luna712
032c8404d9
Close overlapping panels on back press 2025-11-19 15:19:46 -07:00
Osten
a3a7b7067b
Include submitList commitCallback to BaseAdapter (from QuickNovel) 2025-11-18 20:29:13 +01:00
Luna712
29f8f02eef
Merge pull request #2146 from Luna712/toolchain
Add jdk toolchain
2025-11-18 17:22:57 +00:00
recloudstream[bot]
4bf659e44a chore(locales): fix locale issues 2025-11-18 17:08:49 +00:00
firelight
d83f324457
Merge pull request #2181 from recloudstream/weblate
Translations update from Hosted Weblate
2025-11-18 17:08:32 +00:00
Hosted Weblate
94552c1a66
Merge remote-tracking branch 'origin/master' 2025-11-18 18:07:23 +01:00
Luna712
10192553ce
Upgrade Gradle wrapper to 9.2.1 (#2230) 2025-11-18 17:07:17 +00:00
rockhero1234
dcc107df09
full series btn (#2220) 2025-11-18 16:18:24 +01:00
Hosted Weblate
34a1a38be0
Merge remote-tracking branch 'origin/master' 2025-11-18 15:12:50 +00:00
Luna712
1f58160630
UI fixes for portrait subtitle offset dialog (#2228) 2025-11-18 16:12:45 +01:00
Hosted Weblate
1d61894d2d
Merge remote-tracking branch 'origin/master' 2025-11-18 15:02:05 +00:00
Luna712
96558ba61b
Handle out of range error in player (#2215) 2025-11-18 16:01:56 +01:00
Kraptor123
c0d5356d81
VkExtractor (#2223) 2025-11-18 15:59:26 +01:00
Hosted Weblate
d75c084b3a
Translated using Weblate (English)
Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (826 of 828 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (828 of 828 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (828 of 828 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'

Translated using Weblate (Polish)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Assamese)

Currently translated at 91.7% (760 of 828 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 97.4% (807 of 828 strings)

Translated using Weblate (Filipino)

Currently translated at 21.0% (174 of 828 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (German)

Currently translated at 99.7% (826 of 828 strings)

Translated using Weblate (Spanish)

Currently translated at 98.6% (817 of 828 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Tamil)

Currently translated at 96.1% (796 of 828 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Turkish)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Swedish)

Currently translated at 93.2% (772 of 828 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Macedonian)

Currently translated at 99.7% (826 of 828 strings)

Translated using Weblate (Italian)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.7% (826 of 828 strings)

Translated using Weblate (Croatian)

Currently translated at 98.1% (813 of 828 strings)

Translated using Weblate (French)

Currently translated at 98.4% (815 of 828 strings)

Translated using Weblate (Greek)

Currently translated at 94.9% (786 of 828 strings)

Translated using Weblate (Czech)

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (825 of 828 strings)

Translated using Weblate (Bulgarian)

Currently translated at 96.3% (798 of 828 strings)

Translated using Weblate (Arabic)

Currently translated at 98.5% (816 of 828 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (828 of 828 strings)

Co-authored-by: Ahmed Al-Nassif <mr.ahmed.nassif@gmail.com>
Co-authored-by: Carrillo Rodriguez <carrillorodriguez672@gmail.com>
Co-authored-by: Deleted User <anonymous2676@users.noreply.hosted.weblate.org>
Co-authored-by: Deleted User <noreply+110771@weblate.org>
Co-authored-by: Esspel <eric.soderstrom06@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Itsmechinmoy <167056923+itsmechinmoy@users.noreply.github.com>
Co-authored-by: Ivan Kostov <jiveq1@gmail.com>
Co-authored-by: John Kennedy Peña <jkhp.jkpa@gmail.com>
Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Co-authored-by: Konstantin <konstantinkreutz@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mikenu Takeru <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Rere Doloi <reredolire@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Serdar Sağlam <teknomobil@msn.com>
Co-authored-by: Sergey Ponomarev <stokito@gmail.com>
Co-authored-by: g333fed <fedorn990@gmail.com>
Co-authored-by: leyakid803 <leyakid803@minduls.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: william piti <loolyowo@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2025-11-17 12:51:29 +01:00
firelight
0579a15f9b
Fix: PIP problems caused by #2199 + Old PIP problems on low API versions. Closes #2202 2025-11-13 13:58:57 +00:00
Luna712
ac3d057358
Upgrade to targetSdk 36 (#2043) 2025-11-12 17:17:30 +01:00
rockhero1234
5370d019ff
minor ui fixes (#2191) 2025-11-12 17:10:52 +01:00
Luna712
73a98be437
Add explicit jsoup dependency (#2143)
We use it in both app and library, allows better dependency management, and also when I was testing AGP 9 alpha with built in Kotlin this was needed for it not to fail, so just doing this now to kinda prepare a little for that, even though it's a long ways out since dokka also doesn't fully support it yet and it's still in alpha, not stable yet.
2025-11-12 17:09:55 +01:00
rockhero1234
e1aec1d1d3
fix auto subtiltle select downloaded (#2200) 2025-11-12 17:01:21 +01:00
Luna712
ed55cb2a0b
Lock orientation when using the rotate button (#2172) 2025-11-12 00:42:26 +01:00
Luna712
523fbc1325
Bump AGP to 8.13.1 (#2186) 2025-11-12 00:29:26 +01:00
firelight
cc242088cf
Merge pull request #2188 from Luna712/player-scroll-fix
Reset player hide delay when scrolling controls
2025-11-11 23:20:33 +00:00
firelight
7b5e213c1f
Merge pull request #2196 from Luna712/packages
Add some missing explicit packages and minor cleanup
2025-11-11 23:12:17 +00:00
firelight
501186a004
Merge pull request #2169 from Luna712/items-size
More response player quality profile items
2025-11-11 22:56:36 +00:00
Luna712
6a5f0815c1
Use 110dp 2025-11-11 15:37:54 -07:00
firelight
a7e8b1bee3
Merge pull request #2184 from Luna712/fix-future-build
Fix one more build issue on future version of Kotlin
2025-11-11 22:35:52 +00:00
Luna712
4107fe1767
Merge branch 'master' into packages 2025-11-11 15:27:05 -07:00
firelight
0ad0577b17
Merge pull request #2195 from Luna712/activity-ktx
Use activity-ktx version
2025-11-11 22:25:05 +00:00
firelight
808b08e1ab
Merge pull request #2187 from Luna712/replace-url-constructor
Replace deprecated URL constructor
2025-11-11 22:19:10 +00:00
firelight
bdab23d967
Merge pull request #2199 from Luna712/pip-buffering
Allow to enter PIP mode if buffering
2025-11-11 22:17:46 +00:00
firelight
eff2c81cde
Merge pull request #2192 from rockhero1234/overlay_fix
retain previous play status ep. overlay
2025-11-11 22:15:42 +00:00
firelight
feea825ba6
Merge pull request #2197 from Luna712/cronet
Enable http2 and brotli in cronet
2025-11-11 22:08:27 +00:00
firelight
217bbeb3ca
Merge pull request #2198 from Luna712/patch-15
Minor update to english translation for a string
2025-11-11 22:07:26 +00:00
Luna712
37e550adce
Allow to enter PIP mode if buffering 2025-11-11 14:55:33 -07:00
Luna712
1ad6ab3411
Minor update to english translation for a string
This may not only be the case for phones but other devices as well.
2025-11-11 13:48:57 -07:00
Luna712
70a810656a
Enable http2 and brotli in cronet
Not everything supports QUIC.
2025-11-11 12:21:54 -07:00
Luna712
7af45a8318
Add some missing explicit packages and minor cleanup 2025-11-11 11:12:40 -07:00
Luna712
7ea89f7b91
Use activity-ktx version 2025-11-11 10:52:32 -07:00
rockhero1234
1cb8171aa5 retain previous play status ep. overlay 2025-11-11 16:54:54 +05:30
Luna712
f834c851b3
Reset player hide delay when scrolling controls
This also increases the delay time just a bit as before it seems it is to fast once scroll finishes if you don't think fast enough, however, I can remove that part if it isn't wanted.
2025-11-10 15:25:07 -07:00
Luna712
d82f471e82
Replace deprecated URL constructor 2025-11-10 12:30:15 -07:00
Luna712
ea474eeef4
Fix one more build issue on future version of Kotlin
It's a warning on Kotlin 2.3 and will be an error in 2.4:

`Type annotation class 'Nullable' of the inferred type is inaccessible. Check the module classpath for missing or conflicting dependencies. This will become an error in language version 2.4.`
2025-11-10 10:45:15 -07:00
firelight
7efc636413
Merge pull request #2167 from Luna712/player-source-portrait
Add portrait mode layouts for dialogs in player
2025-11-10 12:49:35 +00:00
firelight
f321e98be5
Merge pull request #2177 from Luna712/cleanup
Some cleanup to redundant calls
2025-11-10 12:41:50 +00:00
firelight
cefe43dbc2
Merge pull request #2178 from Luna712/m.ok.ru
Add ok.ru extractor version for m.ok.ru
2025-11-10 12:17:47 +00:00
Luna712
354bf947b7
Fix 2025-11-09 16:04:08 -07:00
Luna712
0775b5e3de
Add ok.ru extractor version for m.ok.ru 2025-11-09 16:02:40 -07:00
recloudstream[bot]
5401867a46 chore(locales): fix locale issues 2025-11-09 21:26:01 +00:00
firelight
0acbeecdbc
Merge pull request #2003 from recloudstream/weblate
Translations update from Hosted Weblate
2025-11-09 21:25:48 +00:00
Luna712
6c8f2a0c5c
Upgrade dokka to 2.1.0 (#2007) 2025-11-09 21:24:27 +00:00
Phisher98
a46fe80ba2
Adding HubCloud and PixelDrain Improvement (#2161) 2025-11-09 21:20:36 +00:00
Luna712
05752bf5ee
Some cleanup to redundant calls
These are all build warnings in Kotlin 2.3, and some are errors in 2.4.
2025-11-09 14:14:56 -07:00
Hosted Weblate
30ce24720a
Merge remote-tracking branch 'origin/master' 2025-11-09 15:33:04 +01:00
Luna712
ef114f9271
Use hidden password in add account input (#2168)
Passwords probably shouldn't be visible by default.
2025-11-09 15:32:59 +01:00
Hosted Weblate
a833cda47c
Merge remote-tracking branch 'origin/master' 2025-11-09 15:17:33 +01:00
Luna712
b7e59a01d3
Fix a few warnings (#2165) 2025-11-09 15:17:27 +01:00
Hosted Weblate
46198951bc
Translated using Weblate (German)
Currently translated at 99.8% (827 of 828 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'

Translated using Weblate (Turkish)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Hebrew)

Currently translated at 79.1% (655 of 828 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Assamese)

Currently translated at 91.9% (761 of 828 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 97.5% (808 of 828 strings)

Translated using Weblate (Burmese)

Currently translated at 76.0% (630 of 828 strings)

Translated using Weblate (Galician)

Currently translated at 49.5% (410 of 828 strings)

Translated using Weblate (Korean)

Currently translated at 89.1% (738 of 828 strings)

Translated using Weblate (Arabic (Najdi))

Currently translated at 54.8% (454 of 828 strings)

Translated using Weblate (Malay)

Currently translated at 73.3% (607 of 828 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 90.4% (749 of 828 strings)

Translated using Weblate (Slovak)

Currently translated at 66.5% (551 of 828 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.3% (814 of 828 strings)

Translated using Weblate (Somali)

Currently translated at 68.4% (567 of 828 strings)

Translated using Weblate (Hungarian)

Currently translated at 79.8% (661 of 828 strings)

Translated using Weblate (Urdu)

Currently translated at 87.1% (722 of 828 strings)

Translated using Weblate (Tamil)

Currently translated at 96.2% (797 of 828 strings)

Translated using Weblate (Hebrew)

Currently translated at 76.0% (630 of 828 strings)

Translated using Weblate (Bengali)

Currently translated at 56.8% (471 of 828 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 91.4% (757 of 828 strings)

Translated using Weblate (Swedish)

Currently translated at 93.3% (773 of 828 strings)

Translated using Weblate (Romanian)

Currently translated at 88.4% (732 of 828 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 73.0% (605 of 828 strings)

Translated using Weblate (Croatian)

Currently translated at 98.3% (814 of 828 strings)

Translated using Weblate (Greek)

Currently translated at 95.0% (787 of 828 strings)

Translated using Weblate (Bulgarian)

Currently translated at 96.4% (799 of 828 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Dutch)

Currently translated at 85.0% (704 of 828 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 98.9% (819 of 828 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (827 of 828 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (828 of 828 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Macedonian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Italian)

Currently translated at 98.9% (819 of 828 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Hindi)

Currently translated at 54.4% (451 of 828 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (828 of 828 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (828 of 828 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (818 of 818 strings)

Translated using Weblate (Catalan)

Currently translated at 55.0% (450 of 818 strings)

Translated using Weblate (Catalan)

Currently translated at 53.1% (435 of 818 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Catalan)

Currently translated at 51.3% (420 of 818 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (814 of 818 strings)

Added translation using Weblate (Catalan)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (German)

Currently translated at 99.8% (817 of 818 strings)

Translated using Weblate (French)

Currently translated at 99.8% (817 of 818 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (818 of 818 strings)

Translated using Weblate (Latvian)

Currently translated at 76.4% (625 of 818 strings)

Co-authored-by: Ahmed Abd Elfattah <escuro.anjo@gmail.com>
Co-authored-by: Alejandro Calduch <alexddf@gmail.com>
Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Co-authored-by: Alexander Kryllov <sashakryllov58@gmail.com>
Co-authored-by: Alexandru <negrualexandru52@gmail.com>
Co-authored-by: Ana Coelho <coelhotraduz@gmail.com>
Co-authored-by: Andreas <andreas.blindheim.koppen@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Feike Donia <feikedonia@proton.me>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Itsmechinmoy <167056923+itsmechinmoy@users.noreply.github.com>
Co-authored-by: Kaan <kaandogan2820@gmail.com>
Co-authored-by: Lacey Anaya <yecakeh263@anawalls.com>
Co-authored-by: LagradOst <46196380+Blatzar@users.noreply.github.com>
Co-authored-by: LiJu09 <lisojuraj@gmail.com>
Co-authored-by: Man <thebroker2308@gmail.com>
Co-authored-by: Marco Moreno <hibarioath@proton.me>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mikenu Takeru <mikenu-jp@users.noreply.hosted.weblate.org>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Muhammad Fahad Khan <itxmfahadkhan@gmail.com>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: PiterDev <piterzdev@gmail.com>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Potorochin Max <ornaras.us@gmail.com>
Co-authored-by: Radoslav Vasilev Vasilev <fifata@gmail.com>
Co-authored-by: Riko Miko <rihardslaskovs@gmail.com>
Co-authored-by: Roi Gabay <roigby@gmail.com>
Co-authored-by: Sam Cooper <samcooper838@gmail.com>
Co-authored-by: Samuel Gadiel <samuelgadiel@gmail.com>
Co-authored-by: Shafici Isxariifshe <mega12xhaphiee@gmail.com>
Co-authored-by: The Initiator <eithansten@gmail.com>
Co-authored-by: Turkish Language Team 🇹🇷 <turkishmark@yandex.com>
Co-authored-by: Willy Anjaya <black.neotrouz@gmail.com>
Co-authored-by: ene-sword-group <enguerrand.neyroud@sword-group.com>
Co-authored-by: htet <htetoh2006@outlook.com>
Co-authored-by: jhihyu lin <thomas.jy.lin@gmail.com>
Co-authored-by: jinu147 <nesqea20@gmail.com>
Co-authored-by: rattenpriester <rattenpriester6840@gmail.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: sundarlal sharma <sundelalsharma11@gmail.com>
Co-authored-by: tabtomi8 <tabtomi88@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: தமிழ்நேரம் <tamilneram247@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ars/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ca/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/gl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/lv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/my/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/qt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/so/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ca/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/mk/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2025-11-09 13:51:21 +00:00
Luna712
52100dc5de
Move strictly to version catalog (#2134)
* Move strictly to version catalog

* Update libs.versions.toml
2025-11-08 22:41:08 +00:00
Luna712
3fda1415ae
Bump navigation to 2.9.6 (#2149) 2025-11-08 22:35:21 +00:00
Luna712
8f41722604
Minor improvement to encoding format text 2025-11-08 13:11:11 -07:00
Luna712
b11b324850
Minor padding fixes 2025-11-08 11:52:28 -07:00
Luna712
5049e56f5a
Minor fixes 2025-11-08 11:33:30 -07:00
Luna712
ddbb56a8de
Remove minor outdated APIs (#2159)
* Deprecated for awhile
* Extremely essy to fix (IDE can do it automatically)
* Not even really implemented or functional methods
* Extensions that do still use them are likely broken in other ways at this point due to being mostly unmaintained in that case
2025-11-08 16:15:50 +01:00
Luna712
76ae7cefb3
Use DeprecationLevel.ERROR for old AuthAPI (#2158)
It has been deprecated for 3 months now.
2025-11-08 16:13:14 +01:00
Luna712
3ccb252c09
Fix unchecked cast (#2160) 2025-11-08 16:12:00 +01:00
Luna712
b50ce3977a
Remove APPLICATION_RAWCC from HlsPlaylistParser (#2163)
Per it's own deprecation warning. It isn't supported anyway.
2025-11-08 16:09:07 +01:00
Luna712
829f8edf0b
Replace deprecated url in YoutubeExtractor (#2164) 2025-11-08 16:08:12 +01:00
Luna712
6bb144d10c
Replace deprecated Thread.id in ExceptionHandler (#2166) 2025-11-08 16:05:30 +01:00
Luna712
4425888097
More response player quality profile items
More response for portrait mode as well, so they don't grow massive. Uses the same size as account select linear profile sizes.
2025-11-07 18:18:20 -07:00
Luna712
70dd9e2387
Add portrait layout for source priorities 2025-11-07 16:46:53 -07:00
Luna712
8633682261
A bit better version 2025-11-07 15:48:13 -07:00
Luna712
2afbce665b
Add for subtitle offset dialog also 2025-11-07 14:19:05 -07:00
Luna712
4d40ecc75c
Add portrait mode layout for souece and subtitles select dialog
This doesn't yet fix it for subtitle sync dialog or improvements to quality dialog. For now ir just fixes the source and subtitles select dialog, which is probably the most important. If this method is accepted, I will fix the orhers in similar ways.

Putting a new layout in `layout-port` will make it auto use it instead of the default layout when in portrait mode, and switch back to landscape layout if it's rotated back to landscape mode.

Fixes #2145
Refs #830
2025-11-07 13:59:29 -07:00
Luna712
20fa05b4eb
Require fixLayout() to be overridden (#2155) 2025-11-06 19:04:08 +01:00
Luna712
011a6e0d50
Rename fixPadding to fixLayout and expand usage (#2127) 2025-11-04 21:48:24 +01:00
Luna712
b699e2f3bc
Fix updating UI when changing some settings (#2139) 2025-11-04 21:41:43 +01:00
Luna712
559edb44f2
Fix setting summary (#2141) 2025-11-04 21:35:28 +01:00
Luna712
2307f15bff
Fix library orientation bug (#2140)
Fixes #2129
2025-11-04 14:48:26 +01:00
Luna712
acbedd1c51
Update gradlew (#2115) 2025-10-31 22:00:14 +01:00
Luna712
aec346920e
Fix entering PiP mode even when we shouldn't (#2118) 2025-10-31 03:37:03 +01:00
firelight
38b9734fdb
Fix: Limit video title length to 2 lines, Closes #2105 2025-10-30 21:40:49 +01:00
Luna712
ede1fad282
Make fixSystemBarsPadding not accept a null View (#2114) 2025-10-30 21:25:41 +01:00
firelight
ec06da62ce
Fix: Restore backup now also invalidates library cache, Closes #2112 2025-10-30 20:56:51 +01:00
Luna712
17761e8adf
Fix system bars padding on orientation change in ResultFragmentPhone (#2113) 2025-10-30 20:49:53 +01:00
Yashas
224438cd92
fix: remove listeners before releasing exoplayer (#2104) 2025-10-30 20:44:34 +01:00
Luna712
ae042708e4
Remove handleConfigurationChanged from BaseFragmentHelper (#2111) 2025-10-30 20:20:40 +01:00
Luna712
604880158e
Migrate ResultFragmentTv to use BaseFragment (#2108) 2025-10-30 19:32:03 +01:00
Luna712
3081728afd
Upgrade Gradle wrapper from 9.1.0 to 9.2.0 and use URL validation (#2096) 2025-10-30 17:29:00 +01:00
Luna712
a565f50c46
Add explicit dependency on activity (#2098)
It's implicit right now, but making it explicit makes dependency management easier, especially now that new versions (starting with 1.12.0) require minSdk 23.
2025-10-30 17:10:48 +01:00
Luna712
9600781c4f
Migrate subtitles fragments to use BaseFragment (#2101) 2025-10-30 17:07:29 +01:00
firelight
dbeb815c06
Docs: Additional fixPadding documentation 2025-10-30 17:02:59 +01:00
Luna712
accf840730
Migrate WebviewFragment to use BaseFragment (#2103) 2025-10-30 17:01:33 +01:00
Luna712
3f2d81632f
Migrate EasterEggMonkeFragment to use BaseFragment (#2102) 2025-10-30 17:00:22 +01:00
Luna712
9afaf37091
Migrate settings fragments to use BaseFragment (#2100) 2025-10-30 16:55:44 +01:00
Luna712
cd25048212
Add BaseFragment compat for other Fragment subclasses (#2097) 2025-10-30 00:47:43 +01:00
Luna712
b385c72127
Migrate setup fragments to use BaseFragment (#2095) 2025-10-30 00:35:29 +01:00
firelight
69e8d90ad2
Fix: Correct boundscheck for next episode, Closes #2084 and Closes #2090 2025-10-30 00:26:22 +01:00
Luna712
532c375aad
Migrate SearchFragment to BaseFragment (#2087) 2025-10-29 13:08:13 +01:00
Luna712
a37a56bbfd
Migrate downloads fragments to use BaseFragment (#2088) 2025-10-29 13:05:03 +01:00
Luna712
1ffac969ff
Minor update to english translation for a string (#2092)
Not only phones but other devices too. We should clarify that a bit and use devices here instead of phones.
2025-10-29 12:49:24 +01:00
Luna712
f64be19d15
Add new BaseFragment (#2080) 2025-10-28 22:53:05 +01:00
firelight
993dcd92f0
Fix: Some white theme issues with dialog text and quality profiles 2025-10-28 22:42:34 +01:00
firelight
bcd9d65962
Chore: Refactor AccountAdapter and SelectAdaptor to use BaseAdapter 2025-10-28 18:39:42 +01:00
firelight
2d177cb330
Chore: Refactor ImageAdapter, AccountAdapter, TestResultAdapter and LogcatAdapter to use BaseAdapter 2025-10-28 18:15:34 +01:00
firelight
297e6de658
Fix: Temporarily removed voting api due to bad gateway 2025-10-28 17:42:59 +01:00
firelight
fe68f72e9d
Fix: loadImage fix for resources with attr, Closes #2082 2025-10-28 17:34:18 +01:00
firelight
bc07b86445
Feat: Added CronetDataSource for better cache and networking (#2079) 2025-10-28 17:24:43 +01:00
firelight
a1a5c36db4
Fix: Recycle cache theme invalidation, Closes #2082 2025-10-27 19:02:43 +01:00
firelight
6c30af3790
Fix: Unified resume logic to allow resume from external apps, Closes #2070 2025-10-26 16:37:09 +01:00
firelight
df091502fe
Fix: Homepage switching provider pop-in, might also fix #2072 2025-10-26 14:03:56 +01:00
firelight
9295795ab2
Fix: Upcoming episodes without hourglass icon if we have an faulty poster, also clear image 2025-10-26 02:17:27 +01:00
firelight
856245a23b
Chore: Refactor EpisodeAdapter to use BaseAdapter, and expanded BaseAdapter to allow custom content 2025-10-26 02:50:45 +02:00
firelight
73fec27d00
Chore: Refactor SearchHistoryAdaptor to use BaseAdapter 2025-10-26 02:06:29 +02:00
firelight
95db2d728e
Chore: Refactor PluginAdapter to use BaseAdapter, and fixed ghosting issue 2025-10-26 01:53:57 +02:00
firelight
ce5816ae77
Fix: Random crash on long running sessions, Closes #2071 2025-10-26 00:30:06 +02:00
firelight
68b215cada
Chore: Refactor RepoAdapter to use BaseAdapter 2025-10-26 00:13:12 +02:00
firelight
eb3519731a
Chore: Refactor ProfilesAdapter to use BaseAdapter 2025-10-25 22:36:43 +02:00
firelight
d77fcde347
Chore: Refactor PriorityAdapter to use BaseAdapter 2025-10-25 22:24:39 +02:00
firelight
38e52e24db
Fix: Watch status on unwatched episodes, whoops 2025-10-25 22:00:47 +02:00
firelight
ee3b07916e
Chore: Refactor SubtitleOffsetItemAdapter to use BaseAdapter 2025-10-25 21:55:21 +02:00
firelight
9e72ab2394
Chore: Refactor PageAdapter to use BaseAdapter 2025-10-25 21:41:31 +02:00
firelight
e61715cd99
Chore: Minor refactor to displayPos 2025-10-25 21:26:14 +02:00
rockhero1234
99eaa369ea
feat:check icon on watched episode and auto mark watched (#2023)
* check icon on watched episode

* auto mark episode as watched

* set check above 95 and removed auto watched
2025-10-25 21:24:37 +02:00
rockhero1234
b56e858d48
fix (#2022) 2025-10-25 21:19:07 +02:00
firelight
8dcb38a3bb
Chore: Refactor SearchAdapter to use BaseAdapter, and made NoStateAdapter easier to use 2025-10-25 21:11:19 +02:00
firelight
66b8fe963d
Chore: Refactor BaseAdapter for better view recycling, migrate Actors to BaseAdapter 2025-10-25 20:03:48 +02:00
Luna712
526372a577
Bump version to 4.6.1 (#2068)
Since we've had a major change with edge to edge, bumping the version could help know if it's related if an issue is created.
2025-10-25 19:22:54 +02:00
Luna712
5e3c9ee726
Bump tmdb lib (#2054) 2025-10-25 18:55:51 +02:00
Luna712
b0c78f7820
Unify clear search icons logic (#2062)
Reduces code duplication, and also adds support for them to show on repo and extensions search.
2025-10-25 18:51:10 +02:00
Luna712
eb93b8803d
Add full support for edge-to-edge (#2002) 2025-10-25 18:38:57 +02:00
firelight
29491ae634
Fix: Improved shared pool recycling for actors, quicksearch and recommendations 2025-10-25 15:26:41 +02:00
firelight
071b50e4ae
Fix: Improved shared pool recycling for episodes, search and homepage 2025-10-25 15:09:22 +02:00
Luna712
a100c94d40
Remove backwards compatible constructors that have been deprecated for over a year (#2044) 2025-10-25 14:37:07 +02:00
Luna712
dccfae8506
Bump KGP to 2.2.21 (#2057) 2025-10-25 14:36:18 +02:00
Luna712
2cc86bcb6a
Bump material (#2055) 2025-10-25 14:30:00 +02:00
Luna712
bda260525b
[skip ci] Bump upload-artifact action (#2058) 2025-10-25 12:15:55 +02:00
firelight
168c2e8432
Fix: Skip loading visibility in white theme, Closes #2049 2025-10-24 13:55:34 +02:00
firelight
eaa26b20e3
Fix: Inconsistent icon color for certain icons in settings on light themes, Closes #2047 2025-10-23 21:41:54 +02:00
firelight
ffd3c47dd9
Fix: Contrast issues on AccountSelectActivity on light themes, Closes #2048 2025-10-23 21:30:02 +02:00
firelight
a03079e48c
Fix: Contrast issues on light themes, Closes #2049 2025-10-23 21:06:42 +02:00
firelight
b3054e9bd1
Fix: Subtitle preview with elevation, Closes #2053 2025-10-23 20:16:20 +02:00
Luna712
5898eb2593
Enable deprecations in SubtitleHelper (#2036) 2025-10-23 19:03:44 +02:00
Luna712
4270f159b8
Make old rating system use DeprecationLevel.ERROR (#2041)
It's been deprecated for about 3 months now. This also deprecated `fromOld` and `toOld` since in theory those should never have been used in extensions and also `toRatingInt()` since again, in theory, wouldn't be used once converting to use the new score API.
2025-10-20 21:18:31 +02:00
firelight
5b1400e212
Fix: Subtitles not showing on cancel press, Closes #2042 2025-10-20 20:58:36 +02:00
firelight
28f044ad33
Fix: OOM on removeKeys, Closes #2035 2025-10-19 19:53:10 +02:00
firelight
fe0475f441
Fix(UI): Varius highlight/color changes for better accessibility, Closes #2024 2025-10-18 20:34:05 +02:00
Luna712
50e746c51d
Suppress some internal deprecations (#2019) 2025-10-18 20:10:56 +02:00
Luna712
34833d23d0
Remove usage of deprecated unsafeCheckOpNoThrow (#2018) 2025-10-18 20:10:11 +02:00
Luna712
98e6572bab
Replace deprecated StaticLayout constructor (#2020) 2025-10-18 20:08:31 +02:00
Luna712
7dd650b236
Update some depreciations (#2028) 2025-10-18 20:06:21 +02:00
Luna712
6dc0d6ed73
Replace use of deprecated Util.SDK_INT (#2030) 2025-10-18 20:04:07 +02:00
firelight
1f8cccdbd0
Fix: Next player episode and episode list progress issue, Closes #2025 2025-10-18 18:56:04 +02:00
Luna712
5bcf457814
Enable auto rotate preference by default (#2014)
* Enable auto rotate preference by default

It makes sense to adjust the screen orientation based on the orientation of the video by default.

* Update callsite also
2025-10-18 00:13:37 +02:00
Luna712
760dea2b4f
Provider tests: handle if implementing paginated search (#2016) 2025-10-18 00:12:34 +02:00
Luna712
2e255e6586
Use DeprecationLevel.ERROR for some methods (#2017) 2025-10-18 00:11:38 +02:00
firelight
d84a241026
Feat: Added subtitle alignment setting, and added 400dp elevation closes #2009 2025-10-16 16:34:27 +02:00
Luna712
535cb86934
Make constructor for UpdatedMatroskaExtractor private (#1999)
`'internal' declaration exposes 'public/*package*/' type 'EbmlReader'. This will become an error in language version 2.4.`
2025-10-16 15:54:37 +02:00
rockhero1234
505f869dbf
feat:resume btn with circular progress (#1998) 2025-10-16 15:52:19 +02:00
rockhero1234
e113cee434
fix(tv):not show rotate btn (#2010)
* fix(tv):not show rotate btn

* also emulator
2025-10-16 15:36:22 +02:00
Luna712
2bfee2e4a3
Fix library crash (#2006) 2025-10-16 01:18:15 +02:00
Osten
ac810db3d9
Bump 4.6.0 2025-10-15 19:07:47 +02:00
firelight
876ce09b38
Fix: Show the correct season names in episode list in the player 2025-10-15 17:56:52 +02:00
firelight
2687613e4f
Fix: Hide episode rating if rating < 0.1 to avoid "Rating: 0.0" 2025-10-15 14:03:53 +02:00
firelight
fbc8e6214f
Fix: Reset the episodeProgress view visibility to avoid recycling errors, and readded the "Upcoming Icon" 2025-10-15 13:58:19 +02:00
firelight
566fd741cf
Fix(TV): Minor focus issue on homepage actionbar, solved by keeping the same views instead of replacing them. 2025-10-15 02:11:48 +02:00
Luna712
f2056e7dd1
Bump newpipeextractor to v0.24.8 (#2001) 2025-10-13 18:24:06 +02:00
Luna712
66093253ef
Revert "No gradle cache on prerelease for now (#1986)" (#1996)
This reverts commit 7d4864baa6.
2025-10-12 02:33:55 +02:00
firelight
01792fe704
Increase gradle build memory 2025-10-11 23:54:58 +00:00
Luna712
8483104e67
Fix gradle build (#1997) 2025-10-11 23:53:58 +00:00
firelight
e1b18d9b14
Feat(TV): Added transparent outline to nav_footer_profile_card 2025-10-12 00:59:31 +02:00
firelight
543c0dddf8
Fix(Phone): Season selector with "no season" showing as "Season 0" 2025-10-12 00:38:06 +02:00
Luna712
ac67234076
Bump acra (#1994) 2025-10-12 00:16:44 +02:00
Luna712
a7eacb4a82
Bump qrcode lib (#1990) 2025-10-12 00:09:32 +02:00
Luna712
339e48e66b
Bump navigation libs (#1991) 2025-10-12 00:05:10 +02:00
Luna712
897866bfc5
Bump lifecycle libs (#1992) 2025-10-12 00:02:20 +02:00
Luna712
168e42f1b0
Bump androidx.core:core-ktx (#1993) 2025-10-11 23:59:08 +02:00
Luna712
511c18cd96
Bump coil to 3.3.0 (#1995) 2025-10-11 23:53:58 +02:00
Luna712
ae90ce6be5
Upgrade KGP to 2.2.20 (#1989) 2025-10-11 23:47:57 +02:00
Luna712
521a87857a
Bump gradle and AGP (#1987) 2025-10-11 23:40:27 +02:00
firelight
26504710e4
Fix(TV UI): Correct padding on horizontal views 2025-10-11 22:10:47 +02:00
Luna712
1c3b4363c5
Fix showing episode data in TMDB metaprovider (#1985) 2025-10-11 21:13:15 +02:00
firelight
01decca3f4
UI(TV): New navrail colors 2025-10-11 21:12:13 +02:00
Luna712
7d4864baa6
No gradle cache on prerelease for now (#1986) 2025-10-11 19:46:30 +02:00
firelight
85498f319c
Fix(TV): Minor alpha issue 2025-10-11 17:04:06 +02:00
Luna712
95a42adae2
Bump json lib (#1981) 2025-10-11 16:20:33 +02:00
Luna712
794710a50d
Add score to TMDB metaprovider (#1982) 2025-10-11 14:45:19 +02:00
firelight
dd1f59a7a8
Fix: Minor formatting bug in setPlayerDimen 2025-10-11 14:38:07 +02:00
Luna712
ebb7104747
Bump work-runtime libs (#1980) 2025-10-11 14:32:00 +02:00
Luna712
4d0d2467e0
Bump kotlinxCoroutinesCore (#1977) 2025-10-11 14:29:30 +02:00
valhn
a03f89f9a0
bump nextlib to match media3 version (#1984) 2025-10-11 14:25:23 +02:00
Luna712
2d9abe151b
Cache dependencies in builds (#1983)
* Cache dependencies in builds

When testing this withArtifact Build it improved build speed from ~6 minutes to <2 minutes

* Some newlines
2025-10-11 14:16:51 +02:00
Luna712
e5aa0a3c39
Require optin for prerelease (#1978) 2025-10-10 23:29:17 +00:00
Luna712
ab504fbfa6
Bump junit (#1976) 2025-10-10 21:36:51 +00:00
recloudstream[bot]
986e2a907e chore(locales): fix locale issues 2025-10-10 21:35:53 +00:00
firelight
92eaf3e873
Merge pull request #1928 from recloudstream/weblate
Translations update from Hosted Weblate
2025-10-10 23:35:41 +02:00
Hosted Weblate
cb62fc189c
Merge remote-tracking branch 'origin/master' 2025-10-10 23:35:20 +02:00
Luna712
0dbdebad4b
Bump espresso (#1979) 2025-10-10 21:35:16 +00:00
Hosted Weblate
40d0af8b49
Merge remote-tracking branch 'origin/master' 2025-10-10 21:18:33 +02:00
Luna712
5cd6d93d60
Bump tvprovider lib (#1975) 2025-10-10 21:18:28 +02:00
Hosted Weblate
fa540265f8
Merge remote-tracking branch 'origin/master' 2025-10-10 19:02:44 +00:00
firelight
ede93a3c6b
Fix: White theme UnderlayProgress fix 2025-10-10 21:02:40 +02:00
Hosted Weblate
5c3b37336f
Merge remote-tracking branch 'origin/master' 2025-10-10 18:50:19 +00:00
Luna712
17e56b21bf
Use the newSubtitleFile method (#1964)
* Use the newSubtitleFile method

* Make constructor internal

* Use private again
2025-10-10 20:50:12 +02:00
Hosted Weblate
803cc4b067
Merge remote-tracking branch 'origin/master' 2025-10-10 20:48:18 +02:00
Luna712
00aefad777
Bump compilesdk to 36 (#1974) 2025-10-10 20:48:13 +02:00
Hosted Weblate
fd86a29958
Merge remote-tracking branch 'origin/master' 2025-10-10 18:36:16 +00:00
Luna712
53c2a03c52
Add pagination support to TMDB metaprovider (#1960) 2025-10-10 20:36:11 +02:00
Hosted Weblate
c42cc79f22
Merge remote-tracking branch 'origin/master' 2025-10-10 18:25:59 +00:00
Luna712
f1e4b337c9
Bump tmdb lib (#1973) 2025-10-10 20:25:55 +02:00
Hosted Weblate
1487d1b718
Merge remote-tracking branch 'origin/master' 2025-10-10 20:20:12 +02:00
Luna712
883cf64c6d
Deprecate public constructor for ExtractorLink (#1962) 2025-10-10 20:20:05 +02:00
firelight
e8b8cba8f8
Refactor: Added appliedTheme variable instead of using getDefaultSharedPreferences 2025-10-10 20:14:47 +02:00
doGior
ad51c57b3d
Minor UI changes in dark and amoled themes (#1958)
* Fixed large episode padding when theme is amoled

* Changed card background in dark theme

* Removed padding
2025-10-10 20:09:17 +02:00
Hosted Weblate
76abb125ef
Merge remote-tracking branch 'origin/master' 2025-10-10 20:05:14 +02:00
Luna712
7bfb8fe8c5
Use ERROR deprecation level for SearchResponseList (#1961) 2025-10-10 20:05:09 +02:00
Hosted Weblate
bd1172dfa3
Merge remote-tracking branch 'origin/master' 2025-10-10 17:58:09 +00:00
Luna712
63fb0a4f8b
Bump actions (#1972) 2025-10-10 19:58:05 +02:00
Hosted Weblate
bf79ac1baf
Merge remote-tracking branch 'origin/master' 2025-10-10 17:39:40 +00:00
firelight
0b73841e09
Fix: AlertDialogCustom inheritance fix 2025-10-10 19:39:35 +02:00
Hosted Weblate
da572fdfbf
Merge remote-tracking branch 'origin/master' 2025-10-10 17:38:49 +00:00
Red-To-Tel
053692b631
Update InAppUpdater.kt to fix white text on white background issue (#1967) 2025-10-10 19:38:45 +02:00
Hosted Weblate
141a1a2adb
Translated using Weblate (Turkish)
Currently translated at 100.0% (818 of 818 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (814 of 818 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (818 of 818 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (818 of 818 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (818 of 818 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (818 of 818 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Malay)

Currently translated at 74.5% (608 of 816 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (816 of 816 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (807 of 816 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Turkish)

Currently translated at 100.0% (816 of 816 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (816 of 816 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 99.1% (809 of 816 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.7% (806 of 816 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'

Translated using Weblate (Spanish)

Currently translated at 99.5% (812 of 816 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (816 of 816 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (816 of 816 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (816 of 816 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (816 of 816 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (815 of 815 strings)

Co-authored-by: Adrián Gelmotto Ruiz <adriangelmotto@gmail.com>
Co-authored-by: Dört Koldan Taciz <yapilirnasil327@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Lenny Tran <lennytran49@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mohammad Abdallah <mhmd.abdallah.l@hotmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: diogob003 <diogob003@outlook.com>
Co-authored-by: kerklangsi <arifilah.01@gmail.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ms/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2025-10-10 17:28:06 +00:00
KingLucius
0281eb4185
PercentageCropImageView KDoc and XML usage (#1970) 2025-10-10 19:27:53 +02:00
firelight
3a56d6d36c
Feat(UI): New speed dialog, Closes #1889 2025-10-08 01:11:15 +02:00
firelight
b63990da38
Fix: Hero banner shown in None. Closes #1957 2025-10-07 23:54:33 +02:00
firelight
1f71acabcb
Fix: Single mirror syncdata 2025-10-07 23:44:26 +02:00
Sovan Sinha
bb77d93b5d
Enhancement : hide controls during volume/brightness gestures and restore on pause (#1954)
* player: hide controls during volume/brightness gestures and restore on gesture end if paused

* player: reshow controls only if visible pre-gesture; reveal immediately on gesture completion when paused
2025-10-07 18:23:30 +02:00
firelight
6d93560252
Fix: Deduplicated subtitles 2025-10-07 18:12:46 +02:00
GCDarcy
6611c8a607
Added the option in settings for people to display the series/movie title instead of the name of the current source (partial support for languages) (#1956) 2025-10-07 17:49:19 +02:00
Osten
716b809ea2
Emergency patch for TV performance 2025-10-06 21:40:00 +02:00
firelight
2ca106ecb1
Fix: Recycle setMaxViewPoolSize on HomeChildItemAdapter for larger devices 2025-10-05 22:50:04 +02:00
firelight
dcdf15f484
Fix: Optimized homepage by properly recycling all nested recycleviews 2025-10-05 17:18:30 +02:00
firelight
ca737b1194
Fix(TV): Poster size for remove item 2025-10-05 02:20:45 +02:00
firelight
233fe87cdc
Fix(TV): Focus issue with account 2025-10-05 01:05:55 +02:00
firelight
8250a65aca
Fix: HTML encoded description, and clickable homepage hero 2025-10-05 00:26:27 +02:00
KingLucius
0d83418150
feat(TV UI): Crop the Main banner from the Top (#1952) 2025-10-05 00:04:31 +02:00
firelight
60fab25032
Fix: Minor margin issue (Phone) + Minor 1px rendering issue (TV) 2025-10-04 02:17:19 +02:00
firelight
a70fb87594
Feat: Infinite scrolling on quicksearch and trakt (#1949) 2025-10-04 02:02:42 +02:00
firelight
6632f764b0
Feat: New TV UI for Homepage (#1942) 2025-10-03 22:22:39 +02:00
Phisher98
ccf3b05088
Dailymotion Fix (#1947)
* Dailymotion Improvement

* Dailymotion Improvement Minor Fix
2025-10-03 17:35:44 +02:00
firelight
26570d688f
Chore: Bump versionName 2025-10-02 19:54:54 +02:00
firelight
6d4c530e16
Chore: Bump media3 to 1.8.0 2025-10-02 19:52:08 +02:00
rockhero1234
373d746a3f
feat(library):scroll top on sort (#1938) 2025-10-02 19:34:48 +02:00
firelight
eb016eb39e
Chore: Cleanup unused xml files 2025-10-02 19:16:13 +02:00
firelight
c783e82c80
Fix: Minor typo + testing fix 2025-10-02 19:06:24 +02:00
rockhero1234
7a909ec7c4
ui:circular progress bars (#1941) 2025-10-02 19:04:46 +02:00
rockhero1234
90144a6b01
ui(fix):torrent ui in player (#1940) 2025-09-29 19:28:34 +02:00
firelight
e761ba437f
Fix: Minor bug with subtitle suffix on cached subtitles 2025-09-29 01:13:13 +02:00
Diogo
30cf12d6cc
Use IETF BCP 47 language tag as much as possible (#1815)
* Use IETF BCP 47 as much as possible since not all languages have ISO 639-1 tag
* Fix online subtitles for langs with 2+ letters
* Show flag emoji while filtering online subtitles language
* Show flag on player subtitles download / auto select languages
* Use language code instead of name to filter auto select / download languages
2025-09-29 01:07:20 +02:00
firelight
1f385116dd
Fix: More focus shenanigans 2025-09-28 22:06:38 +02:00
firelight
7948577df7
Fix: Minor focus issue and code formatting 2025-09-28 21:41:40 +02:00
rockhero1234
27acc4b4e1
feat(player):episodes overlay (#1933) 2025-09-28 21:39:39 +02:00
firelight
b5fb1da210
Feat(Trakt): Added score to homepage and search 2025-09-28 21:17:50 +02:00
KingLucius
97ff20ea5e
feat(Trakt): Update Trakt meta provider with last APIs changes (#1936) 2025-09-28 21:17:00 +02:00
Aryan Singh
a4d1d98f7f
Add Android TV channel preview support (#1929) 2025-09-28 14:01:30 +00:00
firelight
91fbec3d08
Refactor: Added VideoGenerator to make IGenerator easier to use correctly. 2025-09-26 21:49:05 +02:00
firelight
9d944f0c44
Fix: Fixed play mirror watch history by assigning it the correct id, Closes #1931 2025-09-26 20:39:53 +02:00
Phisher98
ec6a71521f
GDMirrorBot Improvement and Fixes (#1930)
* GDMirrorbot Improvement
2025-09-26 13:16:25 +02:00
LeopoldoDelgadillo
7f769bd1d3
"Play mirror" function on play context window. (#1926) 2025-09-23 23:41:16 +02:00
rockhero1234
182b38a7a4
feat:hold profile for account edit dialog (#1927)
* feat:hold profile for account edit dialog

* fixed
2025-09-23 23:36:48 +02:00
Osten
7c0dc6c084 Updated style for "SearchBox", aka dub/sub/rating. 2025-09-21 23:50:36 +02:00
firelight
15c320ba4a
Fix: Made score toString removeTrailingZeros act properly on scores with higher precision 2025-09-20 22:13:24 +02:00
firelight
0b11c6475b
Fix: Capitalized AppLanguages 2025-09-20 21:36:06 +02:00
recloudstream[bot]
62fa9e12df chore(locales): fix locale issues 2025-09-20 19:26:29 +00:00
Diogo
69b1709c09
Fix: locale workflow (#1914)
* Rename from values-<language>-r<COUNTRY> to values-b+<language>+<COUNTRY>

* Sort locales by language name instead of iso in locales.py

* Revert to deprecated codes for Indonesian (id → in) and Hebrew (he -> iw)

* Remove "jv" as it's translation file has no strings

* Add name and flag for "ckb" (Sorani / Central Kurdish)
2025-09-20 21:26:13 +02:00
firelight
d8a1aa1f7b
Fix(Player): Fixed #1923 by only allowing volume override when in fullscreen 2025-09-20 14:08:33 +02:00
firelight
53a5f60646
Fix: Removed subSource, and removed "Null subtitles" toast 2025-09-20 14:04:21 +02:00
Diogo
7f010dce44
Addic7ed, Subdl: fix and use IETF language tag (#1895) 2025-09-20 13:56:53 +02:00
Phisher98
9993f79cb6
VidStack Improvement (#1920) 2025-09-20 13:51:03 +02:00
Kraptor123
f5bd9f3121
2 New Themes Dracula Theme And Lavender Dreams (#1908)
* New 2 Themes Dracula And Lavender Dreams

* dracula theme new color changes
2025-09-20 13:41:39 +02:00
Bnyro
5994a0bad1
feat: properly insert copied repo data as new repository (#1917) 2025-09-20 13:35:44 +02:00
Bnyro
237690069d
feat: add streamup extractor api (#1916) 2025-09-20 13:34:27 +02:00
recloudstream[bot]
feeae8907d chore(locales): fix locale issues 2025-09-16 22:33:04 +00:00
firelight
72b849bccb
Merge pull request #1701 from recloudstream/weblate
Translations update from Hosted Weblate
2025-09-17 00:32:45 +02:00
Hosted Weblate
fede044009
Merge remote-tracking branch 'origin/master' 2025-09-15 20:17:10 +02:00
recloudstream[bot]
b270989cf3 chore(locales): fix locale issues 2025-09-15 18:17:06 +00:00
Hosted Weblate
79c2386f39
Merge remote-tracking branch 'origin/master' 2025-09-15 20:16:53 +02:00
Diogo
4d5b74c65b
Settings: translate languages and use IETF BCP 47 tag (#1894) 2025-09-15 20:16:50 +02:00
Hosted Weblate
1df005e705
Merge remote-tracking branch 'origin/master' 2025-09-15 19:52:08 +02:00
rockhero1234
56e5eb2d63
fix:removed volume overlay on trailers (#1904) 2025-09-15 19:52:02 +02:00
Hosted Weblate
7af581ba8f
Translated using Weblate (Filipino)
Currently translated at 21.4% (175 of 815 strings)

Translated using Weblate (Malay)

Currently translated at 72.1% (588 of 815 strings)

Translated using Weblate (Malay)

Currently translated at 72.1% (588 of 815 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Korean)

Currently translated at 90.6% (739 of 815 strings)

Translated using Weblate (French)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Kurdish (Central))

Currently translated at 24.9% (203 of 815 strings)

Translated using Weblate (Arabic)

Currently translated at 99.2% (809 of 815 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Kurdish (Central))

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (815 of 815 strings)

Added translation using Weblate (Kurdish (Central))

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 (Indonesian)

Currently translated at 97.9% (798 of 815 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'

Translated using Weblate (Portuguese)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (809 of 815 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (815 of 815 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (815 of 815 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Polish)

Currently translated at 100.0% (815 of 815 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (German)

Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (814 of 814 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (814 of 814 strings)

Translated using Weblate (French)

Currently translated at 100.0% (814 of 814 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (French)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (French)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (813 of 813 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (813 of 813 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'

Translated using Weblate (Odia)

Currently translated at 34.0% (276 of 811 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (German)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (French)

Currently translated at 99.5% (807 of 811 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (811 of 811 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (806 of 806 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish (Argentina))

Currently translated at 100.0% (4 of 4 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'

Translated using Weblate (French)

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (806 of 806 strings)

Translated using Weblate (German)

Currently translated at 100.0% (805 of 805 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Assamese)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (805 of 805 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (804 of 805 strings)

Translated using Weblate (Romanian)

Currently translated at 91.0% (733 of 805 strings)

Translated using Weblate (Romanian)

Currently translated at 91.0% (733 of 805 strings)

Translated using Weblate (French)

Currently translated at 99.7% (803 of 805 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (805 of 805 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% (805 of 805 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (801 of 805 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (804 of 805 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Hindi)

Currently translated at 55.2% (445 of 805 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (French)

Currently translated at 99.8% (804 of 805 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (803 of 803 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (803 of 803 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Turkish)

Currently translated at 100.0% (800 of 800 strings)

Added translation using Weblate (Javanese)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Indonesian)

Currently translated at 99.6% (797 of 800 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (4 of 4 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (799 of 800 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (800 of 800 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Turkish)

Currently translated at 99.6% (797 of 800 strings)

Translated using Weblate (Turkish)

Currently translated at 99.6% (797 of 800 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (800 of 800 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 99.2% (794 of 800 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (794 of 800 strings)

Translated using Weblate (German)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (800 of 800 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Italian)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (800 of 800 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Tamil)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (796 of 796 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Urdu)

Currently translated at 90.5% (721 of 796 strings)

Translated using Weblate (Urdu)

Currently translated at 90.5% (721 of 796 strings)

Translated using Weblate (Urdu)

Currently translated at 90.5% (721 of 796 strings)

Translated using Weblate (Urdu)

Currently translated at 90.5% (721 of 796 strings)

Translated using Weblate (Urdu)

Currently translated at 90.5% (721 of 796 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Italian)

Currently translated at 99.4% (792 of 796 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Persian)

Currently translated at 58.0% (462 of 796 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 98.7% (786 of 796 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (796 of 796 strings)

Translated using Weblate (Swedish)

Currently translated at 97.2% (771 of 793 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (793 of 793 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Afrikaans)

Currently translated at 29.7% (236 of 793 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 98.9% (785 of 793 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 94.1% (747 of 793 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (793 of 793 strings)

Translated using Weblate (Hungarian)

Currently translated at 83.1% (659 of 793 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (793 of 793 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (793 of 793 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (793 of 793 strings)

Translated using Weblate (Dutch)

Currently translated at 85.4% (678 of 793 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (790 of 790 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (788 of 788 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (782 of 782 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Dutch)

Currently translated at 50.0% (2 of 4 strings)

Translated using Weblate (Dutch)

Currently translated at 86.3% (675 of 782 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Portuguese)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Korean)

Currently translated at 93.3% (730 of 782 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'

Translated using Weblate (Japanese)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Swedish)

Currently translated at 98.0% (767 of 782 strings)

Translated using Weblate (Dutch)

Currently translated at 84.3% (660 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (780 of 782 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Macedonian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (780 of 782 strings)

Translated using Weblate (German)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Macedonian)

Currently translated at 99.8% (781 of 782 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (German)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Hindi)

Currently translated at 54.3% (425 of 782 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (782 of 782 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'

Translated using Weblate (Tamil)

Currently translated at 95.5% (747 of 782 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Arabic)

Currently translated at 99.4% (778 of 782 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (French)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (French)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (782 of 782 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (779 of 780 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (780 of 780 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (775 of 777 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Assamese)

Currently translated at 97.2% (756 of 777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (German)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 96.7% (752 of 777 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.4% (773 of 777 strings)

Translated using Weblate (Swedish)

Currently translated at 97.4% (757 of 777 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Macedonian)

Currently translated at 99.0% (770 of 777 strings)

Translated using Weblate (Italian)

Currently translated at 99.6% (774 of 777 strings)

Translated using Weblate (Croatian)

Currently translated at 99.3% (772 of 777 strings)

Translated using Weblate (Czech)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Arabic)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (French)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (777 of 777 strings)

Co-authored-by: Adolfo Jayme Barrientos <fitojb@ubuntu.com>
Co-authored-by: Aerin Park <aerin.gy.park@gmail.com>
Co-authored-by: Ahmed Abd El-Fattah <a.aelfattah5@gmail.com>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Angel Ivanov <angel.forcebg@gmail.com>
Co-authored-by: Angga Wangza <wangza00@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Arina <hello.arina.s@gmail.com>
Co-authored-by: Bernardo Gomes <bernardo.gomes@bebitterbebetter.com.br>
Co-authored-by: Boqirz <alveromodar@gmail.com>
Co-authored-by: Chris V <krisvasilev2003@gmail.com>
Co-authored-by: Christopher Allen <nsn90255@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Cris P <cristineipop2015@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Darias <yd5uecg8e@mozmail.com>
Co-authored-by: Deleted User <noreply+124913@weblate.org>
Co-authored-by: Dinh Nguyen <dinhitcom@gmail.com>
Co-authored-by: Do you know my name? <anonymous2676@users.noreply.hosted.weblate.org>
Co-authored-by: Empty Context <weblate.come585@passmail.net>
Co-authored-by: Esspel <eric.soderstrom06@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fjuro <git@alius.cz>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Fritz Blümke <fritzbluemke@gmail.com>
Co-authored-by: Geovani Amaral <geovani.af4@gmail.com>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Harshit Sethi <hstsethi@outlook.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Kostov <jiveq1@gmail.com>
Co-authored-by: Jagattananda Satapathy <buzzcraze.official@gmail.com>
Co-authored-by: Jagrit Rajoriya <jagritrajoriya51@gmail.com>
Co-authored-by: Jimmy Dahlberg <jimmydaswag@hotmail.com>
Co-authored-by: John Kennedy Peña <jkhp.jkpa@gmail.com>
Co-authored-by: Juan Rubin <juancrubin08@gmail.com>
Co-authored-by: Juzé <dedakir923@exoular.com>
Co-authored-by: Kerim Demirkaynak <aschannel111@gmail.com>
Co-authored-by: Kingsis Trader <kingsistrader@gmail.com>
Co-authored-by: Lacey Anaya <yecakeh263@anawalls.com>
Co-authored-by: Lourenço Martins <hlourencoam93@gmail.com>
Co-authored-by: MM <metalmario90@gmail.com>
Co-authored-by: Maksim_220 Кабанов <law820314@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Mickael Marques da mota <mickaelmarquesdamota@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: MrSliv69 <mrsliv6969@gmail.com>
Co-authored-by: Nataniel Dika Kurniawan <hikawaart2@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Night Sky <mdrouhin2+weblate@gmail.com>
Co-authored-by: Niko <nikomoto1@gmail.com>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Plamen Ivanov <paco.iwanow@gmail.com>
Co-authored-by: Py- Droid <droidpy587@gmail.com>
Co-authored-by: Quentin <mrbydix@users.noreply.hosted.weblate.org>
Co-authored-by: Ramazan S <me@ramazansancar.com.tr>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: SBS1313 <simonsaade005@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Serdar Sağlam <teknomobil@msn.com>
Co-authored-by: Sergei Bumazhnov <bumazhnov94@gmail.com>
Co-authored-by: Sergey Ponomarev <stokito@gmail.com>
Co-authored-by: Simeon Tthhuueett <simeontthhuueett@gmail.com>
Co-authored-by: Sofia <sofimanci2112@gmail.com>
Co-authored-by: Somme4096 <somme4096@gmail.com>
Co-authored-by: Splow95 <splownder.mau@gmail.com>
Co-authored-by: Uzair Syed <ghost.lord59531@gmail.com>
Co-authored-by: Vdrsl <Nivin2983@gmail.com>
Co-authored-by: Vitalii Panyk <likenistop4ik@gmail.com>
Co-authored-by: alb bla <explodedhiuehthr@tutamail.com>
Co-authored-by: bbstacks <bbstacks1225@gmail.com>
Co-authored-by: dabao1955 <dabao1955@163.com>
Co-authored-by: dedakir923 <dedakir923@exoular.com>
Co-authored-by: ene-sword-group <enguerrand.neyroud@sword-group.com>
Co-authored-by: hidden <rahandgamer400@gmail.com>
Co-authored-by: icosahedr.online <sapero@icosahedr.online>
Co-authored-by: khalidbelk <khalid.belkassmi-el-hafi@epitech.eu>
Co-authored-by: maxim <maximtested@gmail.com>
Co-authored-by: michte <michteting@proton.me>
Co-authored-by: oguzugur28 <oguzugur2834@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: vanapro1 <law820314@gmail.com>
Co-authored-by: william piti <loolyowo@gmail.com>
Co-authored-by: yigit bilgin <yigitbilginbw@hotmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: λpex_herbivore <weblate@imveganbtw.win>
Co-authored-by: Максим Горпиніч <gorpinicmaksim0@gmail.com>
Co-authored-by: Максим Горпиніч <gorpinicmaksim5@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic4@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: 송의찬 <arksos1357@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/af/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ckb/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fil/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
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/or/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/qt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ckb/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/es_AR/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/nl/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2025-09-13 20:02:05 +02:00
KingLucius
2d617da99a
feat(Emulator UI): Click anywhere to hide Episodes (#1899) 2025-09-10 14:43:35 +02:00
KingLucius
994211d32f
Fix: crash on API version < 25 (#1900) 2025-09-10 01:19:17 +02:00
firelight
9b50b11bfa
Feat: Added "Mexican Spanish" as a language in subtitlehelper 2025-09-05 18:13:11 +00:00
Diogo
569f593793
SubtitleHelper: add IETF BCP 47 and OpenSubtitles (#1863) 2025-09-05 20:02:05 +02:00
rockhero1234
bf630923c7
ui:2x alignment and animation (#1885) 2025-09-05 19:52:56 +02:00
BlipBlob
d2472f0d2a
Fix broken resolveUsingWebView (#1887)
* webview

* webview 2
2025-09-05 19:49:44 +02:00
KingLucius
b740afd648
feat(TV UI): Netflix-like Horizontal scrolling behavior (#1888) 2025-09-05 19:45:33 +02:00
BlipBlob
909cdbfffc
Paginated search (#1731) 2025-09-04 01:01:57 +02:00
firelight
4a36048b3e
Fix: Made plugin languages filter only show existing languages. 2025-09-04 00:10:19 +02:00
firelight
91fe001103
Fix: Change color of 2x to always be white, also Fixes #1882 2025-09-03 23:10:53 +02:00
rockhero1234
94c560071a
ui:improve 2x overlay btn (#1883)
* ui:improve 2x overlay btn

* no foreground

* tint in drawable
2025-09-03 23:05:54 +02:00
firelight
c5d1b5f87e
Fix(TV): Focus for reload 2025-08-31 23:40:53 +02:00
rockhero1234
2cce3fe6ad
feat:reload provider btn on emulator and tv (#1877) 2025-08-31 20:01:31 +02:00
Jace
8215aa0b19
[skip ci] feat: Created discoverium.yml file to be discoverable by Discoverium. (#1876) 2025-08-31 19:52:37 +02:00
firelight
27a125705c
Fix(TV): FCast crashing, Closes #1872 2025-08-31 19:51:02 +02:00
firelight
0ffe3822c4
Change versionName to 4.5.5 2025-08-30 17:10:48 +02:00
rockhero1234
37caf794ea
feat:shareable movie/tvshow links (#1860) 2025-08-30 17:08:37 +02:00
rockhero1234
20109d89fc
feat:reload provider on long press source btn (#1871) 2025-08-29 16:28:18 +02:00
rockhero1234
2c4894cc14
feat:head profile image on phone (#1868) 2025-08-29 16:24:05 +02:00
firelight
e8b7829d90
Revert "Hide PIP settings option on TV (#1829)" (#1869)
This reverts commit b2ce0f81f2.
2025-08-27 23:57:56 +02:00
rockhero1234
e60453ba2b
feat:mark as watched up to this episode (#1828) 2025-08-24 15:38:25 +02:00
Phisher98
e193214759
Minor Fix for Filemoon (#1858) 2025-08-22 16:29:57 +02:00
firelight
6cc8b01119
Feat(TV): Added network stream to downloads tab, and updated nextfocus for easier navigation. Closes #1283 2025-08-21 01:13:13 +02:00
firelight
b87e85ba7d
Fix: Force reload account when loading homepage, Closes #1851 2025-08-20 19:20:14 +02:00
Phisher98
b32a27b76f
Dailymotion Minor Fix (#1848)
* Dailymotion

* Dailymotion

* Dailymotion
2025-08-19 17:09:28 +02:00
firelight
5122a72adf
Fix: popCurrentPage fix in player 2025-08-18 20:55:33 +02:00
Phisher98
ec093fb36c
DailyMotion Fix (#1844) 2025-08-18 20:54:42 +02:00
rockhero1234
c4a111ef3a
fix:retain old RepositoryData constructor (#1841) 2025-08-17 14:52:04 +02:00
firelight
ddfaf7e701
Fix: Sync step size crashfix, Closes #1838 2025-08-16 22:09:56 +02:00
KingLucius
b2ce0f81f2
Hide PIP settings option on TV (#1829) 2025-08-15 23:58:25 +02:00
rockhero1234
f3ff6bb4a5
feat:local profile photo (#1826) 2025-08-13 15:40:04 +02:00
rockhero1234
54366bb6c6
feat:added repo icon (#1825)
* feat:added repo icon

* fixes
2025-08-13 15:25:48 +02:00
int3debug
1a36bb4523
fix(setup): mark setup as completed when skipping (#1824) 2025-08-12 15:03:00 +02:00
firelight
4169fd5ae4
Fix: Migrate to authdata for better set/get key security, split up auth into files, added preliminary BackupAPI, use Result instead of Resource. (#1822) 2025-08-12 00:51:29 +02:00
firelight
d103884ac0
Chore: Removed unused code 2025-08-11 21:44:29 +02:00
Diogo
89dee1bd72
Fix: unclosed file descriptor, bump libs min ver (#1818)
* Fix: close inputPFD:ParcelFileDescriptor in AppContextUtils
* Security: bump libs minor version
2025-08-11 21:42:03 +02:00
rockhero1234
eecce5b007
Always show pinned providers (#1820)
* always show pinned

* fix
2025-08-11 18:22:21 +02:00
firelight
df9d16a110
Fix: Readded old tokens to nonTransferableKeys, added invalidateToken to AuthAPI 2025-08-08 18:11:13 +02:00
KingLucius
0054b58b63
Show PIN login only on TV UI (#1814) 2025-08-08 13:53:34 +02:00
firelight
87e89fb5cc
Chore: Code cleanup for SyncAPI, and added supportedWatchTypes 2025-08-08 01:07:59 +02:00
firelight
ab16d24296
Fix: Updated error message to show correct fail error 2025-08-07 19:14:22 +02:00
firelight
f5e9393305
Hotfix(Simkl): Fixed authorization 2025-08-07 19:07:37 +02:00
firelight
fc621cb4d5
Feat(MainAPI): Added headers to SubtitleFile (#1810)
* Feat(MainAPI): Added headers to SubtitleFile

* Feat: Added headers to subtitle implementation
2025-08-07 18:56:29 +02:00
firelight
fcada71b67
Initial migration to stateless auth (#1803)
* Initial migration to stateless auth

* Fix: Fixed AuthLoginResponse password, and made sure access token is non-null

* Docs: Added more docummentation for Auth

* Feat: Tie accounts ids to CloudStream accounts, and added none account
2025-08-07 18:30:23 +02:00
Phisher98
8573de14fd
Improved StreamWishExtractor and Added FileMoon Extractor Separately and Improved also Filsim (#1808) 2025-08-05 18:55:16 +02:00
firelight
17c3584004
Fixed missing imports from last commit 2025-08-05 15:32:50 +02:00
firelight
17c46cf9a5
Fix(Android 14+): Inconsistent back button behavior in player, Closes #1804 2025-08-05 15:29:50 +02:00
firelight
940060c82d
Chore: Migrate to Score api for personalRating 2025-07-31 22:41:22 +00:00
firelight
7610a6b9d5
Chore: Migrate to Score api for AbstractSyncStatus 2025-07-31 22:31:46 +00:00
firelight
f71f22a7d4
Fix(Crash): Avoid crash on LoudnessEnhancer exception, change color instead 2025-07-29 00:55:10 +00:00
firelight
c4cf2d9ca6
Fix: Made more navigation buttons scroll to top on long click 2025-07-29 00:14:58 +00:00
firelight
ef7a970efe
Fix: Added hashCode and equals to Score, fixes inconsistent AsyncListDiffer behavior 2025-07-28 22:34:23 +00:00
firelight
905edfbd94
Fix(UI): Do not show rating if below 0.1/10.0 2025-07-24 13:55:53 +00:00
rockhero1234
0197a65017
Toggle rating label (#1782) 2025-07-24 15:52:27 +02:00
firelight
6daa2946f5
Fix(Player): Fixed motion events in vertical video, Closes #1778 2025-07-23 22:32:02 +00:00
rockhero1234
6ddc88d269
Show estimated download time in notification (#1770) 2025-07-24 00:08:13 +02:00
Nivin
0e44e19e10
Add DRM request headers support in player (#1768) 2025-07-23 20:09:55 +02:00
Osten
16004b8a93
Fix TV backbutton 2025-07-23 14:01:35 +00:00
int3debug
1fd561f5e5
feat: enhance player error logging with url, headers, and referer (#1756) 2025-07-23 14:59:08 +02:00
rockhero1234
13a49063b1
feat:pin providers (#1761) 2025-07-23 14:37:59 +02:00
firelight
4a5ad63c35
Fix: Weird inconsistent subtitle elevation fix? Closes #1777 2025-07-23 12:08:54 +00:00
Osten
91652da920 Bump to 4.5.4 for backwards compatible constructors 2025-07-22 21:20:48 +02:00
firelight
c6254d30a9
Update .gitignore 2025-07-22 18:14:36 +02:00
Onur Civanoğlu
cc5555979a
Add quick scroll-to-top functionality via long press on Home button (#1763) 2025-07-22 18:06:08 +02:00
Osten
aeba48e1ec Merge branch 'master' of https://github.com/recloudstream/cloudstream 2025-07-22 16:26:12 +02:00
Osten
f689dedf68 Bump to 4.5.3 2025-07-22 16:25:54 +02:00
Sarlay
555d977c69
Added a rating preview on the poster (#1745) 2025-07-21 22:16:31 +02:00
Phisher98
ed37c073b1
Dailymotion Improvement to Add Subtitles (#1765) 2025-07-21 15:00:28 +02:00
int3debug
8cc922006f
chore: downgrade media3 to 1.6.1 due to ExoPlaybackException (#1757) 2025-07-19 21:07:03 +02:00
firelight
276bf9f986
Fix(UI): Updated style for buttons in dialog, Closes #1753 2025-07-19 19:04:22 +00:00
firelight
8b6d0ec8c4
Added new class Score (#1748) 2025-07-19 18:09:50 +02:00
firelight
1226b019c6
Fix(TV): Added focus outline to subtitle color selection 2025-07-18 16:38:18 +00:00
int3debug
9d58f04167
chore: update media3 to 1.8.0-beta01 (#1743) 2025-07-18 14:06:16 +02:00
firelight
fc00204ebf
Fix(Phone): Disable seekbar when not shown, Closes #1749 2025-07-17 23:06:57 +00:00
firelight
cdee3ca6aa
Fix: Added top padding to subtitles to prevent {\an7-9} to display outside the screen, Closes #1747 2025-07-17 22:46:13 +00:00
firelight
cd6423ace7
Fix: Small fix to override aligment for some {\anX} types 2025-07-17 22:39:55 +00:00
Osten
b29ba766b0
Updated ErrorLoadingException for Player Toast, fixes torrent blocking other links 2025-07-15 17:23:58 +00:00
Osten
da55a87792
Fixed subtitle crash 2025-07-14 14:11:28 +00:00
firelight
990ce2b8e3
Feat: Added better support for use as an external 2025-07-12 21:18:15 +02:00
firelight
dede16aadb
Feat: Added JustPlayer 2025-07-12 19:49:38 +02:00
firelight
8582f405c3
Fix: Disable 2x for non fullscreen trailers 2025-07-12 19:25:00 +02:00
firelight
c7bbc29743
Feat: Added NextPlayer 2025-07-12 19:20:59 +02:00
firelight
020496353a
Fix: Disable 2x while locked 2025-07-12 19:06:48 +02:00
firelight
d62b969252
Fix: Only show "AlwaysAskAction" in settings 2025-07-12 17:44:37 +02:00
firelight
58b8279f49
Added LibreTorrent and BiglyBT, Closes #1741 2025-07-12 17:37:27 +02:00
Nivin
68b281cd98
Add 'Always ask' option for video player selection (#1723)
* Add 'Always ask' option for video player selection

Introduces an 'Always ask' option in player settings, allowing users to choose a player each time an episode is played. Updates the settings UI, string resources, and episode handling logic to support this feature.

* Refactor AlwaysAskAction as a dedicated VideoClickAction

Introduced AlwaysAskAction as a dedicated VideoClickAction for the 'Always Ask' player option. Refactored logic in ResultViewModel2 to handle player selection dialog via AlwaysAskAction instead of a special action constant. Cleaned up related code and removed the now-unnecessary string and constant definitions.
2025-07-12 16:42:26 +02:00
rockhero1234
0f0d541c53
Added 2x speed on hold (#1729)
* Added 2x speed on hold

* Toggle in Setting & fixes

* fix
2025-07-12 16:39:00 +02:00
BlipBlob
3636a34df7
Make cue rules apply to embedded subtitles (#1735)
* player

* fixes

* Bugfix with styles

---------

Co-authored-by: firelight <147925818+fire-light42@users.noreply.github.com>
2025-07-12 16:27:23 +02:00
BlipBlob
8d25c933cb
Update M3u8Helper.kt (#1733) 2025-07-02 19:04:32 +02:00
CranberrySoup
fa36bcb9f0
Fix subtitle overlap (#1726)
* Update CustomSubtitleDecoderFactory.kt
2025-07-02 01:29:58 +02:00
firelight
3dd7467003
Fix(M3u8 Downloader Part 3): Updated audio detection to conform with the RFC 2025-07-01 13:58:21 +02:00
firelight
d136226fb1
Fix(M3u8 Downloader Part 2): Updated HlsPlaylistParser to include invalid/missing codec and mime types 2025-07-01 01:41:39 +02:00
firelight
8f57588108
Update VideoClickAction.kt 2025-06-30 17:51:29 +02:00
firelight
795629ee42
Feat: Added Vlc Nightly, Closes #1725 2025-06-30 16:16:50 +02:00
firelight
c2e6604fa4
Fix(M3u8 Downloader): Fixed m3u8 missing audio bugs in downloads by adding a real m3u8 parser, and fixed a bug crashing the downloader caused by missing encryption (#1727) 2025-06-30 16:06:58 +02:00
Osten
6780b98330
Fixed issues with embedded subtitles 2025-06-27 23:09:21 +02:00
firelight
5cbbc1bd12
Fix(TV UI): Made focus in settings highlight with border for consistency and reliability 2025-06-27 19:37:39 +02:00
firelight
76c80e2c89
Feat: Added skip loading for acquire links, and updated UI for the dialog 2025-06-27 18:14:44 +02:00
firelight
6609a11a23
Feat: Minor change to skip loading to show how many links are loaded 2025-06-27 00:03:57 +02:00
firelight
63aaba2993
Chore: Replaced all instances of outdated mvvm apis (amap) 2025-06-26 23:44:52 +02:00
firelight
d2e64f29c7
Chore: Replaced all instances of outdated mvvm apis 2025-06-26 23:34:58 +02:00
firelight
fabef62ea6
Fix(TV): Made backbutton hide episodes instead of exit, Closes #1268 2025-06-26 21:57:04 +02:00
firelight
aaa2821f31
Feat: Added settings for poster size, and changed back to hardware bitmaps for #1720 2025-06-26 18:50:41 +02:00
firelight
409a0aadc3
Feat(TV): Added long-hold popup parity, to replace toast for TV 2025-06-26 01:44:49 +02:00
firelight
d1593ee22e
Fix: Fixed the subtitle elevation in player when opening the UI 2025-06-23 17:09:36 +02:00
firelight
1a841f845d
Feat: Added overscanning setting for TV, Closes #1557 2025-06-23 02:47:34 +02:00
firelight
df15813540
fix: MainAPI added uniqueUrl for consistent keys on JSON 2025-06-23 02:04:14 +02:00
firelight
244484f609
Feat: Add go to downloads button in home if there is no connection 2025-06-23 01:13:19 +02:00
firelight
3d30f9d99b
chore: Removed unused parameters in Resource.Failure, and readded title_shadow for test layout 2025-06-23 00:52:32 +02:00
Luna712
fedfb2250e
Fix typos in unimplimented methods in MainAPI (#1549) 2025-06-23 00:32:42 +02:00
firelight
bdb1e12f3b
feat(TV) : Remove all continue watching items 2025-06-23 00:14:30 +02:00
firelight
959048936a
Minor fix for CloudMailRu 2025-06-22 23:16:51 +02:00
Kraptor123
fe8ca8672d
CloudMailRuExtractor (#1715) 2025-06-22 23:14:55 +02:00
firelight
ce3298822c
Added settings for parallel, and concurrent downloads. Closes #1714 2025-06-22 21:46:29 +02:00
firelight
6f30bb2df4
Fixed #1717 2025-06-22 16:36:07 +02:00
Osten
b3b3559c18 Added more checks for ___DO_NOT_CALL_FROM_A_PLUGIN 2025-06-17 19:20:51 +02:00
Phisher98
2db253eb31
Vidstack Improvements (#1707) 2025-06-17 00:37:17 +02:00
Kraptor123
6280bbd6e9
minor fix (#1705) 2025-06-12 22:49:30 +02:00
Phisher98
20b1d754a6
GDMirrorbot Fix (#1706) 2025-06-12 22:49:13 +02:00
redblacker8
3654831d8d
Fix vidhidepro for other case (#1689) 2025-05-30 00:28:45 +00:00
recloudstream[bot]
982dc9b5ba chore(locales): fix locale issues 2025-05-30 00:27:58 +00:00
Weblate (bot)
f892b66754
Translated using Weblate (#1692) 2025-05-30 00:27:44 +00:00
feroxx
f71ea63224
Fix Trailer and yt playing (#1694)
* Bump newpipeextractor to v0.24.6
2025-05-30 00:26:46 +00:00
Phisher98
0281675ae8
Voe Fix (#1696) 2025-05-20 16:16:40 +00:00
recloudstream[bot]
5183024b7a chore(locales): fix locale issues 2025-05-16 00:10:07 +00:00
Weblate (bot)
18caefa9f9
Translations update from Weblate
Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (German)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Hindi)

Currently translated at 54.3% (425 of 782 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (782 of 782 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'

Translated using Weblate (Tamil)

Currently translated at 95.5% (747 of 782 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Arabic)

Currently translated at 99.4% (778 of 782 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (French)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (French)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (781 of 782 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (782 of 782 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (782 of 782 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Spanish)

Currently translated at 99.8% (779 of 780 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (780 of 780 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (780 of 780 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Greek)

Currently translated at 99.7% (775 of 777 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Assamese)

Currently translated at 97.2% (756 of 777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (German)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 96.7% (752 of 777 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.4% (773 of 777 strings)

Translated using Weblate (Swedish)

Currently translated at 97.4% (757 of 777 strings)

Translated using Weblate (Polish)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Macedonian)

Currently translated at 99.0% (770 of 777 strings)

Translated using Weblate (Italian)

Currently translated at 99.6% (774 of 777 strings)

Translated using Weblate (Croatian)

Currently translated at 99.3% (772 of 777 strings)

Translated using Weblate (Czech)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Arabic)

Currently translated at 99.8% (776 of 777 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (French)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (777 of 777 strings)

































Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ja/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translation: Cloudstream/App

Co-authored-by: Ahmed Abd El-Fattah <a.aelfattah5@gmail.com>
Co-authored-by: Boqirz <alveromodar@gmail.com>
Co-authored-by: Christopher Allen <nsn90255@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Do you know my name? <anonymous2676@users.noreply.hosted.weblate.org>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: GiannosOB <giannos2105@gmail.com>
Co-authored-by: Harshit Sethi <hstsethi@outlook.com>
Co-authored-by: Ivan Kostov <jiveq1@gmail.com>
Co-authored-by: Lacey Anaya <yecakeh263@anawalls.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Plamen Ivanov <paco.iwanow@gmail.com>
Co-authored-by: Quentin <mrbydix@users.noreply.hosted.weblate.org>
Co-authored-by: Ramazan S <me@ramazansancar.com.tr>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Serdar Sağlam <teknomobil@msn.com>
Co-authored-by: Sergey Ponomarev <stokito@gmail.com>
Co-authored-by: dedakir923 <dedakir923@exoular.com>
Co-authored-by: khalidbelk <khalid.belkassmi-el-hafi@epitech.eu>
Co-authored-by: maxim <maximtested@gmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: william piti <loolyowo@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
2025-05-16 00:09:52 +00:00
Bnyro
418da4da7e
fix: vidhide fails to extract relative urls (#1691) 2025-05-15 23:55:28 +00:00
firelight
4217a716cd
Updated the safe function for more ergonomic code (#1675) 2025-04-29 20:33:52 +02:00
firelight
19220557f8
Better video cache abrupt exit 2025-04-27 22:21:44 +02:00
firelight
31ed9e9bf1
Fixed caption regex 2025-04-27 21:28:52 +02:00
IndusAryan
3455a25469
fix: fixed coil headers again again 💀 💀 (#1668)
* fixed coil headers again again 💀 💀

* add few logs and verbose function name

* update agp again
2025-04-27 20:53:55 +02:00
Phisher98
149783b26f
Minor Fix for AniListApi Recommendation null (#1672)
* Minor Fix for VidStack

* Fix Voe Extractor

* Minor Fix for AniListApi Recommendation null
2025-04-27 20:41:14 +02:00
Phisher98
00aa63682a
Fix Voe Extractor (#1667)
* Minor Fix for VidStack

* Fix Voe Extractor
2025-04-21 21:08:40 +02:00
Phisher98
1ffc8dd147
Minor Fix for VidStack (#1666) 2025-04-17 19:15:48 +02:00
Luna712
590fbd89db
Add volume booster above 100% (#1444) 2025-04-17 19:13:49 +02:00
firelight
775d117a97
Added more subtitle options and general subtitle fixes.
Added Bold, Italic and Background radius. Fixed subtitle size override, and anX anchor
2025-04-16 23:27:37 +02:00
Osten
6990f58d68
Fixed pause unpause issue 2025-04-12 21:58:08 +02:00
Bnyro
e0f2299d1d
feat: add secvideo (fsst, dsst, csst) extractor (#1654) 2025-04-10 19:48:56 +02:00
Bnyro
6fd5548f8a
feat: add nathanfromsubject mirror of voe (#1657) 2025-04-10 19:48:00 +02:00
int3debug
97f33c6b6e
refact: unify torrent error messages (#1655) 2025-04-10 19:45:16 +02:00
Bnyro
adb4315eb2
feat: add vinovo extractor (#1649) 2025-04-09 18:13:01 +02:00
Bnyro
706581c4a2
feat: add bigwarp extractors (#1650) 2025-04-08 21:58:26 +02:00
Bnyro
18c87c9361
feat: add earnvids extractors based on vidhide and fix vidhide (#1651) 2025-04-08 21:57:34 +02:00
Osten
381e3ebf25
Removed Prerelease annotation 2025-04-04 22:20:18 +02:00
Osten
d9541585c7
Fix name of MyDramaListDemo 2025-04-04 22:05:57 +02:00
KingLucius
66e48c7d49
Fix MyDramaList Api key (#1642)
* Fix MyDramaList Api key

* Update build.gradle.kts
2025-04-04 22:00:54 +02:00
Osten
ea8bb101fb
Bump to 4.5.2 2025-04-04 21:49:49 +02:00
recloudstream[bot]
ed2aaea0ee chore(locales): fix locale issues 2025-04-04 19:48:41 +00:00
Osten
3e739fdc1a
Merge pull request #1537 from recloudstream/weblate
Translations update from Hosted Weblate
2025-04-04 21:48:27 +02:00
Hosted Weblate
0ef0540d8c
Merge remote-tracking branch 'origin/master' 2025-04-04 21:47:57 +02:00
KingLucius
ce71940a34
Long click source to copy URL (#1638) 2025-04-04 21:47:53 +02:00
Hosted Weblate
6055278c5c
Translated using Weblate (Spanish)
Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.6% (774 of 777 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (German)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (777 of 777 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (777 of 777 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (774 of 775 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Afrikaans)

Currently translated at 30.1% (233 of 773 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Romanian)

Currently translated at 91.9% (711 of 773 strings)

Translated using Weblate (Romanian)

Currently translated at 91.9% (711 of 773 strings)

Translated using Weblate (Hindi)

Currently translated at 54.4% (421 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Swedish)

Currently translated at 98.0% (758 of 773 strings)

Translated using Weblate (French)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Persian)

Currently translated at 50.1% (388 of 773 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Russian)

Currently translated at 97.4% (753 of 773 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (772 of 773 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (772 of 773 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.6% (770 of 773 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (773 of 773 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (773 of 773 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'

Translated using Weblate (German)

Currently translated at 99.7% (769 of 771 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Persian)

Currently translated at 49.5% (382 of 771 strings)

Translated using Weblate (Persian)

Currently translated at 46.3% (357 of 771 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (770 of 771 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.8% (770 of 771 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (771 of 771 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (770 of 771 strings)

Translated using Weblate (Japanese)

Currently translated at 25.0% (1 of 4 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (770 of 771 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (English)

Currently translated at 100.0% (771 of 771 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (765 of 765 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (765 of 765 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (756 of 756 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Azerbaijani)

Currently translated at 34.2% (259 of 756 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Spanish)

Currently translated at 99.8% (755 of 756 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (755 of 756 strings)

Translated using Weblate (French)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (756 of 756 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Hungarian)

Currently translated at 86.6% (655 of 756 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.4% (752 of 756 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'

Translated using Weblate (Vietnamese)

Currently translated at 99.8% (755 of 756 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.0% (749 of 756 strings)

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Korean)

Currently translated at 96.0% (726 of 756 strings)

Translated using Weblate (Macedonian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Assamese)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Azerbaijani)

Currently translated at 30.0% (227 of 756 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (756 of 756 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'

Translated using Weblate (Arabic (Levantine))

Currently translated at 99.8% (755 of 756 strings)

Translated using Weblate (Korean)

Currently translated at 95.7% (724 of 756 strings)

Translated using Weblate (qt (generated) (qt))

Currently translated at 98.1% (742 of 756 strings)

Translated using Weblate (German)

Currently translated at 98.8% (747 of 756 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (753 of 756 strings)

Translated using Weblate (Russian)

Currently translated at 99.3% (751 of 756 strings)

Translated using Weblate (Urdu)

Currently translated at 92.4% (699 of 756 strings)

Translated using Weblate (Tamil)

Currently translated at 98.6% (746 of 756 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.9% (748 of 756 strings)

Translated using Weblate (Romanian)

Currently translated at 93.1% (704 of 756 strings)

Translated using Weblate (Macedonian)

Currently translated at 99.6% (753 of 756 strings)

Translated using Weblate (Greek)

Currently translated at 93.2% (705 of 756 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (751 of 756 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.6% (753 of 756 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.4% (752 of 756 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Vietnamese)

Currently translated at 98.9% (748 of 756 strings)

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Portuguese)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (French)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Assamese)

Currently translated at 100.0% (756 of 756 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'

Translated using Weblate (Arabic (Levantine))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Vietnamese)

Currently translated at 98.9% (748 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (756 of 756 strings)

Merge remote-tracking branch 'origin/master'

Merge remote-tracking branch 'origin/master'

Translated using Weblate (Dutch)

Currently translated at 87.2% (657 of 753 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (753 of 753 strings)

Co-authored-by: 404NotFound <404others@pm.me>
Co-authored-by: Ahmed Abd El-Fattah <a.aelfattah5@gmail.com>
Co-authored-by: An Nguyen <annguyen.it.utc@gmail.com>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Balázs Sipos <balazs.sipos118@gmail.com>
Co-authored-by: Bruno Fragoso <darth_signa@hotmail.com>
Co-authored-by: Chip <itsjustchipoverhere@gmail.com>
Co-authored-by: Christo <jchristomuller@gmail.com>
Co-authored-by: Cris P <cristineipop2015@gmail.com>
Co-authored-by: Dan <jonweblin2205@protonmail.com>
Co-authored-by: Doctorredits_here <182783629+doctorreditshere@users.noreply.github.com>
Co-authored-by: Esspel <eric.soderstrom06@gmail.com>
Co-authored-by: Fikri Akbar <akbarfikri1221@gmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Francisco Serrador <fserrador@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Itsmechinmoy <itsmechinmoy@users.noreply.hosted.weblate.org>
Co-authored-by: Jamil Farajov <jamilfarajov@gmail.com>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: KING APPS <kiperking1@gmail.com>
Co-authored-by: Konstantin <konstantinkreutz@gmail.com>
Co-authored-by: LaTruite <sanchezlatruite@tutamail.com>
Co-authored-by: Lancelot65 <lancelotbulte@gmail.com>
Co-authored-by: Lionharth <leon.fredriksson@icloud.com>
Co-authored-by: Maddyhf <madhavsharma91458@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Matthaiks <kitynska@gmail.com>
Co-authored-by: Metehan Öztürk <metehan61ozturk@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mina Myoui <myouiminayang@gmail.com>
Co-authored-by: MindustryLogic <xyndermultilanguagelism@gmail.com>
Co-authored-by: Nguyễn Tiến Đạt <dn16092000@gmail.com>
Co-authored-by: Ohirok <ohirok@users.noreply.hosted.weblate.org>
Co-authored-by: Parsa Nobahari <parsa.nobahari@gmail.com>
Co-authored-by: Pizza Party <paol.m@proton.me>
Co-authored-by: Ramazan S <me@ramazansancar.com.tr>
Co-authored-by: Rere Doloi <reredolire@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Riccardo Giordano <rickygiordano03@gmail.com>
Co-authored-by: SBS1313 <simonsaade005@gmail.com>
Co-authored-by: Saúl Palacios <palacios22c@gmail.com>
Co-authored-by: Serdar Sağlam <teknomobil@msn.com>
Co-authored-by: Siber <siber@users.noreply.hosted.weblate.org>
Co-authored-by: Tom Brand <tom9358@gmail.com>
Co-authored-by: dedakir923 <dedakir923@exoular.com>
Co-authored-by: gacarel <gacarel657@bariswc.com>
Co-authored-by: hugoalh <hugoalh@users.noreply.hosted.weblate.org>
Co-authored-by: leyakid803 <leyakid803@minduls.com>
Co-authored-by: stojkovskistefan <stefanstojkovski@gmail.com>
Co-authored-by: zmni <zmni@outlook.com>
Co-authored-by: М Ш <muslimsahmurzaev@gmail.com>
Co-authored-by: Максим Горпиніч <maksimgorpinic2005a@gmail.com>
Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Co-authored-by: 송의찬 <arksos1357@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/af/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/apc/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/as/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/az/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ko/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/qt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ta/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ur/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/fastlane/ja/
Translation: Cloudstream/App
Translation: Cloudstream/Fastlane
2025-04-04 21:47:41 +02:00
KingLucius
f5cd549b69
feat(Extensions): newExtractorLink for developers (#1632)
* feat(Extensions): newExtractorLink for developers

* remove isM3u8 and use type
2025-04-04 21:47:34 +02:00
firelight
4c610aa40c
Fix: Removed head request from downloads as it is slow and timeout does not work 2025-04-02 00:08:30 +00:00
CranberrySoup
5e90819802
Group subtitles with the same name (#1628)
* Group subtitles

* fix empty names
2025-04-01 19:52:57 +02:00
CranberrySoup
da3c4b8181
Fix gradient overlay on loading (#1637) 2025-04-01 19:39:31 +02:00
Osten
b5dd0f1cb9
Nicehttp -> 0.4.13 2025-03-31 01:00:27 +02:00
KingLucius
b9df1387d8
Audio track language in Tracks dialog (#1629) 2025-03-30 16:29:17 +02:00
firelight
4cc398447e
Update prerelease.yml MDL_API_KEY 2025-03-29 20:06:00 +01:00
KingLucius
39a3151cb4
feat(Extensions): MyDramaList Meta provider (#1622)
* feat(Extensions): MyDramaList Meta provider

* use JsonProperty

* BuildConfig KEY & applyMedia
2025-03-29 20:03:01 +01:00
Datj0000
1e69418044
feat: voice search (#1623) 2025-03-29 19:49:32 +01:00
firelight
7e148a4d6a
Biometric crashfix 2025-03-29 18:21:49 +00:00
KingLucius
bc557d1660
Bump biometric & navigation Libs (#1625) 2025-03-29 19:15:09 +01:00
Matt Alexander
950eb6f759
Update README.md (#1626)
Fix docs typo
2025-03-29 18:18:18 +01:00
firelight
d92fbd3f99
Bugfix: MKV Handle any recursive query 2025-03-27 19:23:06 +00:00
KingLucius
7aaa82ef4f
Bump Media3 to 1.6.0 (#1624)
Co-authored-by: firelight <147925818+fire-light42@users.noreply.github.com>
2025-03-27 18:49:55 +01:00
IndusAryan
d5aa758b15
chore: update dependencies and gradle plugin version (#1577)
* Update some libs

* bump gradle to 8.13
2025-03-27 18:46:20 +01:00
Phisher98
79c7e0312c
Update VidStack.kt (#1611)
Making them Public
2025-03-16 12:51:38 +01:00
Phisher98
a03cc93453
Update GDMirrorbot.kt (#1612) 2025-03-16 12:51:27 +01:00
Bnyro
17fe74d432
fix: remove dead code from filesim extractor (#1607) 2025-03-13 20:27:30 +01:00
firelight
429be4cc13
Added timeout API 2025-03-12 19:30:21 +00:00
firelight
d7355c4772
Fix: Added ID_SEEK_HEAD parsing for MKV container. (#1604) 2025-03-11 20:26:09 +01:00
firelight
8f773f20fb
Chore: Bump NiceHttp to 0.4.12 2025-03-11 14:13:26 +00:00
firelight
4c4eee71cb
Fix: Prevent seeking on non seekable media 2025-03-10 21:12:48 +00:00
firelight
75ee406c4e
Fix: Added timeout of 2min to MainAPI 2025-03-09 01:29:40 +00:00
firelight
44fadaf9b0
fix: Released and removed unused data 2025-03-09 01:02:51 +00:00
firelight
f915cd0c20
Feat: Added player notification and play in background via PIP. (#1585)
* Feat: Added player notification and play in background via PIP.
2025-03-08 17:06:16 +01:00
firelight
935fd43d16
Fixed shitty extentions 2025-03-08 15:29:00 +00:00
firelight
c21cf0c115
Fix: getPairList duplicate + reset null 2025-03-08 14:52:56 +00:00
firelight
67d49bcb06
Revert "Filesim (#1597)" (#1599)
This reverts commit ba33f761ed.
2025-03-08 02:04:18 +01:00
Redowan Ahmed Apu
a064fddd96
Get embed url (#1592)
optimize import and regex
2025-03-08 01:04:03 +01:00
Phisher98
ba33f761ed
Filesim (#1597) 2025-03-08 01:02:56 +01:00
int3debug
8d0612a9ae
feat(ui): add torrent sorting option to plugin fragment (#1587) 2025-03-06 12:33:27 +01:00
Phisher98
ade27410ed
Adding LuluStream Extractor Separately and Fixed (#1588)
* Adding VidStack Extractor

* Adding VidStack Extractor

* Adding GDMirrorbot Extractor

* Adding LuluStream Extractor Separately and Fixed
2025-03-06 12:28:00 +01:00
IndusAryan
db9e362fbb
refactor: convert monkey activity 🐒 into a fragment monke 🦍 (#1576)
* Turn monkey activity 🐒 into a fragment monke 🦍
2025-03-05 00:45:12 +01:00
Bnyro
127a9c006d
fix: filesim extractor doesn't work anymore due to embeds (#1581) 2025-03-03 18:22:40 +01:00
Phisher98
cdb70d5707
Adding VidStack and GDMirrorbot Extractor (#1579) 2025-03-03 18:21:52 +01:00
firelight
0a38434dec
Fixed seasons with one episode 2025-03-01 12:50:05 +00:00
Phisher98
8bea8f6a49
Adding Manually Update Plugins (#1512) 2025-03-01 01:02:59 +01:00
IndusAryan
1c6db0e514
refactor: overall navigation mechanism and split logic #1429 (#1430) 2025-03-01 00:41:14 +01:00
Gaurang Khatavkar
ab23f23c71
Feature: episode sort button (#1565)
- Add sort direction indicator (▲/▼) to button text
2025-02-28 21:26:40 +01:00
Osten
846ff38c3b
Lazy system.load torrent lib 2025-02-26 19:12:16 +01:00
730 changed files with 71263 additions and 28847 deletions

30
.github/locales.py vendored
View file

@ -1,14 +1,13 @@
import re import re
import glob import glob
import requests import requests
import os
import lxml.etree as ET # builtin library doesn't preserve comments import lxml.etree as ET # builtin library doesn't preserve comments
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt" SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
START_MARKER = "/* begin language list */" START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */" END_MARKER = "/* end language list */"
XML_NAME = "app/src/main/res/values-" XML_NAME = "app/src/main/res/values-b+"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json" ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
INDENT = " "*4 INDENT = " "*4
@ -21,29 +20,29 @@ rest, after_src = rest.split(END_MARKER)
# Load already added langs # Load already added langs
languages = {} languages = {}
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest): for lang in re.finditer(r'Pair\("(.*)", "(.*)"\)', rest):
flag, name, iso = lang.groups() name, iso = lang.groups()
languages[iso] = (flag, name) languages[iso] = name
# Add not yet added langs # Add not yet added langs
for folder in glob.glob(f"{XML_NAME}*"): for folder in glob.glob(f"{XML_NAME}*"):
iso = folder[len(XML_NAME):] iso = folder[len(XML_NAME):].replace("+", "-")
if iso not in languages.keys(): if iso not in languages.keys():
entry = iso_map.get(iso.lower(),{'nativeName':iso}) entry = iso_map.get(iso.lower(), {'nativeName':iso}) # fallback to iso code if not found
languages[iso] = ("", entry['nativeName'].split(',')[0]) languages[iso] = entry['nativeName'].split(',')[0] # first name if there are multiple
# Create triples # Create pairs
triples = [] pairs = []
for iso in sorted(languages.keys()): for iso in sorted(languages, key=lambda iso: languages[iso].lower()): # sort by language name
flag, name = languages[iso] name = languages[iso]
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),') pairs.append(f'{INDENT}Pair("{name}", "{iso}"),')
# Update settings file # Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write( open(SETTINGS_PATH, "w+",encoding='utf-8').write(
before_src + before_src +
START_MARKER + START_MARKER +
"\n" + "\n" +
"\n".join(triples) + "\n".join(pairs) +
"\n" + "\n" +
END_MARKER + END_MARKER +
after_src after_src
@ -62,8 +61,5 @@ for file in glob.glob(f"{XML_NAME}*/strings.xml"):
with open(file, 'wb') as fp: with open(file, 'wb') as fp:
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n') fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False) tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
# Remove trailing new line to be consistent with weblate
fp.seek(-1, os.SEEK_END)
fp.truncate()
except ET.ParseError as ex: except ET.ParseError as ex:
print(f"[{file}] {ex}") print(f"[{file}] {ex}")

View file

@ -9,6 +9,9 @@ on:
- '**/wcokey.txt' - '**/wcokey.txt'
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
concurrency: concurrency:
group: "Archive-build" group: "Archive-build"
cancel-in-progress: true cancel-in-progress: true
@ -24,6 +27,7 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets" repository: "recloudstream/secrets"
- name: Generate access token (archive) - name: Generate access token (archive)
id: generate_archive_token id: generate_archive_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v2
@ -31,14 +35,18 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive" repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: '17' distribution: temurin
distribution: 'adopt' java-version: 17
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Fetch keystore - name: Fetch keystore
id: fetch_keystore id: fetch_keystore
run: | run: |
@ -49,24 +57,33 @@ jobs:
KEY_PWD="$(cat keystore_password.txt)" KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}" echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Run Gradle - name: Run Gradle
run: | run: ./gradlew assemblePrereleaseRelease
./gradlew assemblePrerelease
env: env:
SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
- uses: actions/checkout@v4 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: with:
repository: "recloudstream/cloudstream-archive" repository: "recloudstream/cloudstream-archive"
token: ${{ steps.generate_archive_token.outputs.token }} token: ${{ steps.generate_archive_token.outputs.token }}
path: "archive" path: "archive"
- name: Move build - name: Move build
run: | run: cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
- name: Push archive - name: Push archive
run: | run: |

View file

@ -1,19 +1,18 @@
name: Dokka name: Dokka
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
concurrency:
group: "dokka"
cancel-in-progress: true
on: on:
push: push:
branches: branches: [ master ]
# choose your default branch
- master
- main
paths-ignore: paths-ignore:
- '*.md' - '*.md'
permissions:
contents: read
concurrency:
group: "dokka"
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -25,13 +24,14 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/dokka" repository: "recloudstream/dokka"
- name: Checkout - name: Checkout
uses: actions/checkout@master uses: actions/checkout@v6
with: with:
path: "src" path: "src"
- name: Checkout dokka - name: Checkout dokka
uses: actions/checkout@master uses: actions/checkout@v6
with: with:
repository: "recloudstream/dokka" repository: "recloudstream/dokka"
path: "dokka" path: "dokka"
@ -44,13 +44,15 @@ jobs:
rm -rf "./library" rm -rf "./library"
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
distribution: temurin
java-version: 17 java-version: 17
distribution: 'adopt'
- name: Setup Android SDK - name: Setup Gradle
uses: android-actions/setup-android@v3 uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Generate Dokka - name: Generate Dokka
run: | run: |
@ -59,8 +61,7 @@ jobs:
./gradlew docs:dokkaGeneratePublicationHtml ./gradlew docs:dokkaGeneratePublicationHtml
- name: Copy Dokka - name: Copy Dokka
run: | run: cp -r $GITHUB_WORKSPACE/src/docs/build/dokka/html/* $GITHUB_WORKSPACE/dokka/
cp -r $GITHUB_WORKSPACE/src/docs/build/dokka/html/* $GITHUB_WORKSPACE/dokka/
- name: Push builds - name: Push builds
run: | run: |

105
.github/workflows/instrumented-tests.yml vendored Normal file
View file

@ -0,0 +1,105 @@
name: Instrumented Tests
on:
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
jobs:
instrumented-tests:
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
github.event.comment.body == '/run-tests'
steps:
- name: Check permission
uses: actions/github-script@v9
with:
script: |
const login = context.payload.comment.user.login;
const association = context.payload.comment.author_association;
const allowed = ['OWNER', 'MEMBER'];
const isAllowed =
login === 'Luna712' ||
allowed.includes(association);
if (!isAllowed) {
core.setFailed(`User ${login} is not permitted to trigger this workflow.`);
}
- uses: actions/checkout@v6
with:
ref: refs/pull/${{ github.event.issue.number }}/head
- name: Post started comment
uses: actions/github-script@v9
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Instrumented tests are running. [View live progress](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
})
- name: Set up JDK 17
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 17
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache-read-only: false
- name: Get target SDK
id: sdk
run: |
TARGET_SDK=$(grep 'targetSdk' gradle/libs.versions.toml | grep -o '[0-9]\+' | head -1)
echo "version=$TARGET_SDK" >> $GITHUB_OUTPUT
- name: Enable KVM
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run Instrumented Tests
id: tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ steps.sdk.outputs.version }}
arch: x86_64
profile: Nexus 6
script: ./gradlew connectedPrereleaseDebugAndroidTest
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v7
with:
name: instrumented-test-results
path: '**/build/reports/androidTests/'
- name: Post finished comment
if: always()
uses: actions/github-script@v9
with:
script: |
const success = '${{ steps.tests.outcome }}' === 'success';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: success
? 'Instrumented tests passed. [View results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
: 'Instrumented tests failed. [View results](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})'
})

View file

@ -1,88 +0,0 @@
name: Issue automatic actions
on:
issues:
types: [opened]
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@v7
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@v4
- 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@v7
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

@ -12,6 +12,9 @@ concurrency:
group: "pre-release" group: "pre-release"
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: write
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -23,14 +26,18 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets" repository: "recloudstream/secrets"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: '17' distribution: temurin
distribution: 'adopt' java-version: 17
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Fetch keystore - name: Fetch keystore
id: fetch_keystore id: fetch_keystore
run: | run: |
@ -41,18 +48,27 @@ jobs:
KEY_PWD="$(cat keystore_password.txt)" KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}" echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Run Gradle - name: Run Gradle
run: | run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar
./gradlew assemblePrerelease build androidSourcesJar
./gradlew makeJar # for classes.jar, has to be done after assemblePrerelease
env: env:
SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }} SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }} SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
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 - name: Create pre-release
uses: "marvinpinto/action-automatic-releases@latest" uses: marvinpinto/action-automatic-releases@latest
with: with:
repo_token: "${{ secrets.GITHUB_TOKEN }}" repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "pre-release" automatic_release_tag: "pre-release"

View file

@ -2,22 +2,40 @@ name: Artifact Build
on: [pull_request] on: [pull_request]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: '17' distribution: temurin
distribution: 'adopt' java-version: 17
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
cache-read-only: false
- name: Ensure binary compatibility
# This is to ensure that your code is backwards compatible.
# If this fails you need to add a @Prerelease annotation to new code.
run: ./gradlew library:checkKotlinAbi
- name: Run Gradle - name: Run Gradle
run: ./gradlew assemblePrereleaseDebug run: ./gradlew assemblePrereleaseDebug lint check
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: pull-request-build name: pull-request-build
path: "app/build/outputs/apk/prerelease/debug/*.apk" path: "app/build/outputs/apk/prerelease/debug/*.apk"

View file

@ -1,17 +1,19 @@
name: Fix locale issues name: Fix locale issues
on: on:
workflow_dispatch:
push: push:
branches: [ master ]
paths: paths:
- '**.xml' - '**.xml'
branches: workflow_dispatch:
- master
concurrency: concurrency:
group: "locale" group: "locale"
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: read
jobs: jobs:
create: create:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -23,15 +25,17 @@ jobs:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream" repository: "recloudstream/cloudstream"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
- name: Install dependencies - name: Install dependencies
run: | run: pip3 install lxml requests
pip3 install lxml
- name: Edit files - name: Edit files
run: | run: python3 .github/locales.py
python3 .github/locales.py
- name: Commit to the repo - name: Commit to the repo
run: | run: |
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com" git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"

218
.gitignore vendored
View file

@ -1,5 +1,3 @@
*.iml
.gradle
/local.properties /local.properties
/.idea/caches /.idea/caches
/.idea/misc.xml /.idea/misc.xml
@ -11,6 +9,220 @@
.DS_Store .DS_Store
/build /build
/captures /captures
.externalNativeBuild
.cxx .cxx
.kotlin/*
# Created by https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=kotlin,java,android,androidstudio,visualstudiocode
### Android ###
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
### Android Patch ###
gen-external-apklibs
# Replacement of .externalNativeBuild directories introduced
# with Android Studio 3.5.
### Java ###
# Compiled class file
*.class
# Log file
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### Kotlin ###
# Compiled class file
# Log file
# BlueJ files
# Mobile Tools for Java (J2ME)
# Package Files #
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
### VisualStudioCode ###
.vscode/*
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### AndroidStudio ###
# Covers files to be ignored for android development using Android Studio.
# Built application files
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
# Generated files
bin/
gen/
out/
# Gradle files
.gradle
# Signing files
.signing/
# Local configuration file (sdk path, etc)
# Proguard folder generated by Eclipse
proguard/
# Log Files
# Android Studio
/*/build/
/*/local.properties
/*/out
/*/*/build
/*/*/production
.navigation/
*.ipr
*~
*.swp
# Keystore files
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Android Patch
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
# NDK
obj/
# IntelliJ IDEA
*.iws
/out/
# User-specific configurations
.idea/caches/
.idea/libraries/
.idea/shelf/
.idea/workspace.xml
.idea/tasks.xml
.idea/.name
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/encodings.xml
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/dictionaries
.idea/vcs.xml
.idea/jsLibraryMappings.xml
.idea/datasources.xml
.idea/dataSources.ids
.idea/sqlDataSources.xml
.idea/dynamic.xml
.idea/uiDesigner.xml
.idea/assetWizardSettings.xml
.idea/gradle.xml
.idea/jarRepositories.xml
.idea/navEditor.xml
# Legacy Eclipse project files
.classpath
.project
.cproject
.settings/
# Mobile Tools for Java (J2ME)
# Package Files #
# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
## Plugin-specific files:
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Mongo Explorer plugin
.idea/mongoSettings.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
### AndroidStudio Patch ###
!/gradle/wrapper/gradle-wrapper.jar
# End of https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,visualstudiocode

1
.idea/.name generated
View file

@ -1 +0,0 @@
CloudStream

View file

@ -1,123 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View file

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

7
.idea/discord.xml generated
View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

21
.idea/gradle.xml generated
View file

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/docs" />
<option value="$PROJECT_DIR$/library" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://github.com/psiegman/mvn-repo/raw/master/releases" />
</remote-repository>
<remote-repository>
<option name="id" value="maven2" />
<option name="name" value="maven2" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
</component>
</project>

6
.idea/studiobot.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedOut" />
</component>
</project>

6
.idea/vcs.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -1,6 +0,0 @@
{
"githubPullRequests.ignoredPullRequestBranches": [
"master"
],
"java.configuration.updateBuildConfiguration": "interactive"
}

11
AI-POLICY.md Normal file
View file

@ -0,0 +1,11 @@
# AI Policy
AI is a great tool. However, we want you to follow these rules regarding usage of AI in order to ensure the quality of both code and discussions.
1. Always state any AI usage in pull requests and issues.
2. Always test code before making a pull request. We do not want to test your AI generated code.
3. Listen to humans over computers. Contributors to CloudStream know this codebase better than an AI.
4. You should be able to explain and fix any code you submit. We do in-depth reviews and will reject low effort contributions.

21
COMPOSE.md Normal file
View file

@ -0,0 +1,21 @@
# Migration guide to Compose
### 1. MVI instead of MVVM
The current design of CloudStream loosely uses the MVVM architecture.
This means that the UI invokes the viewmodel with function calls, and it responds with LiveData fields that are observed. While this has worked, it generates a lot of boilerplate and has created some friction.
To make it easier to work with Compose, the new architecture will be based on MVI. In short this means that the viewmodel exposes a singular immutable class that is observed, and receives all UI events with a singular event that is a sealed class. All the UI should be able to be recreated based on this singular state class, and all interactions should be able to be replayed using only the event callback.
For a more detailed overview, see: https://www.youtube.com/watch?v=b2z1jvD4VMQ
This is part of the effort to make CloudStream cross platform, as it allows us to decouple UI and logic.
### 2. KMP-compatible libraries
We plan to leverage Kotlin's KMP project to compile our code to different architectures. However, this requires us to only use KMP-compatible libraries, no Java. Therefore any pull requests must ensure that they use KMP-compatible libraries only.
### 3. UI Changes
While migrating to the new compose UI, you also have the opportunity to change the UI. However, this should only be to freshen up the UI, not completely redesign it. It is also important to stress that this process should not lose any features of the old UI, and be very conservative with adding new features.

View file

@ -13,7 +13,7 @@
+ [Bugs Reports:](#bug_report) + [Bugs Reports:](#bug_report)
+ [Enhancement:](#enhancment) + [Enhancement:](#enhancment)
+ [Extension Development:](#extensions) + [Extension Development:](#extensions)
+ [Languauge Support:](#languages) + [Language Support:](#languages)
+ [Further Sources](#contact_and_sources) + [Further Sources](#contact_and_sources)

View file

@ -1,53 +1,96 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform
import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins { plugins {
id("com.android.application") alias(libs.plugins.android.application)
id("kotlin-android") alias(libs.plugins.dokka)
id("org.jetbrains.dokka") alias(libs.plugins.kotlin.serialization)
} }
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
fun getGitCommitHash(): String { abstract class GenerateGitHashTask : DefaultTask() {
return try {
val headFile = file("${project.rootDir}/.git/HEAD")
@get:InputFile
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val headFile: RegularFileProperty
@get:InputDirectory
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val headsDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val head = headFile.get().asFile
val hash = try {
if (head.exists()) {
// Read the commit hash from .git/HEAD // Read the commit hash from .git/HEAD
if (headFile.exists()) { val headContent = head.readText().trim()
val headContent = headFile.readText().trim()
if (headContent.startsWith("ref:")) { if (headContent.startsWith("ref:")) {
val refPath = headContent.substring(5) // e.g., refs/heads/main val refPath = headContent.substring(5) // e.g., refs/heads/main
val commitFile = file("${project.rootDir}/.git/$refPath") val commitFile = File(head.parentFile, refPath)
if (commitFile.exists()) commitFile.readText().trim() else "" if (commitFile.exists()) commitFile.readText().trim() else ""
} else headContent // If it's a detached HEAD (commit hash directly) } else headContent // If it's a detached HEAD (commit hash directly)
} else { } else "" // If .git/HEAD doesn't exist
"" // If .git/HEAD doesn't exist
}.take(7) // Return the short commit hash
} catch (_: Throwable) { } catch (_: Throwable) {
"" // Just return an empty string if any exception occurs "" // Just set to an empty string if any exception occurs
}.take(7) // Get the short commit hash
val outFile = outputDir.file("git-hash.txt").get().asFile
outFile.parentFile.mkdirs()
outFile.writeText(hash)
} }
} }
val generateGitHash = tasks.register<GenerateGitHashTask>("generateGitHash") {
val gitDir = layout.projectDirectory.dir("../.git")
headFile.set(gitDir.file("HEAD"))
headsDir.set(gitDir.dir("refs/heads"))
outputDir.set(layout.buildDirectory.dir("generated/git"))
}
android { android {
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
testOptions { testOptions {
unitTests.isReturnDefaultValues = true unitTests.isReturnDefaultValues = true
} }
viewBinding { // Looks like google likes to add metadata only they can read https://gitlab.com/IzzyOnDroid/repo/-/work_items/491
enable = true dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
androidComponents {
onVariants { variant ->
variant.sources.assets?.addGeneratedSourceDirectory(
generateGitHash,
GenerateGitHashTask::outputDir
)
}
} }
signingConfigs { signingConfigs {
if (prereleaseStoreFile != null) { // We just use SIGNING_KEY_ALIAS here since it won't change
// so won't kill the configuration cache.
if (System.getenv("SIGNING_KEY_ALIAS") != null) {
create("prerelease") { create("prerelease") {
storeFile = file(prereleaseStoreFile) val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
storeFile = prereleaseStoreFile?.let { file(it) }
storePassword = System.getenv("SIGNING_STORE_PASSWORD") storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD") keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
@ -61,12 +104,10 @@ android {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = libs.versions.minSdk.get().toInt() minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 65 versionCode = libs.versions.versionCode.get().toInt()
versionName = "4.5.1" versionName = libs.versions.versionName.get()
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
resValue("string", "commit_hash", getGitCommitHash())
resValue("bool", "is_prerelease", "false")
// Reads local.properties // Reads local.properties
val localProperties = gradleLocalProperties(rootDir, project.providers) val localProperties = gradleLocalProperties(rootDir, project.providers)
@ -86,6 +127,16 @@ android {
"SIMKL_CLIENT_SECRET", "SIMKL_CLIENT_SECRET",
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.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" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@ -113,12 +164,9 @@ android {
productFlavors { productFlavors {
create("stable") { create("stable") {
dimension = "state" dimension = "state"
resValue("bool", "is_prerelease", "false")
} }
create("prerelease") { create("prerelease") {
dimension = "state" dimension = "state"
resValue("bool", "is_prerelease", "true")
buildConfigField("boolean", "BETA", "true")
applicationIdSuffix = ".prerelease" applicationIdSuffix = ".prerelease"
if (signingConfigs.names.contains("prerelease")) { if (signingConfigs.names.contains("prerelease")) {
signingConfig = signingConfigs.getByName("prerelease") signingConfig = signingConfigs.getByName("prerelease")
@ -136,13 +184,29 @@ android {
targetCompatibility = JavaVersion.toVersion(javaTarget.target) targetCompatibility = JavaVersion.toVersion(javaTarget.target)
} }
java {
// Use Java 17 toolchain even if a higher JDK runs the build.
// We still use Java 8 for now which higher JDKs have deprecated.
toolchain {
languageVersion.set(JavaLanguageVersion.of(libs.versions.jdkToolchain.get()))
}
}
lint { lint {
abortOnError = false
checkReleaseBuilds = false checkReleaseBuilds = false
} }
buildFeatures { buildFeatures {
buildConfig = true buildConfig = true
viewBinding = true
}
packaging {
jniLibs {
// Enables legacy JNI packaging to reduce APK size (similar to builds before minSdk 23).
// Note: This may increase app startup time slightly.
useLegacyPackaging = true
}
} }
namespace = "com.lagradost.cloudstream3" namespace = "com.lagradost.cloudstream3"
@ -153,44 +217,46 @@ dependencies {
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.json) testImplementation(libs.json)
androidTestImplementation(libs.core) androidTestImplementation(libs.core)
implementation(libs.junit.ktx)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core) androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.instancio.core)
androidTestImplementation(libs.junit.ktx)
androidTestImplementation(libs.kotlin.test)
// Android Core & Lifecycle // Android Core & Lifecycle
implementation(libs.core.ktx) implementation(libs.core.ktx)
implementation(libs.activity.ktx)
implementation(libs.annotation)
implementation(libs.appcompat) implementation(libs.appcompat)
implementation(libs.navigation.ui.ktx) implementation(libs.fragment.ktx)
implementation(libs.lifecycle.livedata.ktx) implementation(libs.bundles.lifecycle)
implementation(libs.lifecycle.viewmodel.ktx) implementation(libs.bundles.navigation)
implementation(libs.navigation.fragment.ktx) implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.serialization.json) // JSON Parser
// Design & UI // Design & UI
implementation(libs.preference.ktx) implementation(libs.preference.ktx)
implementation(libs.material) implementation(libs.material)
implementation(libs.constraintlayout) implementation(libs.constraintlayout)
implementation(libs.swiperefreshlayout)
// Coil Image Loading // Coil Image Loading
implementation(libs.coil) implementation(libs.bundles.coil)
implementation(libs.coil.network.okhttp)
// Media 3 (ExoPlayer) // Media 3 (ExoPlayer)
implementation(libs.bundles.media3) implementation(libs.bundles.media3)
implementation(libs.video) implementation(libs.video)
// FFmpeg Decoding
implementation(libs.bundles.nextlib)
// Anime-db for filler
implementation(libs.anime.db)
// PlayBack // PlayBack
implementation(libs.colorpicker) // Subtitle Color Picker implementation(libs.colorpicker) // Subtitle Color Picker
implementation(libs.newpipeextractor) // For Trailers implementation(libs.newpipeextractor) // For Trailers
implementation(libs.juniversalchardet) // Subtitle Decoding implementation(libs.juniversalchardet) // Subtitle Decoding
// FFmpeg Decoding
implementation(libs.bundles.nextlibMedia3)
// Crash Reports (AcraApplication.kt)
implementation(libs.acra.core)
implementation(libs.acra.toast)
// UI Stuff // UI Stuff
implementation(libs.shimmer) // Shimmering Effect (Loading Skeleton) implementation(libs.shimmer) // Shimmering Effect (Loading Skeleton)
implementation(libs.palette.ktx) // Palette for Images -> Colors implementation(libs.palette.ktx) // Palette for Images -> Colors
@ -201,50 +267,37 @@ dependencies {
implementation(libs.qrcode.kotlin) // QR Code for PIN Auth on TV implementation(libs.qrcode.kotlin) // QR Code for PIN Auth on TV
// Extensions & Other Libs // Extensions & Other Libs
implementation(libs.jsoup) // HTML Parser
implementation(libs.ksoup) // HTML Parser
implementation(libs.rhino) // Run JavaScript implementation(libs.rhino) // Run JavaScript
implementation(libs.quickjs)
implementation(libs.fuzzywuzzy) // Library/Ext Searching with Levenshtein Distance
implementation(libs.safefile) // To Prevent the URI File Fu*kery implementation(libs.safefile) // To Prevent the URI File Fu*kery
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
implementation(libs.conscrypt.android) { implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9
version { implementation(libs.jackson.module.kotlin) // JSON Parser
strictly("2.5.2") implementation(libs.zipline)
}
because("2.5.3 crashes everything for everyone.") // Temp/deprecated; will be removed once extensions have time to migrate from using it
} // To Fix SSL Fu*kery on Android 9 implementation("com.google.code.gson:gson:2.11.0")
implementation(libs.jackson.module.kotlin) { // Deprecated; will be removed once extensions have time to migrate from using it
version { implementation("me.xdrop:fuzzywuzzy:1.4.0")
strictly("2.13.1")
}
because("Don't Bump Jackson above 2.13.1, Crashes on Android TV's and FireSticks that have Min API Level 25 or Less.")
} // JSON Parser
// Torrent Support // Torrent Support
// implementation(libs.torrentserver) implementation(libs.torrentserver)
// Downloading & Networking // Downloading & Networking
implementation(libs.work.runtime)
implementation(libs.work.runtime.ktx) implementation(libs.work.runtime.ktx)
implementation(libs.nicehttp) // HTTP Lib implementation(libs.nicehttp) // HTTP Lib
implementation(project(":library") { implementation(project(":library"))
// There does not seem to be a good way of getting the android flavor.
val isDebug = gradle.startParameter.taskRequests.any { task ->
task.args.any { arg ->
arg.contains("debug", true)
}
}
this.extra.set("isDebug", isDebug)
})
} }
tasks.register<Jar>("androidSourcesJar") { tasks.register<Jar>("androidSourcesJar") {
archiveClassifier.set("sources") archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) // Full Sources from(android.sourceSets.getByName("main").java.directories) // Full Sources
} }
tasks.register<Copy>("copyJar") { tasks.register<Copy>("copyJar") {
dependsOn("build", ":library:jvmJar")
from( from(
"build/intermediates/compile_app_classes_jar/prereleaseDebug/bundlePrereleaseDebugClassesToCompileJar", "build/intermediates/compile_app_classes_jar/prereleaseDebug/bundlePrereleaseDebugClassesToCompileJar",
"../library/build/libs" "../library/build/libs"
@ -271,15 +324,21 @@ tasks.register<Jar>("makeJar") {
tasks.withType<KotlinJvmCompile> { tasks.withType<KotlinJvmCompile> {
compilerOptions { compilerOptions {
jvmTarget.set(javaTarget) jvmTarget.set(javaTarget)
freeCompilerArgs.add("-Xjvm-default=all-compatibility") jvmDefault.set(JvmDefaultMode.ENABLE)
optIn.addAll(
"com.lagradost.cloudstream3.InternalAPI",
"com.lagradost.cloudstream3.Prerelease",
)
} }
} }
dokka { dokka {
moduleName = "App" moduleName = "App"
dokkaSourceSets { dokkaSourceSets {
main { configureEach {
suppress = name != "prereleaseDebug"
analysisPlatform = KotlinPlatform.JVM analysisPlatform = KotlinPlatform.JVM
displayName = "JVM"
documentedVisibilities( documentedVisibilities(
VisibilityModifier.Public, VisibilityModifier.Public,
VisibilityModifier.Protected VisibilityModifier.Protected

13
app/lint.xml Normal file
View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<!-- ByteOrderMark has errors in values-b+ja/strings.xml, but it's handled by weblate so we don't really care. -->
<issue id="ByteOrderMark" severity="ignore" />
<!-- We don't care about MissingTranslation since it's handled by weblate. -->
<issue id="MissingTranslation" severity="ignore" />
<!-- We only care about the source language here. -->
<issue id="StringFormatInvalid">
<ignore path="**/res/values-*/**" />
</issue>
</lint>

View file

@ -7,6 +7,7 @@ import android.view.LayoutInflater
import androidx.test.core.app.ActivityScenario import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
@ -54,12 +55,6 @@ class ExampleInstrumentedTest {
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView } return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
} }
@Test
fun providersExist() {
Assert.assertTrue(getAllProviders().isNotEmpty())
println("Done providersExist")
}
@Throws @Throws
private inline fun <reified T : ViewBinding> testAllLayouts( private inline fun <reified T : ViewBinding> testAllLayouts(
activity: Activity, activity: Activity,
@ -88,6 +83,8 @@ class ExampleInstrumentedTest {
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv) // testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv) //testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv)
testAllLayouts<BottomResultviewPreviewBinding>(activity, R.layout.bottom_resultview_preview,R.layout.bottom_resultview_preview_tv)
testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv) testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv) testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
@ -133,14 +130,14 @@ class ExampleInstrumentedTest {
@Test @Test
@Throws(AssertionError::class) @Throws(AssertionError::class)
fun providerCorrectData() { fun providerCorrectData() {
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 } val langTagsIETF = SubtitleHelper.languages.map { it.IETF_tag }
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty()) Assert.assertFalse("IETFTagNames does not contain any languages", langTagsIETF.isNullOrEmpty())
for (api in getAllProviders()) { for (api in getAllProviders()) {
Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE") Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE")
Assert.assertTrue("Api does not contain a name", api.name != "NONE") Assert.assertTrue("Api does not contain a name", api.name != "NONE")
Assert.assertTrue( Assert.assertTrue(
"Api ${api.name} does not contain a valid language code", "Api ${api.name} does not contain a valid language code",
isoNames.contains(api.lang) langTagsIETF.contains(api.lang)
) )
Assert.assertTrue( Assert.assertTrue(
"Api ${api.name} does not contain any supported types", "Api ${api.name} does not contain any supported types",

View file

@ -0,0 +1,154 @@
package com.lagradost.cloudstream3
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.lagradost.cloudstream3.SkipSerializationTest
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}")
val failures = mutableListOf<String>()
serializableClasses.forEach { kClass ->
runCatching {
val instance = Instancio.of(kClass.java).withMaxDepth(10).create()
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}")
}.onFailure { e ->
failures.add("FAILED ${kClass.qualifiedName}: ${e.message}")
}
}
if (failures.isNotEmpty()) {
throw AssertionError("${failures.size} class(es) failed:\n${failures.joinToString("\n")}")
}
}
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
@Test
fun isIdenticalDeserialization() {
val serializableClasses = findSerializableClasses("com.lagradost")
println("Number of serializable classes: ${serializableClasses.size}")
val failures = mutableListOf<String>()
serializableClasses.forEach { kClass ->
runCatching {
val instance = Instancio.of(kClass.java).withMaxDepth(10).create()
// 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}")
}.onFailure { e ->
failures.add("FAILED ${kClass.qualifiedName}: ${e.message}")
}
}
if (failures.isNotEmpty()) {
throw AssertionError("${failures.size} class(es) failed:\n${failures.joinToString("\n")}")
}
}
// 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 }
&& kClass.java.annotations.none { it is SkipSerializationTest }
&& !kClass.isAbstract
}
}
@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,41 @@
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.SerialName
import kotlinx.serialization.Serializable
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@Serializable
data class UriData(
@Serializable(with = UriSerializer::class)
@SerialName("uri") val uri: Uri = Uri.EMPTY,
)
class UriSerializerTest {
@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

@ -16,12 +16,53 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" /> <uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- We can use this directly as CS3 is not on Play Store --> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- We can use this directly as CS3 is not on Play Store -->
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> <!-- We can use to read the tv channel list -->
<!-- Required for OpenInAppAction and getting arbitrary Aniyomi packages --> <!-- Required for OpenInAppAction and getting arbitrary Aniyomi packages -->
<uses-permission <uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<queries>
<!--
QUERY_ALL_PACKAGES does not work on some devices running Android 11+ (like Google TV 14),
so we must explicitly specify the packages and intent patterns we query to ensure visibility.
-->
<!-- For external video players -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="video/*" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/x-mpegURL" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/vnd.apple.mpegurl" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="magnet" />
</intent>
<!-- Common players supported in actions/temp -->
<package android:name="org.videolan.vlc" />
<package android:name="org.videolan.vlc.debug" />
<package android:name="is.xyz.mpv" />
<package android:name="is.xyz.mpv.ytdl" />
<package android:name="app.marlboroadvance.mpvex" />
<package android:name="live.mehiz.mpvkt" />
<package android:name="live.mehiz.mpvkt.preview" />
<package android:name="com.brouken.player" />
<package android:name="dev.anilbeesetti.nextplayer" />
<package android:name="com.instantbits.cast.webvideo" />
<package android:name="com.gianlu.aria2android" />
<!-- Torrent clients -->
<package android:name="org.proninyaroslav.libretorrent" />
<package android:name="com.biglybt.android.client" />
</queries>
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
@ -33,9 +74,8 @@
<!-- Without the large heap Exoplayer buffering gets reset due to OOM. --> <!-- Without the large heap Exoplayer buffering gets reset due to OOM. -->
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android--> <!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
<application <application
android:name=".AcraApplication" android:name=".CloudStreamApp"
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:appCategory="video" android:appCategory="video"
android:banner="@mipmap/ic_banner" android:banner="@mipmap/ic_banner"
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
@ -43,11 +83,12 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:pageSizeCompat="enabled"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="35"> tools:targetApi="${target_sdk_version}">
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
@ -108,14 +149,31 @@
android:launchMode="singleTask" android:launchMode="singleTask"
is a bit experimental, it makes loading repositories from browser still stay on the same page is a bit experimental, it makes loading repositories from browser still stay on the same page
no idea about side effects no idea about side effects
Not exported to prevent bypassing the AccountSelectActivity
--> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode"
android:exported="true" android:exported="false"
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true"> android:supportsPictureInPicture="true" />
<activity
android:name=".ui.account.AccountSelectActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune --> <!-- cloudstreamplayer://encodedUrl?name=Dune -->
<intent-filter> <intent-filter>
@ -142,7 +200,14 @@
<data android:scheme="cloudstreamrepo" /> <data android:scheme="cloudstreamrepo" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="csshare" />
</intent-filter>
<!-- Allow searching with intents: cloudstreamsearch://Your%20Name --> <!-- Allow searching with intents: cloudstreamsearch://Your%20Name -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -166,7 +231,7 @@
<data android:scheme="cloudstreamcontinuewatching" /> <data android:scheme="cloudstreamcontinuewatching" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="false">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -179,25 +244,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.account.AccountSelectActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="true">
<intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ui.EasterEggMonke"
android:exported="true" />
<receiver <receiver
android:name=".receivers.VideoDownloadRestartReceiver" android:name=".receivers.VideoDownloadRestartReceiver"
android:enabled="false" android:enabled="false"
@ -213,6 +259,12 @@
android:foregroundServiceType="dataSync" android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<service
android:name=".services.DownloadQueueService"
android:enabled="true"
android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- Necessary for WorkManager services: https://stackoverflow.com/a/77186316 --> <!-- Necessary for WorkManager services: https://stackoverflow.com/a/77186316 -->
<service <service
android:name="androidx.work.impl.foreground.SystemForegroundService" android:name="androidx.work.impl.foreground.SystemForegroundService"

View file

@ -1,28 +0,0 @@
#include <jni.h>
#include <csignal>
#include <android/log.h>
#define TAG "CloudStream Crash Handler"
volatile sig_atomic_t gSignalStatus = 0;
void handleNativeCrash(int signal) {
gSignalStatus = signal;
}
extern "C" JNIEXPORT void JNICALL
Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) {
#define REGISTER_SIGNAL(X) signal(X, handleNativeCrash);
REGISTER_SIGNAL(SIGSEGV)
#undef REGISTER_SIGNAL
}
//extern "C" JNIEXPORT void JNICALL
//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) {
// int *p = nullptr;
// *p = 0;
//}
extern "C" JNIEXPORT int JNICALL
Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) {
//__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus);
return gSignalStatus;
}

View file

@ -1,233 +1,78 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import coil3.PlatformContext
import coil3.SingletonImageLoader
import com.lagradost.api.setContext
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.ImageLoader
import kotlinx.coroutines.runBlocking
import org.acra.ACRA
import org.acra.ReportField
import org.acra.config.CoreConfiguration
import org.acra.data.CrashReportData
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
import org.acra.sender.ReportSender
import org.acra.sender.ReportSenderFactory
import java.io.File
import java.io.FileNotFoundException
import java.io.PrintStream
import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.system.exitProcess
class CustomReportSender : ReportSender {
// Sends all your crashes to google forms
override fun send(context: Context, errorContent: CrashReportData) {
/*println("Sending report")
val url =
"https://docs.google.com/forms/d/e/$id/formResponse"
val data = mapOf(
"entry.$entry" to errorContent.toJSON()
)
thread { // to not run it on main thread
runBlocking {
suspendSafeApiCall {
app.post(url, data = data)
//println("Report response: $post")
}
}
}
runOnMainThread { // to run it on main looper
normalSafeApiCall {
Toast.makeText(context, R.string.acra_report_toast, Toast.LENGTH_SHORT).show()
}
}*/
}
}
class CustomSenderFactory : ReportSenderFactory {
override fun create(context: Context, config: CoreConfiguration): ReportSender {
return CustomReportSender()
}
override fun enabled(config: CoreConfiguration): Boolean {
return true
}
}
class ExceptionHandler(val errorFile: File, val onError: (() -> Unit)) :
Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, error: Throwable) {
ACRA.errorReporter.handleException(error)
try {
PrintStream(errorFile).use { ps ->
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
ps.println("Fatal exception on thread ${thread.name} (${thread.id})")
error.printStackTrace(ps)
}
} catch (ignored: FileNotFoundException) {
}
try {
onError.invoke()
} catch (ignored: Exception) {
}
exitProcess(1)
}
}
class AcraApplication : Application(), SingletonImageLoader.Factory {
override fun onCreate() {
super.onCreate()
// if we want to initialise coil at earliest
// (maybe when loading an image or gif using in splash screen activity)
//ImageLoader.buildImageLoader(applicationContext)
ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
}.also {
exceptionHandler = it
Thread.setDefaultUncaughtExceptionHandler(it)
}
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
context = base
initAcra {
//core configuration:
buildConfigClass = BuildConfig::class.java
reportFormat = StringFormat.JSON
reportContent = listOf(
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
ReportField.STACK_TRACE,
)
// removed this due to bug when starting the app, moved it to when it actually crashes
//each plugin you chose above can be configured in a block like this:
/*toast {
text = getString(R.string.acra_report_toast)
//opening this block automatically enables the plugin.
}*/
}
}
override fun newImageLoader(context: PlatformContext): coil3.ImageLoader {
// Coil Module will be initialized & setSafe globally when first loadImage() is invoked
return ImageLoader.buildImageLoader(applicationContext)
}
companion object {
var exceptionHandler: ExceptionHandler? = null
/** Use to get activity from Context */
tailrec fun Context.getActivity(): Activity? {
return when (this) {
is Activity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
}
private var _context: WeakReference<Context>? = null
var context
get() = _context?.get()
private set(value) {
_context = WeakReference(value)
setContext(WeakReference(value))
}
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
return context?.getKey(path, valueType)
}
fun <T : Any> setKeyClass(path: String, value: T) {
context?.setKey(path, value)
}
fun removeKeys(folder: String): Int? {
return context?.removeKeys(folder)
}
fun <T> setKey(path: String, value: T) {
context?.setKey(path, value)
}
fun <T> setKey(folder: String, path: String, value: T) {
context?.setKey(folder, path, value)
}
inline fun <reified T : Any> getKey(path: String, defVal: T?): T? {
return context?.getKey(path, defVal)
}
inline fun <reified T : Any> getKey(path: String): T? {
return context?.getKey(path)
}
inline fun <reified T : Any> getKey(folder: String, path: String): T? {
return context?.getKey(folder, path)
}
inline fun <reified T : Any> getKey(folder: String, path: String, defVal: T?): T? {
return context?.getKey(folder, path, defVal)
}
fun getKeys(folder: String): List<String>? {
return context?.getKeys(folder)
}
fun removeKey(folder: String, path: String) {
context?.removeKey(folder, path)
}
fun removeKey(path: String) {
context?.removeKey(path)
}
/** /**
* If fallbackWebview is true and a fragment is supplied then it will open a webview with the url if the browser fails. * Deprecated alias for CloudStreamApp for backwards compatibility with plugins.
* */ * Use CloudStreamApp instead.
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) { */
context?.openBrowser(url, fallbackWebview, fragment) @Deprecated(
} message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp"),
/** Will fallback to webview if in TV layout */ level = DeprecationLevel.ERROR
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
isLayout(TV or EMULATOR),
activity?.supportFragmentManager?.fragments?.lastOrNull()
) )
} class AcraApplication {
companion object {
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.context"),
level = DeprecationLevel.ERROR
)
val context get() = CloudStreamApp.context
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.removeKeys(folder)"),
level = DeprecationLevel.ERROR
)
fun removeKeys(folder: String): Int? =
CloudStreamApp.removeKeys(folder)
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(path, value)"),
level = DeprecationLevel.ERROR
)
fun <T> setKey(path: String, value: T) =
CloudStreamApp.setKey(path, value)
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(folder, path, value)"),
level = DeprecationLevel.ERROR
)
fun <T> setKey(folder: String, path: String, value: T) =
CloudStreamApp.setKey(folder, path, value)
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path, defVal)"),
level = DeprecationLevel.ERROR
)
inline fun <reified T : Any> getKey(path: String, defVal: T?): T? =
CloudStreamApp.getKey(path, defVal)
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(path)"),
level = DeprecationLevel.ERROR
)
inline fun <reified T : Any> getKey(path: String): T? =
CloudStreamApp.getKey(path)
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path)"),
level = DeprecationLevel.ERROR
)
inline fun <reified T : Any> getKey(folder: String, path: String): T? =
CloudStreamApp.getKey(folder, path)
@Deprecated(
message = "AcraApplication is deprecated, use CloudStreamApp instead",
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.getKey(folder, path, defVal)"),
level = DeprecationLevel.ERROR
)
inline fun <reified T : Any> getKey(folder: String, path: String, defVal: T?): T? =
CloudStreamApp.getKey(folder, path, defVal)
} }
} }

View file

@ -0,0 +1,181 @@
package com.lagradost.cloudstream3
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.os.Build
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.SingletonImageLoader
import com.lagradost.api.setContext
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.mvvm.safeAsync
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppContextUtils.openBrowser
import com.lagradost.cloudstream3.utils.AppDebug
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.removeKeys
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.ImageLoader.buildImageLoader
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.FileNotFoundException
import java.io.PrintStream
import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.concurrent.thread
import kotlin.system.exitProcess
class ExceptionHandler(
val errorFile: File,
val onError: (() -> Unit)
) : Thread.UncaughtExceptionHandler {
override fun uncaughtException(thread: Thread, error: Throwable) {
try {
val threadId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
thread.threadId()
} else {
@Suppress("DEPRECATION")
thread.id
}
PrintStream(errorFile).use { ps ->
ps.println("Currently loading extension: ${PluginManager.currentlyLoading ?: "none"}")
ps.println("Fatal exception on thread ${thread.name} ($threadId)")
error.printStackTrace(ps)
}
} catch (_: FileNotFoundException) {
}
try {
onError()
} catch (_: Exception) {
}
exitProcess(1)
}
}
class CloudStreamApp : Application(), SingletonImageLoader.Factory {
override fun onCreate() {
super.onCreate()
// If we want to initialize Coil as early as possible, maybe when
// loading an image or GIF in a splash screen activity.
// buildImageLoader(applicationContext)
ExceptionHandler(filesDir.resolve("last_error")) {
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
startActivity(Intent.makeRestartActivityTask(intent!!.component))
}.also {
exceptionHandler = it
Thread.setDefaultUncaughtExceptionHandler(it)
}
AppDebug.isDebug = BuildConfig.DEBUG
}
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
context = base
}
override fun newImageLoader(context: PlatformContext): ImageLoader {
// Coil module will be initialized globally when first loadImage() is invoked.
return buildImageLoader(applicationContext)
}
companion object {
var exceptionHandler: ExceptionHandler? = null
/** Use to get Activity from Context. */
tailrec fun Context.getActivity(): Activity? {
return when (this) {
is Activity -> this
is ContextWrapper -> baseContext.getActivity()
else -> null
}
}
private var _context: WeakReference<Context>? = null
var context
get() = _context?.get()
private set(value) {
_context = WeakReference(value)
setContext(value)
}
fun <T : Any> getKeyClass(path: String, valueType: Class<T>): T? {
return context?.getKey(path, valueType)
}
fun <T : Any> setKeyClass(path: String, value: T) {
context?.setKey(path, value)
}
fun removeKeys(folder: String): Int? {
return context?.removeKeys(folder)
}
fun <T> setKey(path: String, value: T) {
context?.setKey(path, value)
}
fun <T> setKey(folder: String, path: String, value: T) {
context?.setKey(folder, path, value)
}
inline fun <reified T : Any> getKey(path: String, defVal: T?): T? {
return context?.getKey(path, defVal)
}
inline fun <reified T : Any> getKey(path: String): T? {
return context?.getKey(path)
}
inline fun <reified T : Any> getKey(folder: String, path: String): T? {
return context?.getKey(folder, path)
}
inline fun <reified T : Any> getKey(folder: String, path: String, defVal: T?): T? {
return context?.getKey(folder, path, defVal)
}
fun getKeys(folder: String): List<String>? {
return context?.getKeys(folder)
}
fun removeKey(folder: String, path: String) {
context?.removeKey(folder, path)
}
fun removeKey(path: String) {
context?.removeKey(path)
}
/** If fallbackWebView is true and a fragment is supplied then it will open a WebView with the URL if the browser fails. */
fun openBrowser(url: String, fallbackWebView: Boolean = false, fragment: Fragment? = null) {
context?.openBrowser(url, fallbackWebView, fragment)
}
/** Will fall back to WebView if in TV or emulator layout. */
fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser(
url,
isLayout(TV or EMULATOR),
activity?.supportFragmentManager?.fragments?.lastOrNull()
)
}
}
}

View file

@ -1,13 +1,16 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.Manifest import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.Manifest
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
@ -24,32 +27,41 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isNotEmpty
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.CastSession
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.actions.OpenInAppAction import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.player.PlayerPipHelper.isPIPPossible
import com.lagradost.cloudstream3.ui.player.Torrent import com.lagradost.cloudstream3.ui.player.Torrent
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.ui.result.ActorAdaptor
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter
import com.lagradost.cloudstream3.ui.result.ImageAdapter
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe import com.lagradost.cloudstream3.utils.UiText
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import org.schabi.newpipe.extractor.NewPipe
enum class FocusDirection { enum class FocusDirection {
Start, Start,
@ -89,17 +101,24 @@ object CommonActivity {
get() { get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels) return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
} }
val screenWidthWithOrientation: Int
get() {
return displayMetrics.widthPixels
}
val screenHeightWithOrientation: Int
get() {
return displayMetrics.heightPixels
}
var isPipDesired: Boolean = false
var canEnterPipMode: Boolean = false
var canShowPipMode: Boolean = false
var isInPIPMode: Boolean = false var isInPIPMode: Boolean = false
val onColorSelectedEvent = Event<Pair<Int, Int>>() val onColorSelectedEvent = Event<Pair<Int, Int>>()
val onDialogDismissedEvent = Event<Int>() val onDialogDismissedEvent = Event<Int>()
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
var appliedTheme: Int = 0
var appliedColor: Int = 0
private var currentToast: Toast? = null private var currentToast: Toast? = null
@ -167,27 +186,40 @@ object CommonActivity {
toast.duration = duration ?: Toast.LENGTH_SHORT toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
toast.view = binding.root // FIXME Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. toast.view =
binding.root // FIXME Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
currentToast = toast currentToast = toast
toast.show() toast.show()
val handler = Handler(Looper.getMainLooper())
val ref = WeakReference(toast)
/* Clean up activity leak */
handler.postDelayed({
if (ref.get() == currentToast) {
currentToast = null
}
}, 10_000)
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
} }
/** /**
* Not all languages can be fetched from locale with a code. * Set locale
* This map allows sidestepping the default Locale(languageCode) * @param languageTag shall a IETF BCP 47 conformant tag.
* when setting the app language. * Check [com.lagradost.cloudstream3.utils.SubtitleHelper].
**/ *
val appLanguageExceptions = hashMapOf( * See locales on:
"zh-rTW" to Locale.TRADITIONAL_CHINESE * https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-core/availableLocales.json
) * https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
* https://android.googlesource.com/platform/frameworks/base/+/android-16.0.0_r2/core/res/res/values/locale_config.xml
fun setLocale(context: Context?, languageCode: String?) { * https://iso639-3.sil.org/code_tables/639/data/all
if (context == null || languageCode == null) return */
val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode) fun setLocale(context: Context?, languageTag: String?) {
if (context == null || languageTag == null) return
val locale = Locale.forLanguageTag(languageTag)
val resources: Resources = context.resources val resources: Resources = context.resources
val config = resources.configuration val config = resources.configuration
Locale.setDefault(locale) Locale.setDefault(locale)
@ -195,8 +227,12 @@ object CommonActivity {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
context.createConfigurationContext(config) context.createConfigurationContext(config)
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics) // FIXME this should be replaced resources.updateConfiguration(
config,
resources.displayMetrics
) // FIXME this should be replaced
} }
fun Context.updateLocale() { fun Context.updateLocale() {
@ -208,28 +244,24 @@ object CommonActivity {
fun init(act: Activity) { fun init(act: Activity) {
setActivityInstance(act) setActivityInstance(act)
ioSafe { Torrent.deleteAllFiles() } ioSafe { Torrent.deleteAllFiles() }
val componentActivity = activity as? ComponentActivity ?: return val componentActivity = activity as? ComponentActivity ?: return
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
componentActivity.updateLocale() componentActivity.updateLocale()
componentActivity.updateTv() componentActivity.updateTv()
AccountManager.initMainAPI()
NewPipe.init(DownloaderTestImpl.getInstance()) NewPipe.init(DownloaderTestImpl.getInstance())
MainActivity.activityResultLauncher = componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> MainActivity.activityResultLauncher =
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == AppCompatActivity.RESULT_OK) { if (result.resultCode == AppCompatActivity.RESULT_OK) {
val actionUid = getKey<String>("last_click_action") ?: return@registerForActivityResult val actionUid =
getKey<String>("last_click_action") ?: return@registerForActivityResult
Log.d(TAG, "Loading action $actionUid result handler") Log.d(TAG, "Loading action $actionUid result handler")
val action = VideoClickActionHolder.getByUniqueId(actionUid) as? OpenInAppAction ?: return@registerForActivityResult val action = VideoClickActionHolder.getByUniqueId(actionUid) as? OpenInAppAction
?: return@registerForActivityResult
action.onResultSafe(act, result.data) action.onResultSafe(act, result.data)
removeKey("last_click_action") removeKey("last_click_action")
removeKey("last_opened_id") removeKey("last_opened")
} }
} }
@ -251,13 +283,15 @@ object CommonActivity {
} }
} }
/** Enters pip mode if it is both possible and desired to do so*/
private fun Activity.enterPIPMode() { private fun Activity.enterPIPMode() {
if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return if (!isPipDesired || !this.isPIPPossible()) return
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try { try {
enterPictureInPictureMode(PictureInPictureParams.Builder().build()) enterPictureInPictureMode(PictureInPictureParams.Builder().build())
} catch (e: Exception) { } catch (_: Exception) {
// Use fallback just in case // Use fallback just in case
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
enterPictureInPictureMode() enterPictureInPictureMode()
@ -273,17 +307,18 @@ object CommonActivity {
} }
} }
fun onUserLeaveHint(act: Activity?) { fun onUserLeaveHint(act: Activity) {
if (canEnterPipMode && canShowPipMode) { // On Android 12 and later we use setAutoEnterEnabled() instead.
act?.enterPIPMode() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) return
} act.enterPIPMode()
} }
fun updateTheme(act: Activity) { fun updateTheme(act: Activity) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act) val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
if (settingsManager if (settingsManager
.getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System" .getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
) {
loadThemes(act) loadThemes(act)
} }
} }
@ -315,6 +350,10 @@ object CommonActivity {
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.MonetMode else R.style.AppTheme R.style.MonetMode else R.style.AppTheme
"Dracula" -> R.style.DraculaMode
"Lavender" -> R.style.LavenderMode
"SilentBlue" -> R.style.SilentBlueMode
else -> R.style.AppTheme else -> R.style.AppTheme
} }
@ -347,9 +386,13 @@ object CommonActivity {
else -> R.style.OverlayPrimaryColorNormal else -> R.style.OverlayPrimaryColorNormal
} }
act.theme.applyStyle(currentTheme, true) act.theme.applyStyle(currentTheme, true)
act.theme.applyStyle(currentOverlayTheme, true) act.theme.applyStyle(currentOverlayTheme, true)
appliedTheme = currentTheme
appliedColor = currentOverlayTheme
act.updateTv()
if (isLayout(TV)) act.theme.applyStyle(R.style.AppThemeTvOverlay, true)
act.theme.applyStyle( act.theme.applyStyle(
R.style.LoadedStyle, R.style.LoadedStyle,
true true
@ -380,8 +423,7 @@ object CommonActivity {
private fun View.hasContent(): Boolean { private fun View.hasContent(): Boolean {
return isShown && when (this) { return isShown && when (this) {
//is RecyclerView -> this.childCount > 0 is ViewGroup -> this.isNotEmpty()
is ViewGroup -> this.childCount > 0
else -> true else -> true
} }
} }
@ -411,7 +453,7 @@ object CommonActivity {
// if cant focus but visible then break and let android decide // if cant focus but visible then break and let android decide
// the exception if is the view is a parent and has children that wants focus // the exception if is the view is a parent and has children that wants focus
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent -> val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0 parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.isNotEmpty()
} ?: false } ?: false
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
@ -489,84 +531,8 @@ object CommonActivity {
} }
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) { fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?): Boolean? {
return null
// 149 keycode_numpad 5
when (keyCode) {
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
PlayerEventType.SeekForward
}
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
PlayerEventType.SeekBack
}
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_CHANNEL_UP -> {
PlayerEventType.NextEpisode
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_CHANNEL_DOWN -> {
PlayerEventType.PrevEpisode
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
PlayerEventType.Pause
}
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
PlayerEventType.Play
}
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
PlayerEventType.Lock
}
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
PlayerEventType.ToggleHide
}
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
PlayerEventType.ToggleMute
}
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
PlayerEventType.ShowMirrors
}
// OpenSubtitles shortcut
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
PlayerEventType.ShowSpeed
}
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
PlayerEventType.Resize
}
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp
}
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
PlayerEventType.SkipCurrentChapter
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}
else -> null
}?.let { playerEvent ->
playerEventListener?.invoke(playerEvent)
}
//when (keyCode) {
// KeyEvent.KEYCODE_DPAD_CENTER -> {
// println("DPAD PRESSED")
// }
//}
} }
/** overrides focus and custom key events */ /** overrides focus and custom key events */
@ -603,6 +569,7 @@ object CommonActivity {
else -> null else -> null
} }
// println("NEXT FOCUS : $nextView") // println("NEXT FOCUS : $nextView")
if (nextView != null) { if (nextView != null) {
nextView.requestFocus() nextView.requestFocus()
@ -610,10 +577,15 @@ object CommonActivity {
return true return true
} }
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && // 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 || keyCode == KeyEvent.KEYCODE_ENTER) &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) { ) {
UIHelper.showInputMethod(act.currentFocus?.findFocus()) showInputMethod(act.currentFocus?.findFocus())
} }
//println("Keycode: $keyCode") //println("Keycode: $keyCode")
@ -622,7 +594,6 @@ object CommonActivity {
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}", // "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG // Toast.LENGTH_LONG
//) //)
} }
// if someone else want to override the focus then don't handle the event as it is already // if someone else want to override the focus then don't handle the event as it is already

View file

@ -1,11 +0,0 @@
package com.lagradost.cloudstream3
import android.view.LayoutInflater
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.ui.HeaderViewDecoration
fun setHeaderDecoration(view: RecyclerView, @LayoutRes headerViewRes: Int) {
val headerView = LayoutInflater.from(view.context).inflate(headerViewRes, null)
view.addItemDecoration(HeaderViewDecoration(headerView))
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
package com.lagradost.cloudstream3.actions
import android.content.Context
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
class AlwaysAskAction : VideoClickAction() {
override val name = txt(R.string.player_settings_always_ask)
override val isPlayer = true
// Only show in settings, not on a video
override fun shouldShow(context: Context?, video: ResultEpisode?): Boolean = video == null
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
// This is handled specially in ResultViewModel2.kt by detecting the AlwaysAskAction
// and showing the player selection dialog instead of executing the action directly
throw NotImplementedError("AlwaysAskAction is handled specially by the calling code")
}
}

View file

@ -6,8 +6,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.net.toUri import androidx.core.net.toUri
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
@ -21,7 +21,8 @@ import java.io.File
fun updateDurationAndPosition(position: Long, duration: Long) { fun updateDurationAndPosition(position: Long, duration: Long) {
if (position <= 0 || duration <= 0) return if (position <= 0 || duration <= 0) return
DataStoreHelper.setViewPos(getKey("last_opened_id"), position, duration) val episode = getKey<ResultEpisode>("last_opened") ?: return
DataStoreHelper.setViewPosAndResume(episode.id, position, duration, episode, null)
ResultFragment.updateUI() ResultFragment.updateUI()
} }
@ -98,7 +99,7 @@ abstract class OpenInAppAction(
intent.component = ComponentName(packageName, intentClass) intent.component = ComponentName(packageName, intentClass)
} }
putExtra(context, intent, video, result, index) putExtra(context, intent, video, result, index)
setKey("last_opened_id", video.id) setKey("last_opened", video)
launchResult(intent) launchResult(intent)
} }

View file

@ -12,44 +12,63 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.temp.BiglyBTPackage
import com.lagradost.cloudstream3.actions.temp.CopyClipboardAction import com.lagradost.cloudstream3.actions.temp.CopyClipboardAction
import com.lagradost.cloudstream3.actions.temp.JustPlayerPackage
import com.lagradost.cloudstream3.actions.temp.LibreTorrentPackage
import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage 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.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.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
import com.lagradost.cloudstream3.actions.temp.VlcNightlyPackage
import com.lagradost.cloudstream3.actions.temp.VlcPackage import com.lagradost.cloudstream3.actions.temp.VlcPackage
import com.lagradost.cloudstream3.actions.temp.WebVideoCastPackage import com.lagradost.cloudstream3.actions.temp.WebVideoCastPackage
import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe 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.ExtractorLinkType
import com.lagradost.cloudstream3.utils.UiText
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.FutureTask import java.util.concurrent.FutureTask
import kotlin.reflect.jvm.jvmName
object VideoClickActionHolder { object VideoClickActionHolder {
val allVideoClickActions = threadSafeListOf( val allVideoClickActions = atomicListOf(
// Default // Default
PlayInBrowserAction(), PlayInBrowserAction(),
CopyClipboardAction(), CopyClipboardAction(),
ViewM3U8Action(), ViewM3U8Action(),
PlayMirrorAction(),
// main support external apps // main support external apps
VlcPackage(), VlcPackage(),
MpvPackage(), MpvPackage(),
MpvExPackage(),
NextPlayerPackage(),
JustPlayerPackage(),
FcastAction(), FcastAction(),
LibreTorrentPackage(),
BiglyBTPackage(),
// forks/backup apps // forks/backup apps
VlcNightlyPackage(),
WebVideoCastPackage(), WebVideoCastPackage(),
MpvYTDLPackage(), MpvYTDLPackage(),
MpvKtPackage(), MpvKtPackage(),
MpvKtPreviewPackage(), MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option
AlwaysAskAction(),
// added by plugins // added by plugins
// ... // ...
) )
@ -141,7 +160,7 @@ abstract class VideoClickAction {
} }
} }
fun uniqueId() = "$sourcePlugin:${this::class.jvmName}" fun uniqueId() = "$sourcePlugin:${this::class.qualifiedName}"
@Throws @Throws
abstract fun shouldShow(context: Context?, video: ResultEpisode?): Boolean abstract fun shouldShow(context: Context?, video: ResultEpisode?): Boolean

View file

@ -0,0 +1,30 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
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/devgianlu/Aria2Android */
@Suppress("unused")
class Aria2Package : OpenInAppAction(
appName = txt("Aria2"),
packageName = "com.gianlu.aria2android",
intentClass = "com.gianlu.aria2android.MainActivity"
) {
override val oneSource: Boolean = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
throw NotImplementedError("Aria2Android is missing getIntent, and onNewIntent, meaning it cant handle intents")
}
override fun onResult(activity: Activity, intent: Intent?) = Unit
}

View file

@ -0,0 +1,36 @@
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.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/BiglySoftware/BiglyBT-Android */
class BiglyBTPackage : OpenInAppAction(
appName = txt("BiglyBT"),
packageName = "com.biglybt.android.client",
intentClass = "com.biglybt.android.client.activity.IntentHandler"
) {
// Only torrents are supported by the app
override val sourceTypes: Set<ExtractorLinkType> =
setOf(ExtractorLinkType.MAGNET, ExtractorLinkType.TORRENT)
override val oneSource: Boolean = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.data = result.links[index!!].url.toUri()
}
override fun onResult(activity: Activity, intent: Intent?) = Unit
}

View file

@ -0,0 +1,160 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.SkipSerializationTest
import com.lagradost.cloudstream3.ui.player.ExtractorUri
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
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.newExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromLanguageToTagIETF
import com.lagradost.cloudstream3.utils.serializers.UriSerializer
import com.lagradost.cloudstream3.utils.txt
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
/**
* If you want to support CloudStream 3 as an external player, then this shows how to play any video link
* For basic interactions, just `intent.data = uri` works
*
* However for more advanced use, CloudStream 3 also supports playlists of MinimalVideoLink and MinimalSubtitleLink with a `String[]` of JSON
* These are passed as LINKS_EXTRA and SUBTITLE_EXTRA respectively
*/
@Suppress("Unused")
class CloudStreamPackage : OpenInAppAction(
appName = txt("CloudStream"),
packageName = BuildConfig.APPLICATION_ID, //"com.lagradost.cloudstream3" or "com.lagradost.cloudstream3.prerelease"
intentClass = "com.lagradost.cloudstream3.ui.player.DownloadedPlayerActivity"
) {
override val oneSource: Boolean = false
companion object {
const val SUBTITLE_EXTRA: String = "subs" // Json of an array of MinimalVideoLink
const val LINKS_EXTRA: String = "links" // Json of an array of MinimalSubtitleLink
const val TITLE_EXTRA: String = "title" // Unused (String)
const val ID_EXTRA: String =
"id" // Identification number for the video(s), used to store start time (Int)
const val POSITION_EXTRA: String = "pos" // Start time in MS (Long)
const val DURATION_EXTRA: String = "dur" // Duration time in MS (Long)
}
@Serializable
@SkipSerializationTest //.Uri has issues with Jackson
data class MinimalVideoLink(
@JsonProperty("uri") @SerialName("uri")
@Serializable(with = UriSerializer::class)
val uri: Uri?,
@JsonProperty("url") @SerialName("url") val url: String?,
@JsonProperty("mimeType") @SerialName("mimeType") val mimeType: String = "video/mp4",
@JsonProperty("name") @SerialName("name") val name: String?,
@JsonProperty("headers") @SerialName("headers") var headers: Map<String, String> = mapOf(),
@JsonProperty("quality") @SerialName("quality") val quality: Int?,
) {
companion object {
fun fromExtractor(link: ExtractorLink): MinimalVideoLink = MinimalVideoLink(
uri = null,
url = link.url,
name = link.name,
mimeType = link.type.getMimeType(),
headers = if (link.referer.isBlank()) emptyMap() else mapOf("referer" to link.referer) + link.headers,
quality = link.quality
)
}
suspend fun toExtractorLink(): Pair<ExtractorLink?, ExtractorUri?> =
url?.let { url ->
newExtractorLink(
source = "NONE",
name = name ?: "Unknown",
url = url,
type = ExtractorLinkType.entries.firstOrNull { ty -> ty.getMimeType() == mimeType }
?: ExtractorLinkType.VIDEO) {
this@newExtractorLink.headers =
this@MinimalVideoLink.headers
this@newExtractorLink.quality =
this@MinimalVideoLink.quality ?: Qualities.Unknown.value
}
} to uri?.let { uri ->
ExtractorUri(
uri = uri,
name = name ?: "Unknown",
)
}
}
@Serializable
data class MinimalSubtitleLink(
@JsonProperty("url") @SerialName("url") val url: String,
@JsonProperty("mimeType") @SerialName("mimeType") val mimeType: String = "text/vtt",
@JsonProperty("name") @SerialName("name") val name: String?,
@JsonProperty("headers") @SerialName("headers") var headers: Map<String, String> = mapOf(),
) {
companion object {
fun fromSubtitle(sub: SubtitleData): MinimalSubtitleLink = MinimalSubtitleLink(
url = sub.url,
mimeType = sub.mimeType,
name = sub.originalName,
headers = sub.headers,
)
}
fun toSubtitleData(): SubtitleData = SubtitleData(
url = url,
nameSuffix = "",
mimeType = mimeType,
originalName = name ?: "Unknown",
headers = headers,
origin = SubtitleOrigin.URL,
languageCode = fromCodeToLangTagIETF(name) ?:
fromLanguageToTagIETF(name, true) ?:
name,
)
}
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.apply {
val position = getViewPos(video.id)?.position
if (position != null)
putExtra(POSITION_EXTRA, position)
putExtra(ID_EXTRA, video.id)
putExtra(TITLE_EXTRA, video.name)
putExtra(
SUBTITLE_EXTRA,
result.subs.map { MinimalSubtitleLink.fromSubtitle(it).toJson() }.toTypedArray()
)
putExtra(
LINKS_EXTRA,
result.links.filter { it !is ExtractorLinkPlayList && it !is DrmExtractorLink }
.map { MinimalVideoLink.fromExtractor(it).toJson() }.toTypedArray()
)
}
}
override fun onResult(activity: Activity, intent: Intent?) {
// No results yet
}
}

View file

@ -0,0 +1,37 @@
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.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/moneytoo/Player/ */
class JustPlayerPackage : OpenInAppAction(
appName = txt("JustPlayer"),
packageName = "com.brouken.player",
intentClass = "com.brouken.player.PlayerActivity"
) {
override val sourceTypes: Set<ExtractorLinkType> =
setOf(ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8, ExtractorLinkType.DASH)
override val oneSource: Boolean = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
// While JustPlayer has support for subs, it cant add both subs and links at the same time
// See https://github.com/moneytoo/Player/blob/49d80eb8de7a7bfc662393fdf114788fed1ebb2e/app/src/main/java/com/brouken/player/PlayerActivity.java#L794
intent.data = result.links[index!!].url.toUri()
}
override fun onResult(activity: Activity, intent: Intent?) = Unit
}

View file

@ -0,0 +1,36 @@
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.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/proninyaroslav/libretorrent */
class LibreTorrentPackage : OpenInAppAction(
appName = txt("LibreTorrent"),
packageName = "org.proninyaroslav.libretorrent",
intentClass = "org.proninyaroslav.libretorrent.ui.addtorrent.AddTorrentActivity"
) {
// Only torrents are supported by the app
override val sourceTypes: Set<ExtractorLinkType> =
setOf(ExtractorLinkType.MAGNET, ExtractorLinkType.TORRENT)
override val oneSource: Boolean = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.data = result.links[index!!].url.toUri()
}
override fun onResult(activity: Activity, intent: Intent?) = Unit
}

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.actions.temp
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.lagradost.cloudstream3.actions.OpenInAppAction import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition import com.lagradost.cloudstream3.actions.updateDurationAndPosition
@ -45,7 +44,7 @@ open class MpvKtPackage(
intent.apply { intent.apply {
putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray()) putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray())
setDataAndType(Uri.parse(link.url), "video/*") setDataAndType(link.url.toUri(), "video/*")
// m3u8 plays, but changing sources feature is not available // m3u8 plays, but changing sources feature is not available
// makeTempM3U8Intent(activity, this, result) // makeTempM3U8Intent(activity, this, result)

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.actions.temp
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.lagradost.api.Log import com.lagradost.api.Log
import com.lagradost.cloudstream3.actions.OpenInAppAction import com.lagradost.cloudstream3.actions.OpenInAppAction
@ -18,6 +17,9 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkType
// https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904 // https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
// https://mpv-android.github.io/mpv-android/intent.html // https://mpv-android.github.io/mpv-android/intent.html
//https://github.com/marlboro-advance/mpvEx
class MpvExPackage: MpvPackage("mpvEx","app.marlboroadvance.mpvex","app.marlboroadvance.mpvex.ui.player.PlayerActivity")
class MpvYTDLPackage : MpvPackage("MPV YTDL", "is.xyz.mpv.ytdl") { class MpvYTDLPackage : MpvPackage("MPV YTDL", "is.xyz.mpv.ytdl") {
override val sourceTypes = setOf( override val sourceTypes = setOf(
ExtractorLinkType.VIDEO, ExtractorLinkType.VIDEO,
@ -26,10 +28,10 @@ class MpvYTDLPackage : MpvPackage("MPV YTDL", "is.xyz.mpv.ytdl") {
) )
} }
open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv"): OpenInAppAction( open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv",intentClass:String = "is.xyz.mpv.MPVActivity"): OpenInAppAction(
txt(appName), txt(appName),
packageName, packageName,
"is.xyz.mpv.MPVActivity" intentClass
) { ) {
override val oneSource = true // mpv has poor playlist support on TV override val oneSource = true // mpv has poor playlist support on TV
override suspend fun putExtra( override suspend fun putExtra(
@ -44,7 +46,7 @@ open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv
putExtra("title", video.name) putExtra("title", video.name)
if (index != null) { if (index != null) {
setDataAndType(Uri.parse(result.links.getOrNull(index)?.url ?: return), "video/*") setDataAndType((result.links.getOrNull(index)?.url ?: return).toUri(), "video/*")
} else { } else {
makeTempM3U8Intent(context, this, result) makeTempM3U8Intent(context, this, result)
} }

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,35 @@
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.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.txt
/** https://github.com/anilbeesetti/nextplayer */
class NextPlayerPackage : OpenInAppAction(
appName = txt("NextPlayer"),
packageName = "dev.anilbeesetti.nextplayer",
intentClass = "dev.anilbeesetti.nextplayer.feature.player.PlayerActivity"
) {
override val sourceTypes: Set<ExtractorLinkType> =
setOf(ExtractorLinkType.VIDEO, ExtractorLinkType.M3U8, ExtractorLinkType.DASH)
override val oneSource: Boolean = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.data = result.links[index!!].url.toUri()
}
override fun onResult(activity: Activity, intent: Intent?) = Unit
}

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

@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.actions.temp
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import androidx.core.net.toUri
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
@ -33,7 +33,7 @@ class PlayInBrowserAction: VideoClickAction() {
) { ) {
val link = result.links.getOrNull(index ?: 0) ?: return val link = result.links.getOrNull(index ?: 0) ?: return
val i = Intent(Intent.ACTION_VIEW) val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(link.url) i.data = link.url.toUri()
launch(i) launch(i)
} }
} }

View file

@ -0,0 +1,65 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.player.ExtractorUri
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LOADTYPE_INAPP
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.player.VideoGenerator
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.txt
class PlayMirrorAction : VideoClickAction() {
override val name = txt(R.string.episode_action_play_mirror)
override val oneSource = true
override val isPlayer = true
override val sourceTypes: Set<ExtractorLinkType> = LOADTYPE_INAPP
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
//Implemented a generator to handle the single
val activity = context as? Activity ?: return
val link = index?.let { result.links[it] }
val generatorMirror = object : VideoGenerator<ResultEpisode>(listOf(video)) {
override val hasCache: Boolean = false
override val canSkipLoading: Boolean = false
override fun getId(index: Int): Int = video.id
override suspend fun generateLinks(
clearCache: Boolean,
sourceTypes: Set<ExtractorLinkType>,
callback: (Pair<ExtractorLink?, ExtractorUri?>) -> Unit,
subtitleCallback: (SubtitleData) -> Unit,
offset: Int,
isCasting: Boolean
): Boolean {
index?.let { callback(link to null) }
result.subs.forEach { subtitle -> subtitleCallback(subtitle) }
return true
}
}
activity.navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
generatorMirror, 0, result.syncData
)
)
}
}

View file

@ -6,7 +6,7 @@ import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.net.toUri import androidx.core.net.toUri
import com.lagradost.api.Log import com.lagradost.api.Log
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.actions.OpenInAppAction import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.makeTempM3U8Intent import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.actions.updateDurationAndPosition import com.lagradost.cloudstream3.actions.updateDurationAndPosition
@ -19,7 +19,12 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
// https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 // https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
// https://wiki.videolan.org/Android_Player_Intents/ // https://wiki.videolan.org/Android_Player_Intents/
class VlcPackage: OpenInAppAction( class VlcNightlyPackage : VlcPackage() {
override val packageName = "org.videolan.vlc.debug"
override val appName = txt("VLC Nightly")
}
open class VlcPackage: OpenInAppAction(
appName = txt("VLC"), appName = txt("VLC"),
packageName = "org.videolan.vlc", packageName = "org.videolan.vlc",
intentClass = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { intentClass = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@ -55,7 +60,7 @@ class VlcPackage: OpenInAppAction(
intent.putExtra("secure_uri", true) intent.putExtra("secure_uri", true)
intent.putExtra("title", video.name) intent.putExtra("title", video.name)
val subsLang = getKey(SUBTITLE_AUTO_SELECT_KEY) ?: "en" val subsLang = getKey<String>(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
result.subs.firstOrNull { result.subs.firstOrNull {
subsLang == it.languageCode subsLang == it.languageCode
}?.let { }?.let {

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.actions.temp
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.core.net.toUri import androidx.core.net.toUri
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
@ -38,7 +37,7 @@ class WebVideoCastPackage: OpenInAppAction(
val link = result.links[index ?: 0] val link = result.links[index ?: 0]
intent.apply { intent.apply {
setDataAndType(Uri.parse(link.url), "video/*") setDataAndType(link.url.toUri(), "video/*")
val title = video.name ?: video.headerName val title = video.name ?: video.headerName

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.actions.temp.fcast package com.lagradost.cloudstream3.actions.temp.fcast
import android.content.Context import android.content.Context
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickAction

View file

@ -7,6 +7,7 @@ import android.net.nsd.NsdServiceInfo
import android.os.Build import android.os.Build
import android.os.ext.SdkExtensions import android.os.ext.SdkExtensions
import android.util.Log import android.util.Log
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
class FcastManager { class FcastManager {
@ -72,18 +73,25 @@ class FcastManager {
} }
override fun onServiceFound(serviceInfo: NsdServiceInfo?) { override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
if (serviceInfo == null) return // Safe here as, java.lang.NoClassDefFoundError: Failed resolution of: Landroid/net/nsd/NsdManager$ServiceInfoCallback
safe {
if (serviceInfo == null) return@safe
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(
Build.VERSION_CODES.TIRAMISU) >= 7) { Build.VERSION_CODES.TIRAMISU
nsdManager?.registerServiceInfoCallback(serviceInfo, ) >= 7
) {
nsdManager?.registerServiceInfoCallback(
serviceInfo,
Runnable::run, Runnable::run,
object : NsdManager.ServiceInfoCallback { object : NsdManager.ServiceInfoCallback {
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) { override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
Log.e(tag, "Service registration failed: $errorCode") Log.e(tag, "Service registration failed: $errorCode")
} }
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) { override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
Log.d(tag, Log.d(
tag,
"Service updated: ${serviceInfo.serviceName}," + "Service updated: ${serviceInfo.serviceName}," +
"Net: ${serviceInfo.hostAddresses.firstOrNull()?.hostAddress}" "Net: ${serviceInfo.hostAddresses.firstOrNull()?.hostAddress}"
) )
@ -92,18 +100,24 @@ class FcastManager {
_currentDevices.add(PublicDeviceInfo(serviceInfo)) _currentDevices.add(PublicDeviceInfo(serviceInfo))
} }
} }
override fun onServiceLost() { override fun onServiceLost() {
Log.d(tag, "Service lost: ${serviceInfo.serviceName},") Log.d(tag, "Service lost: ${serviceInfo.serviceName},")
synchronized(_currentDevices) { synchronized(_currentDevices) {
_currentDevices.removeIf { it.rawName == serviceInfo.serviceName } _currentDevices.removeIf { it.rawName == serviceInfo.serviceName }
} }
} }
override fun onServiceInfoCallbackUnregistered() {} override fun onServiceInfoCallbackUnregistered() {}
}) })
} else { } else {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
nsdManager?.resolveService(serviceInfo, object : ResolveListener { nsdManager?.resolveService(serviceInfo, object : ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {} override fun onResolveFailed(
serviceInfo: NsdServiceInfo?,
errorCode: Int
) {
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) { override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
if (serviceInfo == null) return if (serviceInfo == null) return
@ -120,6 +134,7 @@ class FcastManager {
}) })
} }
} }
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) { override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
if (serviceInfo == null) return if (serviceInfo == null) return
@ -168,7 +183,8 @@ class PublicDeviceInfo(serviceInfo: NsdServiceInfo) {
val host: String? = if ( val host: String? = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
SdkExtensions.getExtensionVersion( SdkExtensions.getExtensionVersion(
Build.VERSION_CODES.TIRAMISU) >= 7 Build.VERSION_CODES.TIRAMISU
) >= 7
) { ) {
serviceInfo.hostAddresses.firstOrNull()?.hostAddress serviceInfo.hostAddresses.firstOrNull()?.hostAddress
} else { } else {

View file

@ -1,5 +1,9 @@
package com.lagradost.cloudstream3.actions.temp.fcast package com.lagradost.cloudstream3.actions.temp.fcast
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1 // See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
enum class Opcode(val value: Byte) { enum class Opcode(val value: Byte) {
None(0), None(0),
@ -18,18 +22,18 @@ enum class Opcode(val value: Byte) {
Pong(13); Pong(13);
} }
@Serializable
data class PlayMessage( data class PlayMessage(
val container: String, @JsonProperty("container") @SerialName("container") val container: String,
val url: String? = null, @JsonProperty("url") @SerialName("url") val url: String? = null,
val content: String? = null, @JsonProperty("content") @SerialName("content") val content: String? = null,
val time: Double? = null, @JsonProperty("time") @SerialName("time") val time: Double? = null,
val speed: Double? = null, @JsonProperty("speed") @SerialName("speed") val speed: Double? = null,
val headers: Map<String, String>? = null @JsonProperty("headers") @SerialName("headers") val headers: Map<String, String>? = null,
) )
data class SeekMessage( data class SeekMessage(
val time: Double val time: Double,
) )
data class PlaybackUpdateMessage( data class PlaybackUpdateMessage(
@ -37,26 +41,26 @@ data class PlaybackUpdateMessage(
val time: Double, val time: Double,
val duration: Double, val duration: Double,
val state: Int, val state: Int,
val speed: Double val speed: Double,
) )
data class VolumeUpdateMessage( data class VolumeUpdateMessage(
val generationTime: Long, val generationTime: Long,
val volume: Double val volume: Double,
) )
data class PlaybackErrorMessage( data class PlaybackErrorMessage(
val message: String val message: String,
) )
data class SetSpeedMessage( data class SetSpeedMessage(
val speed: Double val speed: Double,
) )
data class SetVolumeMessage( data class SetVolumeMessage(
val volume: Double val volume: Double,
) )
data class VersionMessage( data class VersionMessage(
val version: Long val version: Long,
) )

View file

@ -1,16 +1,68 @@
package com.lagradost.cloudstream3.mvvm package com.lagradost.cloudstream3.mvvm
import android.view.View
import androidx.activity.ComponentActivity
import androidx.core.view.doOnAttach
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.ui.BaseFragment
/** NOTE: Only one observer at a time per value */ /** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) { fun <T> ComponentActivity.observe(liveData: LiveData<T>, action: (T) -> Unit) {
liveData.removeObservers(this) observeNullable(liveData) { t -> t?.run(action) }
liveData.observe(this) { it?.let { t -> action(t) } }
} }
/** NOTE: Only one observer at a time per value */ /** NOTE: Only one observer at a time per value */
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) { fun <T> ComponentActivity.observeNullable(liveData: LiveData<T>, action: (T?) -> Unit) {
liveData.removeObservers(this) liveData.removeObservers(this)
liveData.observe(this) { action(it) } liveData.observe(this, action)
}
/** NOTE: Only one observer at a time per value */
fun <T, V : ViewBinding> BaseFragment<V>.observe(liveData: LiveData<T>, action: (T) -> Unit) {
observeNullable(liveData) { t -> t?.run(action) }
}
/**
* Attaches an observable to the root binding, instead of the fragment. This is more efficient as
* it will not call observe if the view is in the background.
*
* NOTE: Only one observer at a time per value
* */
fun <T, V : ViewBinding> BaseFragment<V>.observeNullable(
liveData: LiveData<T>, action: (T?) -> Unit
) {
val root = this.binding?.root
if (root == null) {
liveData.removeObservers(this)
liveData.observe(this, action)
} else {
root.doOnAttach { view ->
// On attach should make findViewTreeLifecycleOwner non-null, but use "this" just in case
val owner: LifecycleOwner = view.findViewTreeLifecycleOwner() ?: this@observeNullable
liveData.removeObservers(owner)
liveData.observe(owner, action)
}
}
}
/** NOTE: Only one observer at a time per value */
fun <T> View.observe(liveData: LiveData<T>, action: (T) -> Unit) {
observeNullable(liveData) { t -> t?.run(action) }
}
/** NOTE: Only one observer at a time per value */
fun <T> View.observeNullable(liveData: LiveData<T>, action: (T?) -> Unit) {
doOnAttach { view ->
// On attach should make findViewTreeLifecycleOwner non-null
val owner: LifecycleOwner? = view.findViewTreeLifecycleOwner()
if(owner == null) {
debugException { "Expected non-null findViewTreeLifecycleOwner" }
return@doOnAttach
}
liveData.removeObservers(owner)
liveData.observe(owner, action)
}
} }

View file

@ -5,7 +5,7 @@ import android.webkit.CookieManager
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugWarning import com.lagradost.cloudstream3.mvvm.debugWarning
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.Requests.Companion.await
import com.lagradost.nicehttp.cookies import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -32,7 +32,7 @@ class CloudflareKiller : Interceptor {
init { init {
// Needs to clear cookies between sessions to generate new cookies. // Needs to clear cookies between sessions to generate new cookies.
normalSafeApiCall { safe {
// This can throw an exception on unsupported devices :( // This can throw an exception on unsupported devices :(
CookieManager.getInstance().removeAllCookies(null) CookieManager.getInstance().removeAllCookies(null)
} }
@ -77,7 +77,7 @@ class CloudflareKiller : Interceptor {
} }
private fun getWebViewCookie(url: String): String? { private fun getWebViewCookie(url: String): String? {
return normalSafeApiCall { return safe {
CookieManager.getInstance()?.getCookie(url) CookieManager.getInstance()?.getCookie(url)
} }
} }

View file

@ -2,9 +2,10 @@ package com.lagradost.cloudstream3.network
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.Prerelease
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ignoreAllSSLErrors import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache import okhttp3.Cache
@ -15,19 +16,36 @@ import org.conscrypt.Conscrypt
import java.io.File import java.io.File
import java.security.Security import java.security.Security
// Backwards compatible constructor, mark as deprecated later
fun Requests.initClient(context: Context) { fun Requests.initClient(context: Context) {
this.baseClient = buildDefaultClient(context) this.baseClient = buildDefaultClient(context)
} }
/** Only use ignoreSSL if you know what you are doing*/
fun Requests.initClient(context: Context, ignoreSSL: Boolean = false) {
this.baseClient = buildDefaultClient(context, ignoreSSL)
}
// Backwards compatible constructor, mark as deprecated later
fun buildDefaultClient(context: Context): OkHttpClient { fun buildDefaultClient(context: Context): OkHttpClient {
normalSafeApiCall { Security.insertProviderAt(Conscrypt.newProvider(), 1) } return buildDefaultClient(context, false)
}
/** Only use ignoreSSL if you know what you are doing*/
fun buildDefaultClient(context: Context, ignoreSSL: Boolean = false): OkHttpClient {
safe { Security.insertProviderAt(Conscrypt.newProvider(), 1) }
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0) val dns = settingsManager.getInt(context.getString(R.string.dns_pref), 0)
val baseClient = OkHttpClient.Builder() val baseClient = OkHttpClient.Builder()
.followRedirects(true) .followRedirects(true)
.followSslRedirects(true) .followSslRedirects(true)
.ignoreAllSSLErrors() .apply {
if (ignoreSSL) {
ignoreAllSSLErrors()
}
}
.cache( .cache(
// Note that you need to add a ResponseInterceptor to make this 100% active. // Note that you need to add a ResponseInterceptor to make this 100% active.
// The server response dictates if and when stuff should be cached. // The server response dictates if and when stuff should be cached.
@ -52,11 +70,6 @@ fun buildDefaultClient(context: Context): OkHttpClient {
return baseClient return baseClient
} }
//val Request.cookies: Map<String, String>
// get() {
// return this.headers.getCookies("Cookie")
// }
private val DEFAULT_HEADERS = mapOf("user-agent" to USER_AGENT) private val DEFAULT_HEADERS = mapOf("user-agent" to USER_AGENT)
/** /**

View file

@ -7,7 +7,6 @@ import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import kotlin.Throws import kotlin.Throws
abstract class Plugin : BasePlugin() { abstract class Plugin : BasePlugin() {
/** /**
* Called when your Plugin is loaded * Called when your Plugin is loaded
@ -26,10 +25,8 @@ abstract class Plugin : BasePlugin() {
fun registerVideoClickAction(element: VideoClickAction) { fun registerVideoClickAction(element: VideoClickAction) {
Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction") Log.i(PLUGIN_TAG, "Adding ${element.name} VideoClickAction")
element.sourcePlugin = this.filename element.sourcePlugin = this.filename
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.add(element) VideoClickActionHolder.allVideoClickActions.add(element)
} }
}
/** /**
* This will contain your resources if you specified requiresResources in gradle * This will contain your resources if you specified requiresResources in gradle

View file

@ -13,6 +13,7 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.WorkerThread
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@ -20,30 +21,32 @@ import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.removePluginMapping import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.AutoDownloadMode import com.lagradost.cloudstream3.AutoDownloadMode
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.InternalAPI
import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.PROVIDER_STATUS_OK import com.lagradost.cloudstream3.PROVIDER_STATUS_OK
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.actions.VideoClickAction import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.debugPrint import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
import com.lagradost.cloudstream3.plugins.RepositoryManager.sha256
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings
@ -52,12 +55,14 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import com.lagradost.cloudstream3.utils.downloader.DownloadFileManagement.sanitizeFilename
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.txt
import dalvik.system.PathClassLoader import dalvik.system.PathClassLoader
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.File import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
@ -70,13 +75,15 @@ const val EXTENSIONS_CHANNEL_NAME = "Extensions"
const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel" const val EXTENSIONS_CHANNEL_DESCRIPT = "Extension notification channel"
// Data class for internal storage // Data class for internal storage
@Serializable
data class PluginData( data class PluginData(
@JsonProperty("internalName") val internalName: String, @JsonProperty("internalName") @SerialName("internalName") val internalName: String,
@JsonProperty("url") val url: String?, @JsonProperty("url") @SerialName("url") val url: String?,
@JsonProperty("isOnline") val isOnline: Boolean, @JsonProperty("isOnline") @SerialName("isOnline") val isOnline: Boolean,
@JsonProperty("filePath") val filePath: String, @JsonProperty("filePath") @SerialName("filePath") val filePath: String,
@JsonProperty("version") val version: Int, @JsonProperty("version") @SerialName("version") val version: Int,
) { ) {
@WorkerThread
fun toSitePlugin(): SitePlugin { fun toSitePlugin(): SitePlugin {
return SitePlugin( return SitePlugin(
this.filePath, this.filePath,
@ -91,7 +98,9 @@ data class PluginData(
null, null,
null, null,
null, null,
File(this.filePath).length() File(this.filePath).length(),
// No file hash for local plugins. Local plugins have no use for the hash, and it's expensive to compute.
null
) )
} }
} }
@ -145,7 +154,7 @@ object PluginManager {
!it.filePath.contains(repositoryPath) !it.filePath.contains(repositoryPath)
} }
val file = File(repositoryPath) val file = File(repositoryPath)
normalSafeApiCall { safe {
if (file.exists()) file.deleteRecursively() if (file.exists()) file.deleteRecursively()
} }
setKey(PLUGINS_KEY, plugins) setKey(PLUGINS_KEY, plugins)
@ -167,11 +176,11 @@ object PluginManager {
fun getPluginsOnline(): Array<PluginData> { fun getPluginsOnline(): Array<PluginData> {
return getKey(PLUGINS_KEY) ?: emptyArray() return getKey<Array<PluginData>>(PLUGINS_KEY) ?: emptyArray()
} }
fun getPluginsLocal(): Array<PluginData> { fun getPluginsLocal(): Array<PluginData> {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() return getKey<Array<PluginData>>(PLUGINS_KEY_LOCAL) ?: emptyArray()
} }
private val CLOUD_STREAM_FOLDER = private val CLOUD_STREAM_FOLDER =
@ -215,17 +224,17 @@ object PluginManager {
// Helper class for updateAllOnlinePluginsAndLoadThem // Helper class for updateAllOnlinePluginsAndLoadThem
data class OnlinePluginData( data class OnlinePluginData(
val savedData: PluginData, val savedData: PluginData,
val onlineData: Pair<String, SitePlugin>, val onlineData: PluginWrapper,
) { ) {
val isOutdated = val isOutdated =
onlineData.second.version > savedData.version || onlineData.second.version == PLUGIN_VERSION_ALWAYS_UPDATE onlineData.plugin.version > savedData.version || onlineData.plugin.version == PLUGIN_VERSION_ALWAYS_UPDATE
val isDisabled = onlineData.second.status == PROVIDER_STATUS_DOWN val isDisabled = onlineData.plugin.status == PROVIDER_STATUS_DOWN
fun validOnlineData(context: Context): Boolean { fun validOnlineData(context: Context): Boolean {
return getPluginPath( return getPluginPath(
context, context,
savedData.internalName, savedData.internalName,
onlineData.first onlineData.repositoryData.url
).absolutePath == savedData.filePath ).absolutePath == savedData.filePath
} }
} }
@ -255,29 +264,37 @@ object PluginManager {
* 2. If disabled do nothing * 2. If disabled do nothing
* 3. If outdated download and load the plugin * 3. If outdated download and load the plugin
* 4. Else load the plugin normally * 4. Else load the plugin normally
**/ *
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
* If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
*/
@Suppress("FunctionName")
@InternalAPI
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_updateAllOnlinePluginsAndLoadThem(activity: Activity) {
assertNonRecursiveCallstack()
// Load all plugins as fast as possible! // Load all plugins as fast as possible!
loadAllOnlinePlugins(activity) ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(activity)
afterPluginsLoadedEvent.invoke(false) afterPluginsLoadedEvent.invoke(false)
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().apmap { val onlinePlugins = urls.toList().amap {
getRepoPlugins(it.url)?.toList() ?: emptyList() getRepoPlugins(it) ?: emptyList()
}.flatten().distinctBy { it.second.url } }.flatten().distinctBy { it.plugin.url }
// Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated // Iterates over all offline plugins, compares to remote repo and returns the plugins which are outdated
val outdatedPlugins = getPluginsOnline().map { savedData -> val outdatedPlugins = getPluginsOnline().map { savedData ->
onlinePlugins onlinePlugins
.filter { onlineData -> savedData.internalName == onlineData.second.internalName } .filter { onlineData -> savedData.internalName == onlineData.plugin.internalName }
.map { onlineData -> .map { onlineData ->
OnlinePluginData(savedData, onlineData) OnlinePluginData(savedData, onlineData)
}.filter { }.filter {
it.validOnlineData(activity) it.validOnlineData(activity)
} }
}.flatten().distinctBy { it.onlineData.second.url } }.flatten().distinctBy { it.onlineData.plugin.url }
debugPrint { debugPrint {
"Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}" "Outdated plugins: ${outdatedPlugins.filter { it.isOutdated }}"
@ -285,20 +302,21 @@ object PluginManager {
val updatedPlugins = mutableListOf<String>() val updatedPlugins = mutableListOf<String>()
outdatedPlugins.apmap { pluginData -> outdatedPlugins.amap { pluginData ->
if (pluginData.isDisabled) { if (pluginData.isDisabled) {
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name)) //updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
unloadPlugin(pluginData.savedData.filePath) unloadPlugin(pluginData.savedData.filePath)
} else if (pluginData.isOutdated) { } else if (pluginData.isOutdated) {
downloadPlugin( downloadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.plugin.url,
pluginData.onlineData.plugin.fileHash,
pluginData.savedData.internalName, pluginData.savedData.internalName,
File(pluginData.savedData.filePath), File(pluginData.savedData.filePath),
true true
).let { success -> ).let { success ->
if (success) if (success)
updatedPlugins.add(pluginData.onlineData.second.name) updatedPlugins.add(pluginData.onlineData.plugin.name)
} }
} }
} }
@ -324,21 +342,32 @@ object PluginManager {
* 1. Gets all online data from online plugins repo * 1. Gets all online data from online plugins repo
* 2. Fetch all not downloaded plugins * 2. Fetch all not downloaded plugins
* 3. Download them and reload plugins * 3. Download them and reload plugins
**/ *
fun downloadNotExistingPluginsAndLoad(activity: Activity, mode: AutoDownloadMode) { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
* If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
*/
@Suppress("FunctionName")
@InternalAPI
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_downloadNotExistingPluginsAndLoad(
activity: Activity,
mode: AutoDownloadMode
) {
assertNonRecursiveCallstack()
val newDownloadPlugins = mutableListOf<String>() val newDownloadPlugins = mutableListOf<String>()
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().apmap { val onlinePlugins = urls.toList().amap {
getRepoPlugins(it.url)?.toList() ?: emptyList() getRepoPlugins(it)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url } }.flatten().distinctBy { it.plugin.url }
val providerLang = activity.getApiProviderLangSettings() val providerLang = activity.getApiProviderLangSettings()
//Log.i(TAG, "providerLang => ${providerLang.toJson()}") //Log.i(TAG, "providerLang => ${providerLang.toJson()}")
// Iterate online repos and returns not downloaded plugins // Iterate online repos and returns not downloaded plugins
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData -> val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
val sitePlugin = onlineData.second val sitePlugin = onlineData.plugin
val tvtypes = sitePlugin.tvTypes ?: listOf() val tvtypes = sitePlugin.tvTypes ?: listOf()
//Don't include empty urls //Don't include empty urls
@ -350,7 +379,7 @@ object PluginManager {
} }
//Omit already existing plugins //Omit already existing plugins
if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) { if (getPluginPath(activity, sitePlugin.internalName, onlineData.repositoryData.url).exists()) {
Log.i(TAG, "Skip > ${sitePlugin.internalName}") Log.i(TAG, "Skip > ${sitePlugin.internalName}")
return@mapNotNull null return@mapNotNull null
} }
@ -389,16 +418,17 @@ object PluginManager {
} }
//Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}") //Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
notDownloadedPlugins.apmap { pluginData -> notDownloadedPlugins.amap { pluginData ->
downloadPlugin( downloadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.plugin.url,
pluginData.onlineData.plugin.fileHash,
pluginData.savedData.internalName, pluginData.savedData.internalName,
pluginData.onlineData.first, pluginData.onlineData.repositoryData.url,
!pluginData.isDisabled !pluginData.isDisabled
).let { success -> ).let { success ->
if (success) if (success)
newDownloadPlugins.add(pluginData.onlineData.second.name) newDownloadPlugins.add(pluginData.onlineData.plugin.name)
} }
} }
@ -414,12 +444,27 @@ object PluginManager {
Log.i(TAG, "Plugin download done!") Log.i(TAG, "Plugin download done!")
} }
@Throws
private fun assertNonRecursiveCallstack() {
if (Thread.currentThread().stackTrace.any { it.methodName == "loadPlugin" }) {
throw Error("You tried to call a function that will recursively call loadPlugin, this will cause crashes or memory leaks. Do not do this, there is better ways to implement the feature than reloading plugins. Are you sure you read the compile error or docs?")
}
}
/** /**
* Use updateAllOnlinePluginsAndLoadThem * Use updateAllOnlinePluginsAndLoadThem
* */ *
fun loadAllOnlinePlugins(context: Context) { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
* If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
*/
@Suppress("FunctionName")
@InternalAPI
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context: Context) {
assertNonRecursiveCallstack()
// Load all plugins as fast as possible! // Load all plugins as fast as possible!
(getPluginsOnline()).toList().apmap { pluginData -> (getPluginsOnline()).toList().amap { pluginData ->
loadPlugin( loadPlugin(
context, context,
File(pluginData.filePath), File(pluginData.filePath),
@ -430,27 +475,46 @@ object PluginManager {
/** /**
* Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb * Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb
**/ *
fun hotReloadAllLocalPlugins(activity: FragmentActivity?) { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
* If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
*/
@Suppress("FunctionName")
@InternalAPI
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_hotReloadAllLocalPlugins(activity: FragmentActivity?) {
assertNonRecursiveCallstack()
Log.d(TAG, "Reloading all local plugins!") Log.d(TAG, "Reloading all local plugins!")
if (activity == null) return if (activity == null) return
getPluginsLocal().forEach { getPluginsLocal().forEach {
unloadPlugin(it.filePath) unloadPlugin(it.filePath)
} }
loadAllLocalPlugins(activity, true) ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(activity, true)
} }
/** /**
* @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins * @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins
* and reload all pages even if they are previously valid * and reload all pages even if they are previously valid
**/ *
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) { * DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
* If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
*/
@Suppress("FunctionName")
@InternalAPI
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(context: Context, forceReload: Boolean) {
assertNonRecursiveCallstack()
val dir = File(LOCAL_PLUGINS_PATH) val dir = File(LOCAL_PLUGINS_PATH)
if (!dir.exists()) { if (!dir.exists()) {
val res = dir.mkdirs() val res = dir.mkdirs()
if (!res) { if (!res) {
Log.w(TAG, "Failed to create local directories") Log.w(TAG, "Failed to create local directories")
// We have tried to load local plugins, but exit early.
// This needs to be true to prevent the downloader waiting for plugins.
loadedLocalPlugins = true
return return
} }
} }
@ -471,7 +535,7 @@ object PluginManager {
// Make sure all local plugins are fully refreshed. // Make sure all local plugins are fully refreshed.
removeKey(PLUGINS_KEY_LOCAL) removeKey(PLUGINS_KEY_LOCAL)
sortedPlugins?.sortedBy { it.name }?.apmap { file -> sortedPlugins?.sortedBy { it.name }?.amap { file ->
try { try {
val destinationFile = File(pluginDirectory, file.name) val destinationFile = File(pluginDirectory, file.name)
@ -479,7 +543,8 @@ object PluginManager {
// has been modified (check file length and modification time). // has been modified (check file length and modification time).
if (!destinationFile.exists() || if (!destinationFile.exists() ||
destinationFile.length() != file.length() || destinationFile.length() != file.length() ||
destinationFile.lastModified() != file.lastModified()) { destinationFile.lastModified() != file.lastModified()
) {
// Copy the file to the app-specific plugin directory // Copy the file to the app-specific plugin directory
file.copyTo(destinationFile, overwrite = true) file.copyTo(destinationFile, overwrite = true)
@ -502,14 +567,19 @@ object PluginManager {
afterPluginsLoadedEvent.invoke(forceReload) afterPluginsLoadedEvent.invoke(forceReload)
} }
/** @return true if safe mode is enabled in any possible way. */
fun isSafeMode(): Boolean {
return checkSafeModeFile() || lastError != null
}
/** /**
* This can be used to override any extension loading to fix crashes! * This can be used to override any extension loading to fix crashes!
* @return true if safe mode file is present * @return true if safe mode file is present
**/ **/
fun checkSafeModeFile(): Boolean { fun checkSafeModeFile(): Boolean {
return normalSafeApiCall { return safe {
val folder = File(CLOUD_STREAM_FOLDER) val folder = File(CLOUD_STREAM_FOLDER)
if (!folder.exists()) return@normalSafeApiCall false if (!folder.exists()) return@safe false
val files = folder.listFiles { _, name -> val files = folder.listFiles { _, name ->
name.equals("safe", ignoreCase = true) name.equals("safe", ignoreCase = true)
} }
@ -546,7 +616,7 @@ object PluginManager {
return false return false
} }
InputStreamReader(stream).use { reader -> InputStreamReader(stream).use { reader ->
manifest = parseJson(reader, BasePlugin.Manifest::class.java) manifest = parseJson<BasePlugin.Manifest>(reader.readText())
} }
} }
@ -587,9 +657,15 @@ object PluginManager {
context.resources.configuration context.resources.configuration
) )
} }
synchronized(plugins) {
plugins[filePath] = pluginInstance plugins[filePath] = pluginInstance
}
synchronized(classLoaders) {
classLoaders[loader] = pluginInstance classLoaders[loader] = pluginInstance
}
synchronized(urlPlugins) {
urlPlugins[data.url ?: filePath] = pluginInstance urlPlugins[data.url ?: filePath] = pluginInstance
}
if (pluginInstance is Plugin) { if (pluginInstance is Plugin) {
pluginInstance.load(context) pluginInstance.load(context)
} else { } else {
@ -601,7 +677,7 @@ object PluginManager {
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}") Log.e(TAG, "Failed to load $file: ${Log.getStackTraceString(e)}")
showToast( showToast(
context.getActivity(), // context.getActivity(), // we are not always on the main thread
context.getString(R.string.plugin_load_fail).format(fileName), context.getString(R.string.plugin_load_fail).format(fileName),
Toast.LENGTH_LONG Toast.LENGTH_LONG
) )
@ -625,26 +701,34 @@ object PluginManager {
} }
// remove all registered apis // remove all registered apis
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach { APIHolder.apis.filter { api -> api.sourcePlugin == plugin.filename }.forEach {
removePluginMapping(it) removePluginMapping(it)
} }
}
synchronized(APIHolder.allProviders) { APIHolder.allProviders.withLock {
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.filename } APIHolder.allProviders.removeAll { provider -> provider.sourcePlugin == plugin.filename }
} }
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.filename } extractorApis.withLock {
extractorApis.removeAll { provider -> provider.sourcePlugin == plugin.filename }
synchronized(VideoClickActionHolder.allVideoClickActions) {
VideoClickActionHolder.allVideoClickActions.removeIf { action: VideoClickAction -> action.sourcePlugin == plugin.filename }
} }
VideoClickActionHolder.allVideoClickActions.withLock {
VideoClickActionHolder.allVideoClickActions.removeAll { action -> action.sourcePlugin == plugin.filename }
}
synchronized(classLoaders) {
classLoaders.values.removeIf { v -> v == plugin } classLoaders.values.removeIf { v -> v == plugin }
}
synchronized(plugins) {
plugins.remove(absolutePath) plugins.remove(absolutePath)
}
synchronized(urlPlugins) {
urlPlugins.values.removeIf { v -> v == plugin } urlPlugins.values.removeIf { v -> v == plugin }
} }
}
/** /**
* Spits out a unique and safe filename based on name. * Spits out a unique and safe filename based on name.
@ -673,25 +757,27 @@ object PluginManager {
suspend fun downloadPlugin( suspend fun downloadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
pluginHash: String?,
internalName: String, internalName: String,
repositoryUrl: String, repositoryUrl: String,
loadPlugin: Boolean loadPlugin: Boolean
): Boolean { ): Boolean {
val file = getPluginPath(activity, internalName, repositoryUrl) val file = getPluginPath(activity, internalName, repositoryUrl)
return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin) return downloadPlugin(activity, pluginUrl, pluginHash, internalName, file, loadPlugin)
} }
suspend fun downloadPlugin( suspend fun downloadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
pluginHash: String?,
internalName: String, internalName: String,
file: File, file: File,
loadPlugin: Boolean loadPlugin: Boolean,
): Boolean { ): Boolean {
try { try {
Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}") Log.d(TAG, "Downloading plugin: $pluginUrl to ${file.absolutePath}")
// The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names // The plugin file needs to be salted with the repository url hash as to allow multiple repositories with the same internal plugin names
val newFile = downloadPluginToFile(pluginUrl, file) ?: return false val newFile = downloadPluginToFile(activity, pluginUrl, file, pluginHash) ?: return false
val data = PluginData( val data = PluginData(
internalName, internalName,
@ -734,6 +820,84 @@ object PluginManager {
} }
} }
/**
* DO NOT USE THIS IN A PLUGIN! It may case an infinite recursive loop lagging or crashing everyone's devices.
* If you use it from a plugin, do not expect a stable jvmName, SO DO NOT USE IT!
*/
@Suppress("FunctionName")
@InternalAPI
@Throws
suspend fun ___DO_NOT_CALL_FROM_A_PLUGIN_manuallyReloadAndUpdatePlugins(activity: Activity) {
assertNonRecursiveCallstack()
showToast(activity.getString(R.string.starting_plugin_update_manually), Toast.LENGTH_LONG)
___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(activity)
afterPluginsLoadedEvent.invoke(false)
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().amap {
getRepoPlugins(it) ?: emptyList()
}.flatten().distinctBy { it.plugin.url }
val allPlugins = getPluginsOnline().flatMap { savedData ->
onlinePlugins
.filter { it.plugin.internalName == savedData.internalName }
.mapNotNull { onlineData ->
OnlinePluginData(savedData, onlineData).takeIf { it.validOnlineData(activity) }
}
}.distinctBy { it.onlineData.plugin.url }
val updatedPlugins = mutableListOf<String>()
allPlugins.amap { pluginData ->
if (pluginData.isDisabled) {
Log.e(
"PluginManager",
"Unloading disabled plugin: ${pluginData.onlineData.plugin.name}"
)
unloadPlugin(pluginData.savedData.filePath)
} else {
val existingFile = File(pluginData.savedData.filePath)
if (existingFile.exists()) existingFile.delete()
if (downloadPlugin(
activity,
pluginData.onlineData.plugin.url,
pluginData.onlineData.plugin.fileHash,
pluginData.savedData.internalName,
existingFile,
true
)
) {
updatedPlugins.add(pluginData.onlineData.plugin.name)
}
}
}.also {
main {
val message = if (updatedPlugins.isNotEmpty()) {
activity.getString(R.string.plugins_updated_manually, updatedPlugins.size)
} else {
activity.getString(R.string.no_plugins_updated_manually)
}
showToast(message, Toast.LENGTH_LONG)
val notificationText = UiText.StringResource(
R.string.plugins_updated_manually,
listOf(updatedPlugins.size)
)
createNotification(activity, notificationText, updatedPlugins)
}
}
loadedOnlinePlugins = true
afterPluginsLoadedEvent.invoke(false)
Log.i("PluginManager", "Plugin update done!")
}
private fun Context.createNotificationChannel() { private fun Context.createNotificationChannel() {
hasCreatedNotChanel = true hasCreatedNotChanel = true
// Create the NotificationChannel, but only on API 26+ because // Create the NotificationChannel, but only on API 26+ because

View file

@ -1,37 +1,42 @@
package com.lagradost.cloudstream3.plugins package com.lagradost.cloudstream3.plugins
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.safeAsync
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName import com.lagradost.cloudstream3.plugins.PluginManager.getPluginSanitizedFileName
import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin import com.lagradost.cloudstream3.plugins.PluginManager.unloadPlugin
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.io.BufferedInputStream import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.File import java.io.File
import java.io.InputStream import java.nio.file.AtomicMoveNotSupportedException
import java.io.OutputStream import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.util.concurrent.TimeUnit
/** /**
* Comes with the app, always available in the app, non removable. * Comes with the app, always available in the app, non removable.
* */ */
@Serializable
data class Repository( data class Repository(
@JsonProperty("name") val name: String, @JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?,
@JsonProperty("description") val description: String?, @JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("manifestVersion") val manifestVersion: Int, @JsonProperty("description") @SerialName("description") val description: String?,
@JsonProperty("pluginLists") val pluginLists: List<String> @JsonProperty("manifestVersion") @SerialName("manifestVersion") val manifestVersion: Int,
@JsonProperty("pluginLists") @SerialName("pluginLists") val pluginLists: List<String>,
) )
/** /**
@ -40,40 +45,81 @@ data class Repository(
* 1: Ok * 1: Ok
* 2: Slow * 2: Slow
* 3: Beta only * 3: Beta only
* */ */
@Serializable
data class SitePlugin( data class SitePlugin(
// Url to the .cs3 file // Url to the .cs3 file
@JsonProperty("url") val url: String, @JsonProperty("url") @SerialName("url") val url: String,
// Status to remotely disable the provider // Status to remotely disable the provider
@JsonProperty("status") val status: Int, @JsonProperty("status") @SerialName("status") val status: Int,
// Integer over 0, any change of this will trigger an auto update // Integer over 0, any change of this will trigger an auto update
@JsonProperty("version") val version: Int, @JsonProperty("version") @SerialName("version") val version: Int,
// Unused currently, used to make the api backwards compatible? // Unused currently, used to make the api backwards compatible?
// Set to 1 // Set to 1
@JsonProperty("apiVersion") val apiVersion: Int, @JsonProperty("apiVersion") @SerialName("apiVersion") val apiVersion: Int,
// Name to be shown in app // Name to be shown in app
@JsonProperty("name") val name: String, @JsonProperty("name") @SerialName("name") val name: String,
// Name to be referenced internally. Separate to make name and url changes possible // Name to be referenced internally. Separate to make name and url changes possible
@JsonProperty("internalName") val internalName: String, @JsonProperty("internalName") @SerialName("internalName") val internalName: String,
@JsonProperty("authors") val authors: List<String>, @JsonProperty("authors") @SerialName("authors") val authors: List<String>,
@JsonProperty("description") val description: String?, @JsonProperty("description") @SerialName("description") val description: String?,
// Might be used to go directly to the plugin repo in the future // Might be used to go directly to the plugin repo in the future
@JsonProperty("repositoryUrl") val repositoryUrl: String?, @JsonProperty("repositoryUrl") @SerialName("repositoryUrl") val repositoryUrl: String?,
// These types are yet to be mapped and used, ignore for now // These types are yet to be mapped and used, ignore for now
@JsonProperty("tvTypes") val tvTypes: List<String>?, @JsonProperty("tvTypes") @SerialName("tvTypes") val tvTypes: List<String>?,
@JsonProperty("language") val language: String?, // Most often a language tag like "en" or "zh-TW"
@JsonProperty("iconUrl") val iconUrl: String?, @JsonProperty("language") @SerialName("language") val language: String?,
@JsonProperty("iconUrl") @SerialName("iconUrl") val iconUrl: String?,
// Automatically generated by the gradle plugin // Automatically generated by the gradle plugin
@JsonProperty("fileSize") val fileSize: Long?, @JsonProperty("fileSize") @SerialName("fileSize") val fileSize: Long?,
@JsonProperty("fileHash") @SerialName("fileHash") val fileHash: String?,
) )
@Serializable
data class PluginWrapper(
@JsonProperty("repository") @SerialName("repository") val repository: Repository,
@JsonProperty("repositoryData") @SerialName("repositoryData") val repositoryData: RepositoryData,
@JsonProperty("plugin") @SerialName("plugin") val plugin: SitePlugin
) {
companion object {
private val localRepository = Repository("", "", "", 1, emptyList())
private val localRepositoryData = RepositoryData("", "", "")
fun getLocalPluginWrapper(plugin: SitePlugin): PluginWrapper {
return PluginWrapper(
localRepository,
localRepositoryData,
plugin
)
}
}
}
object RepositoryManager { object RepositoryManager {
const val ONLINE_PLUGINS_FOLDER = "Extensions" const val ONLINE_PLUGINS_FOLDER = "Extensions"
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy { val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray() getKey<Array<RepositoryData>>("PREBUILT_REPOSITORIES") ?: emptyArray()
}
private val GH_REGEX =
Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/** Returns a SHA-256 string of the file content.
* Example: "sha256-b70462c264cb7f90fc2860a8e58d7544ce747ff347d1d11fa093623901853573" **/
@WorkerThread
fun sha256(file: File): String {
val digest = MessageDigest.getInstance("SHA-256")
file.inputStream().use { fis ->
val buffer = ByteArray(8192)
var read = fis.read(buffer)
while (read != -1) {
digest.update(buffer, 0, read)
read = fis.read(buffer)
}
}
return "sha256-" + digest.digest().joinToString("") { "%02x".format(it) }
} }
private val GH_REGEX = Regex("^https://raw.githubusercontent.com/([A-Za-z0-9-]+)/([A-Za-z0-9_.-]+)/(.*)$")
/* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */ /* Convert raw.githubusercontent.com urls to cdn.jsdelivr.net if enabled in settings */
fun convertRawGitUrl(url: String): String { fun convertRawGitUrl(url: String): String {
@ -94,32 +140,37 @@ object RepositoryManager {
else fixedUrl else fixedUrl
} }
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) { } else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
suspendSafeApiCall { safeAsync {
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 -> if (fixedUrl.startsWith("!")) {
it2.headers["Location"]?.let { url -> val response = app.get("https://py.md/${fixedUrl.removePrefix("!")}", allowRedirects = false)
if (url.startsWith("https://cutt.ly/404")) return@suspendSafeApiCall null val url = response.headers["Location"] ?: return@safeAsync null
if (url.removeSuffix("/") == "https://cutt.ly") return@suspendSafeApiCall null if (url.startsWith("https://py.md/404")) return@safeAsync null
return@suspendSafeApiCall url if (url.removeSuffix("/") == "https://py.md") return@safeAsync null
} return@safeAsync url
} else {
val response = app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false)
val url = response.headers["Location"] ?: return@safeAsync null
if (url.startsWith("https://cutt.ly/404")) return@safeAsync null
if (url.removeSuffix("/") == "https://cutt.ly") return@safeAsync null
return@safeAsync url
} }
} }
} else null } else null
} }
suspend fun parseRepository(url: String): Repository? { suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall { return safeAsync {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
app.get(convertRawGitUrl(url)).parsedSafe() app.get(convertRawGitUrl(url), cacheTime = 5, cacheUnit = TimeUnit.MINUTES)
.parsedSafe<Repository>()
} }
} }
private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> { private suspend fun parsePlugins(pluginUrls: String): List<SitePlugin> {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
return try { return try {
val response = app.get(convertRawGitUrl(pluginUrls)) app.get(convertRawGitUrl(pluginUrls), cacheTime = 5, cacheUnit = TimeUnit.MINUTES)
// Normal parsed function not working? .parsed<Array<SitePlugin>>().toList()
// return response.parsedSafe()
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
emptyList() emptyList()
@ -128,37 +179,68 @@ object RepositoryManager {
/** /**
* Gets all plugins from repositories and pairs them with the repository url * Gets all plugins from repositories and pairs them with the repository url
* */ */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? { suspend fun getRepoPlugins(repositoryData: RepositoryData): List<PluginWrapper>? {
val repo = parseRepository(repositoryUrl) ?: return null val repo = parseRepository(repositoryData.url) ?: return null
return repo.pluginLists.amap { url -> val list = repo.pluginLists.amap { url ->
parsePlugins(url).map { parsePlugins(url).map {
repositoryUrl to it PluginWrapper(repo, repositoryData, it)
} }
}.flatten() }.flatten()
return list
} }
suspend fun downloadPluginToFile( suspend fun downloadPluginToFile(
context: Context,
pluginUrl: String, pluginUrl: String,
file: File file: File,
expectedFileHash: String?
): File? { ): File? {
return suspendSafeApiCall { return safeAsync {
file.mkdirs() val parentDir = file.parentFile ?: return@safeAsync null
parentDir.mkdirs()
// Overwrite if exists // Prevent corrupting the plugin file if the operation fails
if (file.exists()) { val tempFile = File.createTempFile(file.name, ".tmp", context.cacheDir)
file.delete()
}
file.createNewFile()
val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body
write(body.byteStream(), file.outputStream())
body.byteStream().use { body ->
tempFile.outputStream().use { fileSteam ->
body.copyTo(fileSteam)
}
}
if (expectedFileHash != null) {
val downloadHash = sha256(tempFile)
if (expectedFileHash != downloadHash) {
tempFile.delete()
throw IllegalStateException("Extension hash mismatch when validating '${file.name}'! Expected: '$expectedFileHash', got: '$downloadHash'.")
}
}
// We prefer the operation to be atomic
try {
Files.move(
tempFile.toPath(),
file.toPath(),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE
)
} catch (_: AtomicMoveNotSupportedException) {
Files.move(
tempFile.toPath(),
file.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
}
file file
} }
} }
fun getRepositories(): Array<RepositoryData> { fun getRepositories(): Array<RepositoryData> {
return getKey(REPOSITORIES_KEY) ?: emptyArray() return getKey<Array<RepositoryData>>(REPOSITORIES_KEY) ?: emptyArray()
} }
// Don't want to read before we write in another thread // Don't want to read before we write in another thread
@ -173,7 +255,7 @@ object RepositoryManager {
/** /**
* Also deletes downloaded repository plugins * Also deletes downloaded repository plugins
* */ */
suspend fun removeRepository(context: Context, repository: RepositoryData) { suspend fun removeRepository(context: Context, repository: RepositoryData) {
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER) val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
@ -191,7 +273,7 @@ object RepositoryManager {
// Unload all plugins, not using deletePlugin since we // Unload all plugins, not using deletePlugin since we
// delete all data and files in deleteRepositoryData // delete all data and files in deleteRepositoryData
normalSafeApiCall { safe {
file.listFiles { plugin: File -> file.listFiles { plugin: File ->
unloadPlugin(plugin.absolutePath) unloadPlugin(plugin.absolutePath)
false false
@ -200,13 +282,4 @@ object RepositoryManager {
PluginManager.deleteRepositoryData(file.absolutePath) PluginManager.deleteRepositoryData(file.absolutePath)
} }
private fun write(stream: InputStream, output: OutputStream) {
val input = BufferedInputStream(stream)
val dataBuffer = ByteArray(512)
var readBytes: Int
while (input.read(dataBuffer).also { readBytes = it } != -1) {
output.write(dataBuffer, 0, readBytes)
}
}
} }

View file

@ -2,62 +2,49 @@ package com.lagradost.cloudstream3.plugins
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import java.security.MessageDigest import java.security.MessageDigest
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
object VotingApi { // please do not cheat the votes lol object VotingApi {
private const val LOGKEY = "VotingApi" private const val LOGKEY = "VotingApi"
private const val API_DOMAIN = "https://api.countify.xyz"
private const val API_DOMAIN = "https://counterapi.com/api" private fun transformUrl(url: String): String =
private fun transformUrl(url: String): String = // dont touch or all votes get reset
MessageDigest MessageDigest
.getInstance("SHA-256") .getInstance("SHA-256")
.digest("${url}#funny-salt".toByteArray()) .digest("${url}#funny-salt".toByteArray())
.fold("") { str, it -> str + "%02x".format(it) } .fold("") { str, it -> str + "%02x".format(it) }
suspend fun SitePlugin.getVotes(): Int { suspend fun SitePlugin.getVotes(): Int = getVotes(url)
return getVotes(url) fun SitePlugin.hasVoted(): Boolean = hasVoted(url)
} suspend fun SitePlugin.vote(): Int = vote(url)
fun SitePlugin.canVote(): Boolean = canVote(this.url)
fun SitePlugin.hasVoted(): Boolean {
return hasVoted(url)
}
suspend fun SitePlugin.vote(): Int {
return vote(url)
}
fun SitePlugin.canVote(): Boolean {
return canVote(this.url)
}
// Plugin url to Int
private val votesCache = mutableMapOf<String, Int>() private val votesCache = mutableMapOf<String, Int>()
private fun getRepository(pluginUrl: String) = pluginUrl
.split("/")
.drop(2)
.take(3)
.joinToString("-")
private suspend fun readVote(pluginUrl: String): Int { private suspend fun readVote(pluginUrl: String): Int {
val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}?readOnly=true" val id = transformUrl(pluginUrl)
Log.d(LOGKEY, "Requesting: $url") val url = "$API_DOMAIN/get-total/$id"
return app.get(url).parsedSafe<Result>()?.value ?: 0 Log.d(LOGKEY, "Requesting GET: $url")
return app.get(url).parsedSafe<CountifyResult>()?.count ?: 0
} }
private suspend fun writeVote(pluginUrl: String): Boolean { private suspend fun writeVote(pluginUrl: String): Boolean {
val url = "${API_DOMAIN}/cs-${getRepository(pluginUrl)}/vote/${transformUrl(pluginUrl)}" val id = transformUrl(pluginUrl)
Log.d(LOGKEY, "Requesting: $url") val url = "$API_DOMAIN/increment/$id"
return app.get(url).parsedSafe<Result>()?.value != null Log.d(LOGKEY, "Requesting POST: $url")
return app.post(url, emptyMap<String, String>())
.parsedSafe<CountifyResult>()?.count != null
} }
suspend fun getVotes(pluginUrl: String): Int = suspend fun getVotes(pluginUrl: String): Int =
@ -65,34 +52,38 @@ object VotingApi { // please do not cheat the votes lol
votesCache[pluginUrl] = it votesCache[pluginUrl] = it
} }
fun hasVoted(pluginUrl: String) = fun hasVoted(pluginUrl: String): Boolean =
getKey("cs3-votes/${transformUrl(pluginUrl)}") ?: false getKey<Boolean>("cs3-votes/${transformUrl(pluginUrl)}") ?: false
fun canVote(pluginUrl: String): Boolean { fun canVote(pluginUrl: String): Boolean =
return PluginManager.urlPlugins.contains(pluginUrl) PluginManager.urlPlugins.contains(pluginUrl)
}
private val voteLock = Mutex() private val voteLock = Mutex()
suspend fun vote(pluginUrl: String): Int { suspend fun vote(pluginUrl: String): Int {
// Prevent multiple requests at the same time.
voteLock.withLock { voteLock.withLock {
if (!canVote(pluginUrl)) { if (!canVote(pluginUrl)) {
main { main {
Toast.makeText(context, R.string.extension_install_first, Toast.LENGTH_SHORT) Toast.makeText(
.show() context,
R.string.extension_install_first,
Toast.LENGTH_SHORT
).show()
} }
return getVotes(pluginUrl) return getVotes(pluginUrl)
} }
if (hasVoted(pluginUrl)) { if (hasVoted(pluginUrl)) {
main { main {
Toast.makeText(context, R.string.already_voted, Toast.LENGTH_SHORT) Toast.makeText(
.show() context,
R.string.already_voted,
Toast.LENGTH_SHORT
).show()
} }
return getVotes(pluginUrl) return getVotes(pluginUrl)
} }
if (writeVote(pluginUrl)) { if (writeVote(pluginUrl)) {
setKey("cs3-votes/${transformUrl(pluginUrl)}", true) setKey("cs3-votes/${transformUrl(pluginUrl)}", true)
votesCache[pluginUrl] = votesCache[pluginUrl]?.plus(1) ?: 1 votesCache[pluginUrl] = votesCache[pluginUrl]?.plus(1) ?: 1
@ -102,7 +93,9 @@ object VotingApi { // please do not cheat the votes lol
} }
} }
private data class Result( @Serializable
val value: Int? private data class CountifyResult(
@JsonProperty("id") @SerialName("id") val id: String? = null,
@JsonProperty("count") @SerialName("count") val count: Int? = null,
) )
} }

View file

@ -0,0 +1,279 @@
package com.lagradost.cloudstream3.services
import android.Manifest
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build.VERSION.SDK_INT
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
import com.lagradost.cloudstream3.MainActivity.Companion.setLastError
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.debugWarning
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.utils.AppContextUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESUME_IN_QUEUE
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.KEY_RESUME_PACKAGES
import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.downloadEvent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.flow.updateAndGet
import kotlinx.coroutines.withTimeoutOrNull
import kotlin.system.measureTimeMillis
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class DownloadQueueService : Service() {
companion object {
const val TAG = "DownloadQueueService"
const val DOWNLOAD_QUEUE_CHANNEL_ID = "cloudstream3.download.queue"
const val DOWNLOAD_QUEUE_CHANNEL_NAME = "Download queue service"
const val DOWNLOAD_QUEUE_CHANNEL_DESCRIPTION = "App download queue notification."
const val DOWNLOAD_QUEUE_NOTIFICATION_ID = 917194232 // Random unique
@Volatile
var isRunning = false
fun getIntent(
context: Context,
): Intent {
return Intent(context, DownloadQueueService::class.java)
}
private val _downloadInstances: MutableStateFlow<List<VideoDownloadManager.EpisodeDownloadInstance>> =
MutableStateFlow(emptyList())
/** Flow of all active downloads, not queued. May temporarily contain completed or failed EpisodeDownloadInstances.
* Completed or failed instances are automatically removed by the download queue service.
*
*/
val downloadInstances: StateFlow<List<VideoDownloadManager.EpisodeDownloadInstance>> =
_downloadInstances
private val totalDownloadFlow =
downloadInstances.combine(DownloadQueueManager.queue) { instances, queue ->
instances to queue
}
.combine(VideoDownloadManager.currentDownloads) { (instances, queue), currentDownloads ->
Triple(instances, queue, currentDownloads)
}
}
private val baseNotification by lazy {
val intent = Intent(this, MainActivity::class.java)
val pendingIntent =
PendingIntentCompat.getActivity(this, 0, intent, 0, false)
val activeDownloads = resources.getQuantityString(R.plurals.downloads_active, 0).format(0)
val activeQueue = resources.getQuantityString(R.plurals.downloads_queued, 0).format(0)
NotificationCompat.Builder(this, DOWNLOAD_QUEUE_CHANNEL_ID)
.setOngoing(true) // Make it persistent
.setAutoCancel(false)
.setColorized(false)
.setOnlyAlertOnce(true)
.setSilent(true)
.setShowWhen(false)
// If low priority then the notification might not show :(
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(this.colorFromAttribute(R.attr.colorPrimary))
.setContentText(activeDownloads)
.setSubText(activeQueue)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.download_icon_load)
}
private fun updateNotification(context: Context, downloads: Int, queued: Int) {
if (context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED
) return
val activeDownloads =
resources.getQuantityString(R.plurals.downloads_active, downloads).format(downloads)
val activeQueue =
resources.getQuantityString(R.plurals.downloads_queued, queued).format(queued)
val newNotification = baseNotification
.setContentText(activeDownloads)
.setSubText(activeQueue)
.build()
safe {
NotificationManagerCompat.from(context)
.notify(DOWNLOAD_QUEUE_NOTIFICATION_ID, newNotification)
}
}
// We always need to listen to events, even before the download is launched.
// Stopping link loading is an event which can trigger before downloading.
val downloadEventListener = { event: Pair<Int, VideoDownloadManager.DownloadActionType> ->
when (event.second) {
VideoDownloadManager.DownloadActionType.Stop -> {
removeKey(KEY_RESUME_PACKAGES, event.first.toString())
removeKey(KEY_RESUME_IN_QUEUE, event.first.toString())
DownloadQueueManager.cancelDownload(event.first)
}
else -> {}
}
}
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
override fun onCreate() {
isRunning = true
val context: Context = this // To make code more readable
Log.d(TAG, "Download queue service started.")
this.createNotificationChannel(
DOWNLOAD_QUEUE_CHANNEL_ID,
DOWNLOAD_QUEUE_CHANNEL_NAME,
DOWNLOAD_QUEUE_CHANNEL_DESCRIPTION
)
if (SDK_INT >= 29) {
startForeground(
DOWNLOAD_QUEUE_NOTIFICATION_ID,
baseNotification.build(),
FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(DOWNLOAD_QUEUE_NOTIFICATION_ID, baseNotification.build())
}
downloadEvent += downloadEventListener
val queueJob = ioSafe {
// Ensure this is up to date to prevent race conditions with MainActivity launches
setLastError(context)
// Early return, to prevent waiting for plugins in safe mode
if (lastError != null) return@ioSafe
// Try to ensure all plugins are loaded before starting the downloader.
// To prevent infinite stalls we use a timeout of 15 seconds, it is judged as long enough
val timeout = 15.seconds
val timeTaken = withTimeoutOrNull(timeout) {
measureTimeMillis {
while (!(PluginManager.loadedOnlinePlugins && PluginManager.loadedLocalPlugins)) {
delay(100.milliseconds)
}
}
}
debugWarning({ timeTaken == null || timeTaken > 3_000 }, {
"Abnormally long downloader startup time of: ${timeTaken ?: timeout.inWholeMilliseconds}ms"
})
debugAssert({ timeTaken == null }, { "Downloader startup should not time out" })
totalDownloadFlow
.debounce { (instances, queue) ->
// Filter away incorrect transient queue states.
// For example when we pop the queue and add a download instance there exists a transient state where
// there is no queue and no download instances (leading to an early exit)
if (instances.isEmpty() && queue.isEmpty()) {
500.milliseconds
} else {
0.milliseconds
}
}
.takeWhile { (instances, queue) ->
// Stop if destroyed
isRunning
// Run as long as there is a queue to process
&& (instances.isNotEmpty() || queue.isNotEmpty())
// Run as long as there are no app crashes
&& lastError == null
}
.collect { (_, queue, currentDownloads) ->
// Remove completed or failed
val newInstances = _downloadInstances.updateAndGet { currentInstances ->
currentInstances.filterNot { it.isCompleted || it.isFailed || it.isCancelled }
}
val maxDownloads = VideoDownloadManager.maxConcurrentDownloads(context)
val currentInstanceCount = newInstances.size
val newDownloads = minOf(
// Cannot exceed the max downloads
maxOf(0, maxDownloads - currentInstanceCount),
// Cannot start more downloads than the queue size
queue.size
)
// Cant start multiple downloads at once. If this is rerun it may start too many downloads.
if (newDownloads > 0) {
_downloadInstances.update { instances ->
val downloadInstance = DownloadQueueManager.popQueue(context)
if (downloadInstance != null) {
downloadInstance.startDownload()
instances + downloadInstance
} else {
instances
}
}
}
// The downloads actually displayed to the user with a notification
val currentVisualDownloads =
currentDownloads.size + newInstances.count {
currentDownloads.contains(it.downloadQueueWrapper.id)
.not()
}
// Just the queue
val currentVisualQueue = queue.size
updateNotification(context, currentVisualDownloads, currentVisualQueue)
}
}
// Stop self regardless of job outcome
queueJob.invokeOnCompletion { throwable ->
if (throwable != null) {
logError(throwable)
}
safe {
stopSelf()
}
}
}
override fun onDestroy() {
Log.d(TAG, "Download queue service stopped.")
downloadEvent -= downloadEventListener
isRunning = false
super.onDestroy()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY // We want the service restarted if its killed
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onTimeout(reason: Int) {
stopSelf()
Log.e(TAG, "Service stopped due to timeout: $reason")
}
}

View file

@ -22,7 +22,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl import com.lagradost.cloudstream3.utils.downloader.DownloadUtils.getImageBitmapFromUrl
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -128,18 +128,18 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
updateProgress(max, progress, true) updateProgress(max, progress, true)
// We need all plugins loaded. // We need all plugins loaded.
PluginManager.loadAllOnlinePlugins(context) PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllOnlinePlugins(context)
PluginManager.loadAllLocalPlugins(context, false) PluginManager.___DO_NOT_CALL_FROM_A_PLUGIN_loadAllLocalPlugins(context, false)
subscriptions.apmap { savedData -> subscriptions.amap { savedData ->
try { try {
val id = savedData.id ?: return@apmap null val id = savedData.id ?: return@amap null
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null val api = getApiFromNameNull(savedData.apiName) ?: return@amap null
// Reasonable timeout to prevent having this worker run forever. // Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) { val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse api.load(savedData.url) as? EpisodeResponse
} ?: return@apmap null } ?: return@amap null
val dubPreference = val dubPreference =
getDub(id) ?: if ( getDub(id) ?: if (

View file

@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.services
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
/** Handle notification actions such as pause/resume downloads */
class VideoDownloadService : Service() { class VideoDownloadService : Service() {
private val downloadScope = CoroutineScope(Dispatchers.Default) private val downloadScope = CoroutineScope(Dispatchers.Default)
@ -42,19 +43,3 @@ class VideoDownloadService : Service() {
super.onDestroy() super.onDestroy()
} }
} }
// override fun onHandleIntent(intent: Intent?) {
// if (intent != null) {
// val id = intent.getIntExtra("id", -1)
// val type = intent.getStringExtra("type")
// if (id != -1 && type != null) {
// val state = when (type) {
// "resume" -> VideoDownloadManager.DownloadActionType.Resume
// "pause" -> VideoDownloadManager.DownloadActionType.Pause
// "stop" -> VideoDownloadManager.DownloadActionType.Stop
// else -> return
// }
// VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
// }
// }
// }
//}

View file

@ -1,18 +1,9 @@
package com.lagradost.cloudstream3.subtitles package com.lagradost.cloudstream3.subtitles
import androidx.annotation.WorkerThread
import androidx.core.net.toUri import androidx.core.net.toUri
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.ui.player.SubtitleOrigin import com.lagradost.cloudstream3.ui.player.SubtitleOrigin
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import okio.BufferedSource import okio.BufferedSource
import okio.buffer import okio.buffer
import okio.sink import okio.sink
@ -20,116 +11,6 @@ import okio.source
import java.io.File import java.io.File
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
interface AbstractSubProvider {
val idPrefix: String
@WorkerThread
@Throws
suspend fun search(query: SubtitleSearch): List<SubtitleEntity>? {
throw NotImplementedError()
}
@WorkerThread
@Throws
suspend fun load(data: SubtitleEntity): String? {
throw NotImplementedError()
}
@WorkerThread
@Throws
suspend fun SubtitleResource.getResources(data: SubtitleEntity) {
this.addUrl(load(data))
}
@WorkerThread
@Throws
suspend fun getResource(data: SubtitleEntity): SubtitleResource {
return SubtitleResource().apply {
this.getResources(data)
}
}
}
class SubRepository(val api: AbstractSubProvider) {
companion object {
data class SavedSearchResponse(
val unixTime: Long,
val response: List<SubtitleEntity>,
val query: SubtitleSearch
)
data class SavedResourceResponse(
val unixTime: Long,
val response: SubtitleResource,
val query: SubtitleEntity
)
// maybe make this a generic struct? right now there is a lot of boilerplate
private val searchCache = threadSafeListOf<SavedSearchResponse>()
private var searchCacheIndex: Int = 0
private val resourceCache = threadSafeListOf<SavedResourceResponse>()
private var resourceCacheIndex: Int = 0
const val CACHE_SIZE = 20
}
val idPrefix: String get() = api.idPrefix
@WorkerThread
suspend fun getResource(data: SubtitleEntity): Resource<SubtitleResource> = safeApiCall {
synchronized(resourceCache) {
for (item in resourceCache) {
// 20 min save
if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
return@safeApiCall item.response
}
}
}
val returnValue = api.getResource(data)
synchronized(resourceCache) {
val add = SavedResourceResponse(unixTime, returnValue, data)
if (resourceCache.size > CACHE_SIZE) {
resourceCache[resourceCacheIndex] = add // rolling cache
resourceCacheIndex = (resourceCacheIndex + 1) % CACHE_SIZE
} else {
resourceCache.add(add)
}
}
returnValue
}
@WorkerThread
suspend fun search(query: SubtitleSearch): Resource<List<SubtitleEntity>> {
return safeApiCall {
synchronized(searchCache) {
for (item in searchCache) {
// 120 min save
if (item.query == query && (unixTime - item.unixTime) < 60 * 120) {
return@safeApiCall item.response
}
}
}
val returnValue = api.search(query) ?: throw ErrorLoadingException("Null subtitles")
// only cache valid return values
if (returnValue.isNotEmpty()) {
val add = SavedSearchResponse(unixTime, returnValue, query)
synchronized(searchCache) {
if (searchCache.size > CACHE_SIZE) {
searchCache[searchCacheIndex] = add // rolling cache
searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE
} else {
searchCache.add(add)
}
}
}
returnValue
}
}
}
/** /**
* A builder for subtitle files. * A builder for subtitle files.
* @see addUrl * @see addUrl
@ -210,4 +91,3 @@ class SubtitleResource {
} }
} }
interface AbstractSubApi : AbstractSubProvider, AuthAPI

View file

@ -1,59 +1,135 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.syncproviders.providers.* import com.lagradost.cloudstream3.syncproviders.providers.Addic7ed
import com.lagradost.cloudstream3.syncproviders.providers.AniListApi
import com.lagradost.cloudstream3.syncproviders.providers.KitsuApi
import com.lagradost.cloudstream3.syncproviders.providers.LocalList
import com.lagradost.cloudstream3.syncproviders.providers.MALApi
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.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.videoskip.AnimeSkipAuth
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
abstract class AccountManager(private val defIndex: Int) : AuthAPI { abstract class AccountManager {
companion object { companion object {
val malApi = MALApi(0).also { api -> const val NONE_ID: Int = -1
LoadResponse.Companion.malIdPrefix = api.idPrefix val malApi = MALApi()
} val kitsuApi = KitsuApi()
val aniListApi = AniListApi(0).also { api -> val aniListApi = AniListApi()
LoadResponse.Companion.aniListIdPrefix = api.idPrefix val simklApi = SimklApi()
}
val simklApi = SimklApi(0).also { api ->
LoadResponse.Companion.simklIdPrefix = api.idPrefix
}
val openSubtitlesApi = OpenSubtitlesApi(0)
val addic7ed = Addic7ed()
val subDlApi = SubDlApi(0)
val localListApi = LocalList() val localListApi = LocalList()
val openSubtitlesApi = OpenSubtitlesApi()
val addic7ed = Addic7ed()
val subDlApi = SubDlApi()
val subSourceApi = SubSourceApi() val subSourceApi = SubSourceApi()
val animeSkipApi = AnimeSkipAuth()
// used to login via app intent var cachedAccounts: MutableMap<String, Array<AuthData>>
val OAuth2Apis var cachedAccountIds: MutableMap<String, Int>
get() = listOf<OAuth2API>(
malApi, aniListApi, simklApi const val ACCOUNT_TOKEN = "auth_tokens"
const val ACCOUNT_IDS = "auth_ids"
fun accounts(prefix: String): Array<AuthData> {
require(prefix != "NONE")
return getKey<Array<AuthData>>(
ACCOUNT_TOKEN,
"${prefix}/${DataStoreHelper.currentAccount}"
) ?: arrayOf()
}
fun updateAccounts(prefix: String, array: Array<AuthData>) {
require(prefix != "NONE")
setKey(ACCOUNT_TOKEN, "${prefix}/${DataStoreHelper.currentAccount}", array)
synchronized(cachedAccounts) {
cachedAccounts[prefix] = array
}
}
fun updateAccountsId(prefix: String, id: Int) {
require(prefix != "NONE")
setKey(ACCOUNT_IDS, "${prefix}/${DataStoreHelper.currentAccount}", id)
synchronized(cachedAccountIds) {
cachedAccountIds[prefix] = id
}
}
val allApis = arrayOf(
SyncRepo(malApi),
SyncRepo(kitsuApi),
SyncRepo(aniListApi),
SyncRepo(simklApi),
SyncRepo(localListApi),
SubtitleRepo(openSubtitlesApi),
SubtitleRepo(addic7ed),
SubtitleRepo(subDlApi),
PlainAuthRepo(animeSkipApi),
SubtitleRepo(subSourceApi)
) )
// this needs init with context and can be accessed in settings fun updateAccountIds() {
val accountManagers val ids = mutableMapOf<String, Int>()
get() = listOf( for (api in allApis) {
malApi, aniListApi, openSubtitlesApi, subDlApi, simklApi //nginxApi ids.put(
api.idPrefix,
getKey<Int>(
ACCOUNT_IDS,
"${api.idPrefix}/${DataStoreHelper.currentAccount}",
NONE_ID
) ?: NONE_ID
) )
}
synchronized(cachedAccountIds) {
cachedAccountIds = ids
}
}
// used for active syncing init {
val SyncApis val data = mutableMapOf<String, Array<AuthData>>()
get() = listOf( val ids = mutableMapOf<String, Int>()
SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi), SyncRepo(simklApi) for (api in allApis) {
data.put(api.idPrefix, accounts(api.idPrefix))
ids.put(
api.idPrefix,
getKey<Int>(
ACCOUNT_IDS,
"${api.idPrefix}/${DataStoreHelper.currentAccount}",
NONE_ID
) ?: NONE_ID
) )
}
cachedAccounts = data
cachedAccountIds = ids
}
val inAppAuths // I do not want to place this in the init block as JVM initialization order is weird, and it may cause exceptions
get() = listOf<InAppAuthAPIManager>( // accessing other classes
openSubtitlesApi, fun initMainAPI() {
subDlApi LoadResponse.malIdPrefix = malApi.idPrefix
)//, nginxApi) LoadResponse.kitsuIdPrefix = kitsuApi.idPrefix
LoadResponse.aniListIdPrefix = aniListApi.idPrefix
LoadResponse.simklIdPrefix = simklApi.idPrefix
}
val subtitleProviders val subtitleProviders = arrayOf(
get() = listOf( SubtitleRepo(openSubtitlesApi),
openSubtitlesApi, SubtitleRepo(addic7ed),
addic7ed, SubtitleRepo(subDlApi),
subDlApi, SubtitleRepo(subSourceApi)
subSourceApi )
val syncApis = arrayOf(
SyncRepo(malApi),
SyncRepo(kitsuApi),
SyncRepo(aniListApi),
SyncRepo(simklApi),
SyncRepo(localListApi)
) )
const val APP_STRING = "cloudstreamapp" const val APP_STRING = "cloudstreamapp"
@ -66,12 +142,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// Instantly resume watching a show // Instantly resume watching a show
const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching" const val APP_STRING_RESUME_WATCHING = "cloudstreamcontinuewatching"
val unixTime: Long const val APP_STRING_SHARE = "csshare"
get() = System.currentTimeMillis() / 1000L
val unixTimeMs: Long
get() = System.currentTimeMillis()
const val MAX_STALE = 60 * 10
fun secondsToReadable(seconds: Int, completedValue: String): String { fun secondsToReadable(seconds: Int, completedValue: String): String {
var secondsLong = seconds.toLong() var secondsLong = seconds.toLong()
@ -93,57 +164,4 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m" return "${if (days != 0L) "$days" + "d " else ""}${if (hours != 0L) "$hours" + "h " else ""}${minutes}m"
} }
} }
var accountIndex = defIndex
private var lastAccountIndex = defIndex
protected val accountId get() = "${idPrefix}_account_$accountIndex"
private val accountActiveKey get() = "${idPrefix}_active"
// int array of all accounts indexes
private val accountsKey get() = "${idPrefix}_accounts"
protected fun removeAccountKeys() {
removeKeys(accountId)
val accounts = getAccounts()?.toMutableList() ?: mutableListOf()
accounts.remove(accountIndex)
setKey(accountsKey, accounts.toIntArray())
init()
}
fun getAccounts(): IntArray? {
return getKey(accountsKey, intArrayOf())
}
fun init() {
accountIndex = getKey(accountActiveKey, defIndex)!!
val accounts = getAccounts()
if (accounts?.isNotEmpty() == true && this.loginInfo() == null) {
accountIndex = accounts.first()
}
}
protected fun switchToNewAccount() {
val accounts = getAccounts()
lastAccountIndex = accountIndex
accountIndex = (accounts?.maxOrNull() ?: 0) + 1
}
protected fun switchToOldAccount() {
accountIndex = lastAccountIndex
}
protected fun registerAccount() {
setKey(accountActiveKey, accountIndex)
val accounts = getAccounts()?.toMutableList() ?: mutableListOf()
if (!accounts.contains(accountIndex)) {
accounts.add(accountIndex)
}
setKey(accountsKey, accounts.toIntArray())
}
fun changeAccount(index: Int) {
accountIndex = index
setKey(accountActiveKey, index)
}
} }

View file

@ -1,20 +1,262 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
interface AuthAPI { import com.fasterxml.jackson.annotation.JsonAlias
val name: String import com.fasterxml.jackson.annotation.JsonProperty
val icon: Int? import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.base64Encode
import com.lagradost.cloudstream3.splitUrlParameters
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.APP_STRING
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import java.security.SecureRandom
val requiresLogin: Boolean data class AuthLoginPage(
/** The website to open to authenticate */
val url: String,
/**
* State/control code to verify against the redirectUrl to make sure the request is valid.
* This parameter will be saved, and then used in AuthAPI::login.
*/
val payload: String? = null,
)
val createAccountUrl : String? @Serializable
data class AuthToken(
/**
* This is the general access tokens/api token representing a logged in user.
* Access tokens are the thing that applications use to make API requests on behalf of a user.
*/
@JsonProperty("accessToken") @SerialName("accessToken")
val accessToken: String? = null,
/** For OAuth a special refresh token is issues to refresh the access token. */
@JsonProperty("refreshToken") @SerialName("refreshToken")
val refreshToken: String? = null,
/** In UnixTime (sec) when it expires */
@JsonProperty("accessTokenLifetime") @SerialName("accessTokenLifetime")
val accessTokenLifetime: Long? = null,
/** In UnixTime (sec) when it expires */
@JsonProperty("refreshTokenLifetime") @SerialName("refreshTokenLifetime")
val refreshTokenLifetime: Long? = null,
/**
* Sometimes AuthToken needs to be customized to store e.g. username/password,
* this acts as a catch all to store text or JSON data.
*/
@JsonProperty("payload") @SerialName("payload")
val payload: String? = null,
) {
fun isAccessTokenExpired(marginSec: Long = 10L) =
accessTokenLifetime != null && unixTime + marginSec >= accessTokenLifetime
// don't change this as all keys depend on it fun isRefreshTokenExpired(marginSec: Long = 10L) =
val idPrefix: String refreshTokenLifetime != null && unixTime + marginSec >= refreshTokenLifetime
}
// if this returns null then you are not logged in @OptIn(ExperimentalSerializationApi::class) // JsonNames is an experimental annotation for now
fun loginInfo(): LoginInfo? @Serializable
fun logOut() data class AuthUser(
/** Account display-name, can also be email if name does not exist */
@JsonProperty("name") @SerialName("name")
val name: String?,
/**
* Unique account identifier. If a subsequent login is done then it
* will be refused if another account with the same id exists.
*/
@JsonProperty("id") @SerialName("id")
val id: Int,
/** Profile picture URL */
@JsonProperty("profilePicture") @SerialName("profilePicture")
val profilePicture: String? = null,
/** Profile picture Headers of the URL */
@JsonProperty("profilePictureHeaders") @JsonAlias("profilePictureHeader")
@SerialName("profilePictureHeaders") @JsonNames("profilePictureHeader")
val profilePictureHeaders: Map<String, String>? = null,
)
/**
* Stores all information that should be used to authorize access.
* Be aware that token and user may change independently when a refresh is needed,
* and as such there should be no strong pairing between the two.
*
* Any local set/get key should use user.id.toString(),
* as token.accessToken (even hashed) is unsecure, and will rotate.
*/
@Serializable
data class AuthData(
@JsonProperty("user") @SerialName("user") val user: AuthUser,
@JsonProperty("token") @SerialName("token") val token: AuthToken,
)
data class AuthPinData(
val deviceCode: String,
val userCode: String,
/** QR Code url */
val verificationUrl: String,
/** In seconds */
val expiresIn: Int,
/** Check if the code has been verified interval */
val interval: Int,
)
/** The login field requirements to display to the user */
data class AuthLoginRequirement(
val password: Boolean = false,
val username: Boolean = false,
val email: Boolean = false,
val server: Boolean = false,
)
/** What the user responds to the AuthLoginRequirement */
@Serializable
data class AuthLoginResponse(
@JsonProperty("password") @SerialName("password") val password: String?,
@JsonProperty("username") @SerialName("username") val username: String?,
@JsonProperty("email") @SerialName("email") val email: String?,
@JsonProperty("server") @SerialName("server") val server: String?,
)
/** Stateless Authentication class used for all personalized content */
abstract class AuthAPI {
open val name: String = "NONE"
open val idPrefix: String = "NONE"
/** Drawable icon of the service */
open val icon: Int? = null
/** If this service requires an account to use */
open val requiresLogin: Boolean = true
/** Link to a website for creating a new account */
open val createAccountUrl: String? = null
/** The sensitive redirect URL from OAuth should contain "/redirectUrlIdentifier" to trigger the login */
open val redirectUrlIdentifier: String? = null
/** Has OAuth2 login support, including login, loginRequest and refreshToken */
open val hasOAuth2: Boolean = false
/** Has on device pin support, aka login with a QR code */
open val hasPin: Boolean = false
/** Has in app login support, aka login with a dialog */
open val hasInApp: Boolean = false
/** The requirements to login in app */
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() = 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() = unixTimeMS
fun splitRedirectUrl(redirectUrl: String): Map<String, String> {
return splitUrlParameters(
redirectUrl.replace(APP_STRING, "https").replace("/#", "?")
)
}
fun generateCodeVerifier(): String {
// It is recommended to use a URL-safe string as code_verifier.
// See section 4 of RFC 7636 for more details.
val secureRandom = SecureRandom()
val codeVerifierBytes = ByteArray(96) // base64 has 6bit per char; (8/6)*96 = 128
secureRandom.nextBytes(codeVerifierBytes)
return base64Encode(codeVerifierBytes).trimEnd('=')
.replace("+", "-").replace("/", "_").replace("\n", "")
}
}
/** Is this url a valid redirect url for this service? */
@Throws
open fun isValidRedirectUrl(url: String): Boolean =
redirectUrlIdentifier != null && url.contains("/$redirectUrlIdentifier")
/** OAuth2 login from a valid redirectUrl, and payload given in loginRequest */
@Throws
open suspend fun login(redirectUrl: String, payload: String?): AuthToken? =
throw NotImplementedError()
/** OAuth2 login request, asking the service to provide a url to open in the browser */
@Throws
open fun loginRequest(): AuthLoginPage? = throw NotImplementedError()
/** Pin login request, asking the service to provide an verificationUrl to display with a QR code */
@Throws
open suspend fun pinRequest(): AuthPinData? = throw NotImplementedError()
/** OAuth2 token refresh, this ensures that all token passed to other functions will be valid */
@Throws
open suspend fun refreshToken(token: AuthToken): AuthToken? = throw NotImplementedError()
/** Pin login, this will be called periodically while logging in to check if the pin has been verified by the user */
@Throws
open suspend fun login(payload: AuthPinData): AuthToken? = throw NotImplementedError()
/** In app login */
@Throws
open suspend fun login(form: AuthLoginResponse): AuthToken? = throw NotImplementedError()
/** Get the visible user account */
@Throws
open suspend fun user(token: AuthToken?): AuthUser? = throw NotImplementedError()
/**
* An optional security measure to make sure that even if an attacker gets ahold of the token, it will be invalid.
*
* Note that this will currently only be called *once* on logout,
* and as such any network issues it will fail silently, and the token will not be revoked.
*/
@Throws
open suspend fun invalidateToken(token: AuthToken): Nothing = throw NotImplementedError()
@Throws
@Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
fun toRepo(): AuthRepo = when (this) {
is SubtitleAPI -> SubtitleRepo(this)
is SyncAPI -> SyncRepo(this)
else -> throw NotImplementedError("Unknown inheritance from AuthAPI")
}
@Suppress("DEPRECATION_ERROR")
@Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
fun loginInfo(): LoginInfo? {
return this.toRepo().authUser()?.let { user ->
LoginInfo(
profilePicture = user.profilePicture,
name = user.name,
accountIndex = -1,
)
}
}
@Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
@Suppress("DEPRECATION_ERROR")
return (this.toRepo() as? SyncRepo)?.library()?.getOrThrow()
}
@Deprecated("Please use the new API for AuthAPI", level = DeprecationLevel.ERROR)
class LoginInfo( class LoginInfo(
val profilePicture: String? = null, val profilePicture: String? = null,
val name: String?, val name: String?,

View file

@ -0,0 +1,168 @@
package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.CloudStreamApp.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.NONE_ID
import com.lagradost.cloudstream3.utils.txt
/** General-purpose repo */
class PlainAuthRepo(api: AuthAPI) : AuthRepo(api)
/** Safe abstraction for AuthAPI that provides both a catching interface, and automatic token management. */
abstract class AuthRepo(open val api: AuthAPI) {
fun isValidRedirectUrl(url: String) = safe { api.isValidRedirectUrl(url) } ?: false
val idPrefix get() = api.idPrefix
val name get() = api.name
val icon get() = api.icon
val requiresLogin get() = api.requiresLogin
val createAccountUrl get() = api.createAccountUrl
val hasOAuth2 get() = api.hasOAuth2
val hasPin get() = api.hasPin
val hasInApp get() = api.hasInApp
val inAppLoginRequirement get() = api.inAppLoginRequirement
val isAvailable get() = !api.requiresLogin || authUser() != null
companion object {
private val oauthPayload: MutableMap<String, String?> = mutableMapOf()
}
@Throws
protected suspend fun freshAuth(): AuthData? {
val data = authData() ?: return null
if (data.token.isAccessTokenExpired()) {
val newToken = api.refreshToken(data.token) ?: return null
val newAuth = AuthData(user = data.user, token = newToken)
refreshUser(newAuth)
return newAuth
}
return data
}
@Throws
fun openOAuth2Page(): Boolean {
val page = api.loginRequest() ?: return false
synchronized(oauthPayload) {
oauthPayload.put(idPrefix, page.payload)
}
openBrowser(page.url)
return true
}
fun openOAuth2PageWithToast() {
try {
if (!openOAuth2Page()) {
showToast(txt(R.string.authenticated_user_fail, api.name))
}
} catch (t: Throwable) {
logError(t)
if (t is ErrorLoadingException && t.message != null) {
showToast(t.message)
return
}
showToast(txt(R.string.authenticated_user_fail, api.name))
}
}
suspend fun logout(from: AuthUser) {
val currentAccounts = AccountManager.accounts(idPrefix)
val (newAccounts, oldAccounts) = currentAccounts.partition { it.user.id != from.id }
if (newAccounts.size < currentAccounts.size) {
AccountManager.updateAccounts(idPrefix, newAccounts.toTypedArray())
AccountManager.updateAccountsId(idPrefix, 0)
}
for (oldAccount in oldAccounts) {
try {
api.invalidateToken(oldAccount.token)
} catch (_: NotImplementedError) {
// no-op
} catch (t: Throwable) {
logError(t)
}
}
}
fun refreshUser(newAuth: AuthData) {
val currentAccounts = AccountManager.accounts(idPrefix)
val newAccounts = currentAccounts.map {
if (it.user.id == newAuth.user.id) {
newAuth
} else {
it
}
}.toTypedArray()
AccountManager.updateAccounts(idPrefix, newAccounts)
}
fun authData(): AuthData? = synchronized(AccountManager.cachedAccountIds) {
AccountManager.cachedAccountIds[idPrefix]?.let { id ->
AccountManager.cachedAccounts[idPrefix]?.firstOrNull { data -> data.user.id == id }
}
}
fun authToken(): AuthToken? = authData()?.token
fun authUser(): AuthUser? = authData()?.user
val accounts
get() = synchronized(AccountManager.cachedAccounts) {
AccountManager.cachedAccounts[idPrefix] ?: emptyArray()
}
var accountId
get() = synchronized(AccountManager.cachedAccountIds) {
AccountManager.cachedAccountIds[idPrefix] ?: NONE_ID
}
set(value) {
AccountManager.updateAccountsId(idPrefix, value)
}
@Throws
suspend fun pinRequest() =
api.pinRequest()
@Throws
private suspend fun setupLogin(token: AuthToken): Boolean {
val user = api.user(token) ?: return false
val newAccount = AuthData(
token = token,
user = user,
)
val currentAccounts = AccountManager.accounts(idPrefix)
if (currentAccounts.any { it.user.id == newAccount.user.id }) {
throw ErrorLoadingException("Already logged into this account")
}
val newAccounts = currentAccounts + newAccount
AccountManager.updateAccounts(idPrefix, newAccounts)
AccountManager.updateAccountsId(idPrefix, user.id)
if (this is SyncRepo) {
requireLibraryRefresh = true
}
return true
}
@Throws
suspend fun login(form: AuthLoginResponse): Boolean {
return setupLogin(api.login(form) ?: return false)
}
@Throws
suspend fun login(payload: AuthPinData): Boolean {
return setupLogin(api.login(payload) ?: return false)
}
@Throws
suspend fun login(redirectUrl: String): Boolean {
return setupLogin(
api.login(
redirectUrl,
synchronized(oauthPayload) { oauthPayload[api.idPrefix] }) ?: return false
)
}
}

View file

@ -0,0 +1,14 @@
package com.lagradost.cloudstream3.syncproviders
/** Work in progress */
abstract class BackupAPI : AuthAPI() {
open val filename : String = "cloudstream-backup.json"
/** Get the backup file as a JSON string from the remote storage. Return null if not found/empty */
@Throws
open suspend fun downloadFile(auth: AuthData?) : String? = throw NotImplementedError()
/** Get the backup file as a JSON string from the remote storage. */
@Throws
open suspend fun uploadFile(auth: AuthData?, data : String) : String? = throw NotImplementedError()
}

View file

@ -1,66 +0,0 @@
package com.lagradost.cloudstream3.syncproviders
import androidx.annotation.WorkerThread
interface InAppAuthAPI : AuthAPI {
data class LoginData(
val username: String? = null,
val password: String? = null,
val server: String? = null,
val email: String? = null,
)
// this is for displaying the UI
val requiresPassword: Boolean
val requiresUsername: Boolean
val requiresServer: Boolean
val requiresEmail: Boolean
// if this is false we can assume that getLatestLoginData returns null and wont be called
// this is used in case for some reason it is not preferred to store any login data besides the "token" or encrypted data
val storesPasswordInPlainText: Boolean
// return true if logged in successfully
suspend fun login(data: LoginData): Boolean
// used to fill the UI if you want to edit any data about your login info
fun getLatestLoginData(): LoginData?
}
abstract class InAppAuthAPIManager(defIndex: Int) : AccountManager(defIndex), InAppAuthAPI {
override val requiresPassword = false
override val requiresUsername = false
override val requiresEmail = false
override val requiresServer = false
override val storesPasswordInPlainText = true
override val requiresLogin = true
// runs on startup
@WorkerThread
open suspend fun initialize() {
}
override fun logOut() {
throw NotImplementedError()
}
override val idPrefix: String
get() = throw NotImplementedError()
override val name: String
get() = throw NotImplementedError()
override val icon: Int? = null
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
throw NotImplementedError()
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
throw NotImplementedError()
}
override fun loginInfo(): AuthAPI.LoginInfo? {
throw NotImplementedError()
}
}

View file

@ -1,27 +0,0 @@
package com.lagradost.cloudstream3.syncproviders
import androidx.fragment.app.FragmentActivity
interface OAuth2API : AuthAPI {
val key: String
val redirectUrl: String
val supportDeviceAuth: Boolean
suspend fun handleRedirect(url: String) : Boolean
fun authenticate(activity: FragmentActivity?)
suspend fun getDevicePin() : PinAuthData? {
return null
}
suspend fun handleDeviceAuth(pinAuthData: PinAuthData) : Boolean {
return false
}
data class PinAuthData(
val deviceCode: String,
val userCode: String,
val verificationUrl: String,
val expiresIn: Int,
val interval: Int,
)
}

View file

@ -0,0 +1,37 @@
package com.lagradost.cloudstream3.syncproviders
import androidx.annotation.WorkerThread
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.subtitles.SubtitleResource
/**
* Stateless subtitle class for external subtitles.
*
* All non-null `AuthToken` will be non-expired when each function is called.
*/
abstract class SubtitleAPI : AuthAPI() {
@WorkerThread
@Throws
open suspend fun search(auth: AuthData?, query: SubtitleSearch): List<SubtitleEntity>? =
throw NotImplementedError()
@WorkerThread
@Throws
open suspend fun load(auth: AuthData?, subtitle: SubtitleEntity): String? =
throw NotImplementedError()
@WorkerThread
@Throws
open suspend fun SubtitleResource.getResources(auth: AuthData?, subtitle: SubtitleEntity) {
this.addUrl(load(auth, subtitle))
}
@WorkerThread
@Throws
suspend fun resource(auth: AuthData?, subtitle: SubtitleEntity): SubtitleResource {
return SubtitleResource().apply {
this.getResources(auth, subtitle)
}
}
}

View file

@ -0,0 +1,95 @@
package com.lagradost.cloudstream3.syncproviders
import androidx.annotation.WorkerThread
import com.lagradost.cloudstream3.APIHolder.unixTime
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.atomicListOf
/** Stateless safe abstraction of SubtitleAPI */
class SubtitleRepo(override val api: SubtitleAPI) : AuthRepo(api) {
companion object {
data class SavedSearchResponse(
val unixTime: Long,
val response: List<SubtitleEntity>,
val query: SubtitleSearch,
val idPrefix: String,
)
data class SavedResourceResponse(
val unixTime: Long,
val response: SubtitleResource,
val query: SubtitleEntity
)
// maybe make this a generic struct? right now there is a lot of boilerplate
private val searchCache = atomicListOf<SavedSearchResponse>()
private var searchCacheIndex: Int = 0
private val resourceCache = atomicListOf<SavedResourceResponse>()
private var resourceCacheIndex: Int = 0
const val CACHE_SIZE = 20
}
@WorkerThread
suspend fun resource(data: SubtitleEntity): Result<SubtitleResource> = runCatching {
val cached = resourceCache.withLock {
var found: SubtitleResource? = null
for (item in resourceCache) {
// 20 min save
if (item.query == data && (unixTime - item.unixTime) < 60 * 20) {
found = item.response
break
}
}
found
}
if (cached != null) return@runCatching cached
val returnValue = api.resource(freshAuth(), data)
resourceCache.withLock {
val add = SavedResourceResponse(unixTime, returnValue, data)
if (resourceCache.size > CACHE_SIZE) {
resourceCache[resourceCacheIndex] = add // rolling cache
resourceCacheIndex = (resourceCacheIndex + 1) % CACHE_SIZE
} else {
resourceCache.add(add)
}
}
returnValue
}
@WorkerThread
suspend fun search(query: SubtitleSearch): Result<List<SubtitleEntity>> {
return runCatching {
val cached = searchCache.withLock {
var found: List<SubtitleEntity>? = null
for (item in searchCache) {
// 120 min save
if (item.idPrefix == idPrefix && item.query == query && (unixTime - item.unixTime) < 60 * 120) {
found = item.response
break
}
}
found
}
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, idPrefix)
searchCache.withLock {
if (searchCache.size > CACHE_SIZE) {
searchCache[searchCacheIndex] = add // rolling cache
searchCacheIndex = (searchCacheIndex + 1) % CACHE_SIZE
} else {
searchCache.add(add)
}
}
}
returnValue
}
}
}

View file

@ -1,45 +1,74 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.* import androidx.annotation.WorkerThread
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.NextAiring
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.ui.SyncWatchType import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.Levenshtein
import com.lagradost.cloudstream3.utils.UiText import com.lagradost.cloudstream3.utils.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.util.Date import java.util.Date
interface SyncAPI : OAuth2API { /**
* Stateless synchronization class, used for syncing status about a specific movie/show.
*
* All non-null `AuthToken` will be non-expired when each function is called.
*/
abstract class SyncAPI : AuthAPI() {
/** /**
* Set this to true if the user updates something on the list like watch status or score * Set this to true if the user updates something on the list like watch status or score
**/ **/
var requireLibraryRefresh: Boolean open var requireLibraryRefresh: Boolean = true
val mainUrl: String open val mainUrl: String = "NONE"
/** Currently unused, but will be used to correctly render the UI.
* This should specify what sync watch types can be used with this service. */
open val supportedWatchTypes: Set<SyncWatchType> = SyncWatchType.entries.toSet()
/** /**
* Allows certain providers to open pages from * Allows certain providers to open pages from
* library links. * library links.
**/ **/
val syncIdName: SyncIdName open val syncIdName: SyncIdName? = null
/** /** Modify the current status of an item */
-1 -> None @Throws
0 -> Watching @WorkerThread
1 -> Completed open suspend fun updateStatus(
2 -> OnHold auth: AuthData?,
3 -> Dropped id: String,
4 -> PlanToWatch newStatus: AbstractSyncStatus
5 -> ReWatching ): Boolean = throw NotImplementedError()
*/
suspend fun score(id: String, status: AbstractSyncStatus): Boolean
suspend fun getStatus(id: String): AbstractSyncStatus? /** Get the current status of an item */
@Throws
@WorkerThread
open suspend fun status(auth: AuthData?, id: String): AbstractSyncStatus? =
throw NotImplementedError()
suspend fun getResult(id: String): SyncResult? /** Get metadata about an item */
@Throws
@WorkerThread
open suspend fun load(auth: AuthData?, id: String): SyncResult? = throw NotImplementedError()
suspend fun search(name: String): List<SyncSearchResult>? /** Search this service for any results for a given query */
@Throws
@WorkerThread
open suspend fun search(auth: AuthData?, query: String): List<SyncSearchResult>? =
throw NotImplementedError()
suspend fun getPersonalLibrary(): LibraryMetadata? /** Get the current library/bookmarks of this service */
@Throws
@WorkerThread
open suspend fun library(auth: AuthData?): LibraryMetadata? = throw NotImplementedError()
fun getIdFromUrl(url: String): String /** Helper function, may be used in the future */
@Throws
open fun urlToId(url: String): String? = null
data class SyncSearchResult( data class SyncSearchResult(
override val name: String, override val name: String,
@ -51,23 +80,20 @@ interface SyncAPI : OAuth2API {
override var quality: SearchQuality? = null, override var quality: SearchQuality? = null,
override var posterHeaders: Map<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
override var id: Int? = null, override var id: Int? = null,
override var score: Score? = null,
) : SearchResponse ) : SearchResponse
abstract class AbstractSyncStatus { abstract class AbstractSyncStatus {
abstract var status: SyncWatchType abstract var status: SyncWatchType
abstract var score: Score?
/** 1-10 */
abstract var score: Int?
abstract var watchedEpisodes: Int? abstract var watchedEpisodes: Int?
abstract var isFavorite: Boolean? abstract var isFavorite: Boolean?
abstract var maxEpisodes: Int? abstract var maxEpisodes: Int?
} }
data class SyncStatus( data class SyncStatus(
override var status: SyncWatchType, override var status: SyncWatchType,
/** 1-10 */ override var score: Score?,
override var score: Int?,
override var watchedEpisodes: Int?, override var watchedEpisodes: Int?,
override var isFavorite: Boolean? = null, override var isFavorite: Boolean? = null,
override var maxEpisodes: Int? = null, override var maxEpisodes: Int? = null,
@ -80,8 +106,7 @@ interface SyncAPI : OAuth2API {
var totalEpisodes: Int? = null, var totalEpisodes: Int? = null,
var title: String? = null, var title: String? = null,
/**1-1000*/ var publicScore: Score? = null,
var publicScore: Int? = null,
/**In minutes*/ /**In minutes*/
var duration: Int? = null, var duration: Int? = null,
var synopsis: String? = null, var synopsis: String? = null,
@ -105,7 +130,6 @@ interface SyncAPI : OAuth2API {
var actors: List<ActorData>? = null, var actors: List<ActorData>? = null,
) )
data class Page( data class Page(
val title: UiText, var items: List<LibraryItem> val title: UiText, var items: List<LibraryItem>
) { ) {
@ -114,13 +138,14 @@ interface SyncAPI : OAuth2API {
ListSorting.Query -> ListSorting.Query ->
if (query != null) { if (query != null) {
items.sortedBy { items.sortedBy {
-FuzzySearch.partialRatio( -Levenshtein.partialRatio(
query.lowercase(), it.name.lowercase() query.lowercase(), it.name.lowercase()
) )
} }
} else items } else items
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) } ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating?.toInt(100) ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating?.toInt(100) ?: 0) }
ListSorting.AlphabeticalA -> items.sortedBy { it.name } ListSorting.AlphabeticalA -> items.sortedBy { it.name }
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed() ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) } ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
@ -153,8 +178,7 @@ interface SyncAPI : OAuth2API {
val syncId: String, val syncId: String,
val episodesCompleted: Int?, val episodesCompleted: Int?,
val episodesTotal: Int?, val episodesTotal: Int?,
/** Out of 100 */ val personalRating: Score?,
val personalRating: Int?,
val lastUpdatedUnixTime: Long?, val lastUpdatedUnixTime: Long?,
override val apiName: String, override val apiName: String,
override var type: TvType?, override var type: TvType?,
@ -164,7 +188,7 @@ interface SyncAPI : OAuth2API {
val releaseDate: Date?, val releaseDate: Date?,
override var id: Int? = null, override var id: Int? = null,
val plot: String? = null, val plot: String? = null,
val rating: Int? = null, override var score: Score? = null,
val tags: List<String>? = null val tags: List<String>? = null
) : SearchResponse ) : SearchResponse
} }

View file

@ -1,48 +1,30 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.ErrorLoadingException /** Stateless safe abstraction of SyncAPI */
import com.lagradost.cloudstream3.mvvm.Resource class SyncRepo(override val api: SyncAPI) : AuthRepo(api) {
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall val syncIdName = api.syncIdName
import com.lagradost.cloudstream3.mvvm.safeApiCall
class SyncRepo(private val repo: SyncAPI) {
val idPrefix = repo.idPrefix
val name = repo.name
val icon = repo.icon
val mainUrl = repo.mainUrl
val requiresLogin = repo.requiresLogin
val syncIdName = repo.syncIdName
var requireLibraryRefresh: Boolean var requireLibraryRefresh: Boolean
get() = repo.requireLibraryRefresh get() = api.requireLibraryRefresh
set(value) { set(value) {
repo.requireLibraryRefresh = value api.requireLibraryRefresh = value
} }
suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Resource<Boolean> { suspend fun updateStatus(id: String, newStatus: SyncAPI.AbstractSyncStatus): Result<Boolean> =
return safeApiCall { repo.score(id, status) } runCatching {
val status = api.updateStatus(freshAuth() ?: return@runCatching false, id, newStatus)
requireLibraryRefresh = true
status
} }
suspend fun getStatus(id: String): Resource<SyncAPI.AbstractSyncStatus> { suspend fun status(id: String): Result<SyncAPI.AbstractSyncStatus?> = runCatching {
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } api.status(freshAuth(), id)
} }
suspend fun getResult(id: String): Resource<SyncAPI.SyncResult> { suspend fun load(id: String): Result<SyncAPI.SyncResult?> = runCatching {
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } api.load(freshAuth(), id)
} }
suspend fun search(query: String): Resource<List<SyncAPI.SyncSearchResult>> { suspend fun library(): Result<SyncAPI.LibraryMetadata?> = runCatching {
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } api.library(freshAuth())
}
suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> {
return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
}
fun hasAccount(): Boolean {
return normalSafeApiCall { repo.loginInfo() != null } ?: false
}
fun getIdFromUrl(url: String): String? = normalSafeApiCall {
repo.getIdFromUrl(url)
} }
} }

View file

@ -1,108 +1,205 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleEntity
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities.SubtitleSearch
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.SubtitleAPI
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToEnglishLanguageName
class Addic7ed : AbstractSubApi { class Addic7ed : SubtitleAPI() {
override val name = "Addic7ed" override val name = "Addic7ed"
override val idPrefix = "addic7ed" override val idPrefix = "addic7ed"
override val requiresLogin = false override val requiresLogin = false
override val icon: Nothing? = null
override val createAccountUrl: Nothing? = null
override fun loginInfo(): Nothing? = null
override fun logOut() {}
companion object { companion object {
const val HOST = "https://www.addic7ed.com" const val HOST = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED" const val TAG = "ADDIC7ED"
} }
private fun fixUrl(url: String): String { private fun String.fixUrl(): String {
val url = this
return if (url.startsWith("/")) HOST + url return if (url.startsWith("/")) HOST + url
else if (!url.startsWith("http")) "$HOST/$url" else if (!url.startsWith("http")) "$HOST/$url"
else url else url
} }
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> { override suspend fun search(
val lang = query.lang auth: AuthData?,
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString()) query: SubtitleSearch
val queryText = query.query.trim() ): List<SubtitleEntity>? {
val langTagIETF = query.lang ?: AllLanguagesName
val langNumAddic7ed =
langTagIETF2Addic7ed[langTagIETF]?.first ?: 0 // all languages = 0
val langName =
langTagIETF2Addic7ed[langTagIETF]?.second ?:
fromTagToEnglishLanguageName(langTagIETF) ?:
"Completed" // this bypasses language filtering
val title = query.query.trim()
val epNum = query.epNumber ?: 0 val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0 val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0 val yearNum = query.year ?: 0
val searchQuery = if (seasonNum > 0) "$title $seasonNum $epNum" else title
var downloadPage = ""
fun cleanResources( fun newSubtitleEntity (
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>, displayName: String?,
name: String, link: String?,
link: String,
headers: Map<String, String>,
isHearingImpaired: Boolean isHearingImpaired: Boolean
) { ): SubtitleEntity? {
results.add( if (displayName.isNullOrBlank() || link.isNullOrBlank()) return null
AbstractSubtitleEntities.SubtitleEntity( return SubtitleEntity(
idPrefix = idPrefix, idPrefix = this.idPrefix,
name = name, name = displayName,
lang = queryLang.toString(), lang = langTagIETF,
data = link, data = link,
source = this.name, source = this.name,
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie, type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
epNumber = epNum, epNumber = epNum,
seasonNumber = seasonNum, seasonNumber = seasonNum,
year = yearNum, year = yearNum,
headers = headers, headers = mapOf("referer" to "$HOST/"),
isHearingImpaired = isHearingImpaired isHearingImpaired = isHearingImpaired
) )
)
} }
val title = queryText.substringBefore("(").trim() val response = app.get(url = "$HOST/search.php?search=$searchQuery&Submit=Search")
val url = "$HOST/search.php?search=${title}&Submit=Search" val hostDocument = response.document
val hostDocument = app.get(url).document
var searchResult = "" // 1st case: found one movie or episode. Redirected to $HOST/movie/1234 or $HOST/serie/show-name/$seasonNum/$epNum/ep-name
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url if (response.url.contains("/movie/") || response.url.contains("/serie/"))
else if (!hostDocument.select("table.tabel") downloadPage = response.url
.isNullOrEmpty()
) searchResult = hostDocument.select("a:contains($title)").attr("href").toString() // 2nd case: found tv series ep list. Redirected to $HOST/show/1234
else { else if (response.url.contains("/show/")) {
val show = val showId = response.url.substringAfterLast("/")
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
?.substringBefore(",")
val doc = app.get( val doc = app.get(
"$HOST/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined", "$HOST/ajax_loadShow.php?show=$showId&season=$seasonNum&langs=|$langNumAddic7ed|&hd=0&hi=0",
referer = "$HOST/" referer = "$HOST/"
).document ).document
doc.select("#season tr:contains($queryLang)").mapNotNull { node ->
if (node.selectFirst("td")?.text()
?.toIntOrNull() == seasonNum && node.select("td:eq(1)")
.text()
.toIntOrNull() == epNum
) searchResult = fixUrl(node.select("a").attr("href"))
}
}
val results = mutableListOf<AbstractSubtitleEntities.SubtitleEntity>()
val document = app.get(
url = fixUrl(searchResult),
).document
document.select(".tabel95 .tabel95 tr:contains($queryLang)").mapNotNull { node -> // get direct subtitles links from list
val name = if (seasonNum > 0) "${document.select(".titulo").text().replace("Subtitle","").trim()}${ return doc.select("#season tbody tr").mapNotNull { node ->
if (node.select("td:eq(1)").text().toIntOrNull() == epNum)
newSubtitleEntity(
displayName = node.select("td:eq(2)").text() + "\n" + node.select("td:eq(4)").text(),
link = node.selectFirst("a[href~=updated\\/|original\\/]")?.attr("href")?.fixUrl(),
isHearingImpaired = node.select("td:eq(6)").text().isNotEmpty()
)
else null
}
// 3rd case: found several or no results. Still in $HOST/search.php?search=title
} else {// (response.url.contains("/search.php"))
downloadPage = hostDocument.select("table.tabel a").selectFirst({
// tv series
if (seasonNum > 0) "a[href~=serie\\/.+\\/$seasonNum\\/$epNum\\/\\w]"
// movie + year
else if( yearNum > 0) "a[href~=movie\\/]:contains($yearNum)"
// movie
else "a[href~=movie\\/]"
}())?.attr("href")?.fixUrl() ?: return null
}
// filter download page by language. Do not work for movies :/
if (downloadPage.contains("/serie/"))
downloadPage = downloadPage.substringBeforeLast("/") + "/$langNumAddic7ed"
val doc = app.get(url = downloadPage).document
// get subtitles links from download page
return doc.select(".tabel95 .tabel95 tr:has(.language):contains($langName)").mapNotNull { node ->
val displayName =
doc.selectFirst("span.titulo")?.text()?.substringBefore(" Subtitle") + "\n" +
node.parent()!!.select(".NewsTitle").text().substringAfter("Version ").substringBefore(", Duration") node.parent()!!.select(".NewsTitle").text().substringAfter("Version ").substringBefore(", Duration")
}" else "${document.select(".titulo").text().replace("Subtitle","").trim()}${node.parent()!!.select(".NewsTitle").text().substringAfter("Version").substringBefore(", Duration")}" val link =
val link = fixUrl(node.select("a.buttonDownload").attr("href")) node.selectFirst("a[href~=updated\\/|original\\/]")?.attr("href")?.fixUrl()
val isHearingImpaired = val isHearingImpaired =
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty() node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNotEmpty()
cleanResources(results, name, link, mapOf("referer" to "$HOST/"), isHearingImpaired)
newSubtitleEntity(displayName, link, isHearingImpaired)
} }
return results
} }
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String { override suspend fun load(
return data.data auth: AuthData?,
subtitle: SubtitleEntity
): String? {
return subtitle.data
} }
// Missing (?_?)
// Pair("2", ""),
// Pair("3", ""),
// Pair("33", ""),
// Pair("34", ""),
// Do not modify unless Addic7ed changes them!
// as they are the exact values from their website
private val langTagIETF2Addic7ed = mapOf(
"ar" to Pair("38", "Arabic"),
"az" to Pair("48", "Azerbaijani"),
"bg" to Pair("35", "Bulgarian"),
"bn" to Pair("47", "Bengali"),
"bs" to Pair("44", "Bosnian"),
"ca" to Pair("12", "Català"),
"cs" to Pair("14", "Czech"),
"cy" to Pair("65", "Welsh"),
"da" to Pair("30", "Danish"),
"de" to Pair("11", "German"),
"el" to Pair("27", "Greek"),
"en" to Pair("1", "English"),
"es-419" to Pair("6", "Spanish (Latin America)"),
"es-ar" to Pair("69", "Spanish (Argentina)"),
"es-es" to Pair("5", "Spanish (Spain)"),
"es" to Pair("4", "Spanish"),
"et" to Pair("54", "Estonian"),
"eu" to Pair("13", "Euskera"),
"fa" to Pair("43", "Persian"),
"fi" to Pair("28", "Finnish"),
"fr-ca" to Pair("53", "French (Canadian)"),
"fr" to Pair("8", "French"),
"gl" to Pair("15", "Galego"),
"he" to Pair("23", "Hebrew"),
"hi" to Pair("55", "Hindi"),
"hr" to Pair("31", "Croatian"),
"hu" to Pair("20", "Hungarian"),
"hy" to Pair("50", "Armenian"),
"id" to Pair("37", "Indonesian"),
"is" to Pair("56", "Icelandic"),
"it" to Pair("7", "Italian"),
"ja" to Pair("32", "Japanese"),
"kn" to Pair("66", "Kannada"),
"ko" to Pair("42", "Korean"),
"lt" to Pair("58", "Lithuanian"),
"lv" to Pair("57", "Latvian"),
"mk" to Pair("49", "Macedonian"),
"ml" to Pair("67", "Malayalam"),
"mr" to Pair("62", "Marathi"),
"ms" to Pair("40", "Malay"),
"nl" to Pair("17", "Dutch"),
"no" to Pair("29", "Norwegian"),
"pl" to Pair("21", "Polish"),
"pt-br" to Pair("10", "Portuguese (Brazilian)"),
"pt" to Pair("9", "Portuguese"),
"ro" to Pair("26", "Romanian"),
"ru" to Pair("19", "Russian"),
"si" to Pair("60", "Sinhala"),
"sk" to Pair("25", "Slovak"),
"sl" to Pair("22", "Slovenian"),
"sq" to Pair("52", "Albanian"),
"sr-latn" to Pair("36", "Serbian (Latin)"),
"sr" to Pair("39", "Serbian (Cyrillic)"),
"sv" to Pair("18", "Swedish"),
"ta" to Pair("59", "Tamil"),
"te" to Pair("63", "Telugu"),
"th" to Pair("46", "Thai"),
"tl" to Pair("68", "Tagalog"),
"tlh" to Pair("61", "Klingon"),
"tr" to Pair("16", "Turkish"),
"uk" to Pair("51", "Ukrainian"),
"vi" to Pair("45", "Vietnamese"),
"yue" to Pair("64", "Cantonese"),
"zh-hans" to Pair("41", "Chinese (Simplified)"),
"zh-hant" to Pair("24", "Chinese (Traditional)"),
)
} }

View file

@ -1,35 +0,0 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API
//TODO dropbox sync
class Dropbox : OAuth2API {
override val idPrefix = "dropbox"
override var name = "Dropbox"
override val key = "zlqsamadlwydvb2"
override val redirectUrl = "dropboxlogin"
override val requiresLogin = true
override val supportDeviceAuth = false
override val createAccountUrl: String? = null
override val icon: Int
get() = TODO("Not yet implemented")
override fun authenticate(activity: FragmentActivity?) {
TODO("Not yet implemented")
}
override suspend fun handleRedirect(url: String): Boolean {
TODO("Not yet implemented")
}
override fun logOut() {
TODO("Not yet implemented")
}
override fun loginInfo(): AuthAPI.LoginInfo? {
TODO("Not yet implemented")
}
}

View file

@ -1,8 +1,658 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import com.fasterxml.jackson.annotation.JsonProperty 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
import com.lagradost.cloudstream3.Score
import com.lagradost.cloudstream3.ShowStatus
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement
import com.lagradost.cloudstream3.syncproviders.AuthLoginResponse
import com.lagradost.cloudstream3.syncproviders.AuthToken
import com.lagradost.cloudstream3.syncproviders.AuthUser
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.SyncWatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.txt
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.text.SimpleDateFormat
import java.time.LocalDate
import java.time.ZoneId
import java.util.Date
import java.util.Locale
const val KITSU_MAX_SEARCH_LIMIT = 20
class KitsuApi: SyncAPI() {
override var name = "Kitsu"
override val idPrefix = "kitsu"
private val apiUrl = "https://kitsu.io/api/edge"
private val fallbackApiUrl = "https://kitsu.app/api/edge"
private val oauthUrl = "https://kitsu.io/api/oauth"
private val fallbackOauthUrl = "https://kitsu.app/api/oauth"
override val hasInApp = true
override val mainUrl = "https://kitsu.app"
override val icon = R.drawable.kitsu_icon
override val syncIdName = SyncIdName.Kitsu
override val createAccountUrl = mainUrl
override val supportedWatchTypes = setOf(
SyncWatchType.WATCHING,
SyncWatchType.COMPLETED,
SyncWatchType.PLANTOWATCH,
SyncWatchType.DROPPED,
SyncWatchType.ONHOLD,
SyncWatchType.NONE
)
override val inAppLoginRequirement = AuthLoginRequirement(
password = true,
email = true
)
private class FallbackInterceptor(private val apiUrl: String, private val fallbackApiUrl: String) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
try {
val response = chain.proceed(request);
if (response.isSuccessful) return response
response.close()
} catch (_: Exception) {
}
val fallbackRequest: Request = request.newBuilder()
.url(request.url.toString().replaceFirst(apiUrl, fallbackApiUrl))
.build()
return chain.proceed(fallbackRequest)
}
}
private val apiFallbackInterceptor = FallbackInterceptor(apiUrl, fallbackApiUrl)
private val oauthFallbackInterceptor = FallbackInterceptor(oauthUrl, fallbackOauthUrl)
override suspend fun login(form: AuthLoginResponse): AuthToken? {
val username = form.email ?: return null
val password = form.password ?: return null
val grantType = "password"
val token = app.post(
"$oauthUrl/token",
data = mapOf(
"grant_type" to grantType,
"username" to username,
"password" to password
),
interceptor = oauthFallbackInterceptor
).parsed<ResponseToken>()
return AuthToken(
accessTokenLifetime = APIHolder.unixTime + token.expiresIn.toLong(),
refreshToken = token.refreshToken,
accessToken = token.accessToken,
)
}
override suspend fun refreshToken(token: AuthToken): AuthToken {
val res = app.post(
"$oauthUrl/token",
data = mapOf(
"grant_type" to "refresh_token",
"refresh_token" to token.refreshToken!!
),
interceptor = oauthFallbackInterceptor
).parsed<ResponseToken>()
return AuthToken(
accessToken = res.accessToken,
refreshToken = res.refreshToken,
accessTokenLifetime = APIHolder.unixTime + res.expiresIn.toLong()
)
}
override suspend fun user(token: AuthToken?): AuthUser? {
val user = app.get(
"$apiUrl/users?filter[self]=true",
headers = mapOf(
"Authorization" to "Bearer ${token?.accessToken ?: return null}"
), cacheTime = 0,
interceptor = apiFallbackInterceptor
).parsed<KitsuResponse>()
if (user.data.isEmpty()) {
return null
}
return AuthUser(
id = user.data[0].id.toInt(),
name = user.data[0].attributes.name,
profilePicture = user.data[0].attributes.avatar?.original
)
}
override suspend fun search(auth: AuthData?, query: String): List<SyncSearchResult>? {
val auth = auth?.token?.accessToken ?: return null
val animeSelectedFields = arrayOf("titles","canonicalTitle","posterImage","episodeCount")
val url = "$apiUrl/anime?filter[text]=$query&page[limit]=$KITSU_MAX_SEARCH_LIMIT&fields[anime]=${animeSelectedFields.joinToString(",")}"
val res = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth",
), cacheTime = 0,
interceptor = apiFallbackInterceptor
).parsed<KitsuResponse>()
return res.data.map {
val attributes = it.attributes
val title = attributes.canonicalTitle ?: attributes.titles?.enJp ?: attributes.titles?.jaJp ?: "No title"
SyncSearchResult(
title,
this.name,
it.id,
"$mainUrl/anime/${it.id}/",
attributes.posterImage?.large ?: attributes.posterImage?.medium
)
}
}
override suspend fun load(auth : AuthData?, id: String): SyncResult? {
val auth = auth?.token?.accessToken ?: return null
if (id.toIntOrNull() == null) {
return null
}
@Serializable
data class KitsuResponse(
@JsonProperty("data") @SerialName("data") val data: KitsuNode,
)
val url =
"$apiUrl/anime/$id"
val anime = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $auth"
),
interceptor = apiFallbackInterceptor
).parsed<KitsuResponse>().data.attributes
return SyncResult(
id = id,
totalEpisodes = anime.episodeCount,
title = anime.canonicalTitle ?: anime.titles?.enJp ?: anime.titles?.jaJp.orEmpty(),
publicScore = Score.from(anime.ratingTwenty, 20),
duration = anime.episodeLength,
synopsis = anime.synopsis,
airStatus = when (anime.status) {
"finished" -> ShowStatus.Completed
"current" -> ShowStatus.Ongoing
else -> null
},
nextAiring = null,
studio = null,
genres = null,
trailers = null,
startDate = LocalDate.parse(anime.startDate).toEpochDay(),
endDate = LocalDate.parse(anime.endDate).toEpochDay(),
recommendations = null,
nextSeason =null,
prevSeason = null,
actors = null,
)
}
override suspend fun status(auth : AuthData?, id: String): AbstractSyncStatus? {
val accessToken = auth?.token?.accessToken ?: return null
val userId = auth.user.id
val selectedFields = arrayOf("status","ratingTwenty", "progress")
val url =
"$apiUrl/library-entries?filter[userId]=$userId&filter[animeId]=$id&fields[libraryEntries]=${selectedFields.joinToString(",")}"
val anime = app.get(
url, headers = mapOf(
"Authorization" to "Bearer $accessToken"
),
interceptor = apiFallbackInterceptor
).parsed<KitsuResponse>().data.firstOrNull()?.attributes
if (anime == null) {
return SyncStatus(
score = null,
status = SyncWatchType.NONE,
isFavorite = null,
watchedEpisodes = null
)
}
return SyncStatus(
score = Score.from(anime.ratingTwenty, 20),
status = SyncWatchType.fromInternalId(kitsuStatusAsString.indexOf(anime.status)),
isFavorite = null,
watchedEpisodes = anime.progress,
)
}
suspend fun getAnimeIdByTitle(title: String): String? {
val animeSelectedFields = arrayOf("titles","canonicalTitle")
val url = "$apiUrl/anime?filter[text]=$title&page[limit]=$KITSU_MAX_SEARCH_LIMIT&fields[anime]=${animeSelectedFields.joinToString(",")}"
val res = app.get(url, interceptor = apiFallbackInterceptor).parsed<KitsuResponse>()
return res.data.firstOrNull()?.id
}
override fun urlToId(url: String): String? =
Regex("""/anime/((.*)/|(.*))""").find(url)?.groupValues?.first()
override suspend fun updateStatus(
auth : AuthData?,
id: String,
newStatus: AbstractSyncStatus
): Boolean {
return setScoreRequest(
auth ?: return false,
id.toIntOrNull() ?: return false,
fromIntToAnimeStatus(newStatus.status),
newStatus.score?.toInt(20),
newStatus.watchedEpisodes
)
}
private suspend fun setScoreRequest(
auth: AuthData,
id: Int,
status: KitsuStatusType? = null,
score: Int? = null,
numWatchedEpisodes: Int? = null,
): Boolean {
val libraryEntryId = getAnimeLibraryEntryId(auth, id)
// Exists entry for anime in library
if (libraryEntryId != null) {
// Delete anime from library
if (status == null || status == KitsuStatusType.None) {
val res = app.delete(
"$apiUrl/library-entries/$libraryEntryId",
headers = mapOf(
"Authorization" to "Bearer ${auth.token.accessToken}"
),
interceptor = apiFallbackInterceptor
)
return res.isSuccessful
}
return setScoreRequest(
auth,
libraryEntryId,
kitsuStatusAsString[maxOf(0, status.value)],
score,
numWatchedEpisodes
)
}
val data = mapOf(
"data" to mapOf(
"type" to "libraryEntries",
"attributes" to mapOf(
"ratingTwenty" to score,
"progress" to numWatchedEpisodes,
"status" to if (status == null) null else kitsuStatusAsString[maxOf(0, status.value)],
),
"relationships" to mapOf(
"anime" to mapOf(
"data" to mapOf(
"type" to "anime",
"id" to id.toString()
)
),
"user" to mapOf(
"data" to mapOf(
"type" to "users",
"id" to auth.user.id
)
)
)
)
)
val res = app.post(
"$apiUrl/library-entries",
headers = mapOf(
"content-type" to "application/vnd.api+json",
"Authorization" to "Bearer ${auth.token.accessToken}"
),
requestBody = data.toJson().toRequestBody(),
interceptor = apiFallbackInterceptor
)
return res.isSuccessful
}
@Suppress("UNCHECKED_CAST")
private suspend fun setScoreRequest(
auth : AuthData,
id: Int,
status: String? = null,
score: Int? = null,
numWatchedEpisodes: Int? = null,
): Boolean {
val data = mapOf(
"data" to mapOf(
"type" to "libraryEntries",
"id" to id.toString(),
"attributes" to mapOf(
"ratingTwenty" to score,
"progress" to numWatchedEpisodes,
"status" to status
)
)
)
val res = app.patch(
"$apiUrl/library-entries/$id",
headers = mapOf(
"content-type" to "application/vnd.api+json",
"Authorization" to "Bearer ${auth.token.accessToken}"
),
requestBody = data.toJson().toRequestBody(),
interceptor = apiFallbackInterceptor
)
return res.isSuccessful
}
private suspend fun getAnimeLibraryEntryId(auth: AuthData, id: Int): Int? {
val userId = auth.user.id
val res = app.get(
"$apiUrl/library-entries?filter[userId]=$userId&filter[animeId]=$id",
headers = mapOf(
"Authorization" to "Bearer ${auth.token.accessToken}"
),
interceptor = apiFallbackInterceptor
).parsed<KitsuResponse>().data.firstOrNull() ?: return null
return res.id.toInt()
}
override suspend fun library(auth : AuthData?): LibraryMetadata? {
val list = getKitsuAnimeListSmart(auth ?: return null)?.groupBy {
convertToStatus(it.attributes.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.toLibraryItem() }
} ?: emptyMap()
// To fill empty lists when Kitsu does not return them
val baseMap =
KitsuStatusType.entries.filter { it.value >= 0 }.associate {
it.stringRes to emptyList<LibraryItem>()
}
return LibraryMetadata(
(baseMap + list).map { LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.ReleaseDateNew,
ListSorting.ReleaseDateOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getKitsuAnimeListSmart(auth : AuthData): Array<KitsuNode>? {
return if (requireLibraryRefresh) {
val list = getKitsuAnimeList(auth.token, auth.user.id)
setKey(KITSU_CACHED_LIST, auth.user.id.toString(), list)
list
} else {
getKey<Array<KitsuNode>>(KITSU_CACHED_LIST, auth.user.id.toString()) as? Array<KitsuNode>
}
}
private suspend fun getKitsuAnimeList(token: AuthToken, userId: Int): Array<KitsuNode> {
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(",")}"
val fullList = mutableListOf<KitsuNode>()
while (true) {
val data: KitsuResponse = getKitsuAnimeListSlice(token, url)
data.data.forEachIndexed { index, value ->
value.anime = data.included?.get(index)
}
fullList.addAll(data.data)
url = data.links?.next ?: break
}
return fullList.toTypedArray()
}
private suspend fun getKitsuAnimeListSlice(token: AuthToken, url: String): KitsuResponse {
return app.get(
url, headers = mapOf(
"Authorization" to "Bearer ${token.accessToken}",
),
interceptor = apiFallbackInterceptor
).parsed<KitsuResponse>()
}
@Serializable
data class ResponseToken(
@JsonProperty("token_type") @SerialName("token_type") val tokenType: String,
@JsonProperty("expires_in") @SerialName("expires_in") val expiresIn: Int,
@JsonProperty("access_token") @SerialName("access_token") val accessToken: String,
@JsonProperty("refresh_token") @SerialName("refresh_token") val refreshToken: String,
)
@Serializable
data class KitsuNode(
@JsonProperty("id") @SerialName("id") val id: String,
@JsonProperty("attributes") @SerialName("attributes") val attributes: KitsuNodeAttributes,
/* User list anime node */
@JsonProperty("relationships") @SerialName("relationships") val relationships: KitsuRelationships?,
@JsonProperty("anime") @SerialName("anime") var anime: KitsuAnimeData?,
) {
fun toLibraryItem(): LibraryItem {
val animeItem = this.anime
val numEpisodes = animeItem?.attributes?.episodeCount
val startDate = animeItem?.attributes?.startDate
val posterImage = animeItem?.attributes?.posterImage
val canonicalTitle = animeItem?.attributes?.canonicalTitle
val titles = animeItem?.attributes?.titles
val animeId = animeItem?.id
val synopsis: String? = animeItem?.attributes?.synopsis
return LibraryItem(
canonicalTitle ?: titles?.enJp ?: titles?.jaJp.orEmpty(),
"https://kitsu.app/anime/${animeId}/",
this.id,
this.attributes.progress,
numEpisodes,
Score.from(this.attributes.ratingTwenty, 20),
parseDateLong(this.attributes.updatedAt),
"Kitsu",
TvType.Anime,
posterImage?.large ?: posterImage?.medium,
null,
null,
plot = synopsis,
releaseDate = if (startDate == null) null else try {
Date.from(LocalDate.parse(startDate).atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant())
} catch (_: RuntimeException) {
null
}
)
}
}
@Serializable
data class KitsuAnimeAttributes(
@JsonProperty("titles") @SerialName("titles") val titles: KitsuTitles?,
@JsonProperty("canonicalTitle") @SerialName("canonicalTitle") val canonicalTitle: String?,
@JsonProperty("posterImage") @SerialName("posterImage") val posterImage: KitsuPosterImage?,
@JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?,
@JsonProperty("startDate") @SerialName("startDate") val startDate: String?,
@JsonProperty("endDate") @SerialName("endDate") val endDate: String?,
@JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Int?,
@JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Int?,
)
@Serializable
data class KitsuAnimeData(
@JsonProperty("id") @SerialName("id") val id: String,
@JsonProperty("attributes") @SerialName("attributes") val attributes: KitsuAnimeAttributes,
)
@Serializable
data class KitsuNodeAttributes(
/* General attributes */
@JsonProperty("titles") @SerialName("titles") val titles: KitsuTitles?,
@JsonProperty("canonicalTitle") @SerialName("canonicalTitle") val canonicalTitle: String?,
@JsonProperty("posterImage") @SerialName("posterImage") val posterImage: KitsuPosterImage?,
@JsonProperty("synopsis") @SerialName("synopsis") val synopsis: String?,
@JsonProperty("startDate") @SerialName("startDate") val startDate: String?,
@JsonProperty("endDate") @SerialName("endDate") val endDate: String?,
@JsonProperty("episodeCount") @SerialName("episodeCount") val episodeCount: Int?,
@JsonProperty("episodeLength") @SerialName("episodeLength") val episodeLength: Int?,
/* User attributes */
@JsonProperty("name") @SerialName("name") val name: String?,
@JsonProperty("location") @SerialName("location") val location: String?,
@JsonProperty("createdAt") @SerialName("createdAt") val createdAt: String?,
@JsonProperty("avatar") @SerialName("avatar") val avatar: KitsuUserAvatar?,
/* User list anime attributes */
@JsonProperty("progress") @SerialName("progress") val progress: Int?,
@JsonProperty("ratingTwenty") @SerialName("ratingTwenty") val ratingTwenty: Int?,
@JsonProperty("updatedAt") @SerialName("updatedAt") val updatedAt: String?,
@JsonProperty("status") @SerialName("status") val status: String?,
)
@Serializable
data class KitsuRelationships(
@JsonProperty("anime") @SerialName("anime") val anime: KitsuRelationshipsAnime?,
)
@Serializable
data class KitsuRelationshipsAnime(
@JsonProperty("links") @SerialName("links") val links: KitsuLinks?,
)
@Serializable
data class KitsuPosterImage(
@JsonProperty("large") @SerialName("large") val large: String?,
@JsonProperty("medium") @SerialName("medium") val medium: String?,
)
@Serializable
data class KitsuTitles(
@JsonProperty("en_jp") @SerialName("en_jp") val enJp: String?,
@JsonProperty("ja_jp") @SerialName("ja_jp") val jaJp: String?,
)
@Serializable
data class KitsuUserAvatar(
@JsonProperty("original") @SerialName("original") val original: String?,
)
@Serializable
data class KitsuLinks(
/* Pagination */
@JsonProperty("first") @SerialName("first") val first: String?,
@JsonProperty("next") @SerialName("next") val next: String?,
@JsonProperty("last") @SerialName("last") val last: String?,
/* Relationships */
@JsonProperty("related") @SerialName("related") val related: String?,
)
@Serializable
data class KitsuResponse(
@JsonProperty("links") @SerialName("links") val links: KitsuLinks?,
@JsonProperty("data") @SerialName("data") val data: List<KitsuNode>,
/* When requesting related info (User library entry -> anime) */
@JsonProperty("included") @SerialName("included") val included: List<KitsuAnimeData>?,
)
companion object {
const val KITSU_CACHED_LIST: String = "kitsu_cached_list"
private fun parseDateLong(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault()).parse(
string ?: return null
)?.time?.div(1000)
} catch (_: Exception) {
null
}
}
private val kitsuStatusAsString =
arrayOf("current", "completed", "on_hold", "dropped", "planned")
private fun fromIntToAnimeStatus(inp: SyncWatchType): KitsuStatusType {
return when (inp) {
SyncWatchType.NONE -> KitsuStatusType.None
SyncWatchType.WATCHING -> KitsuStatusType.Watching
SyncWatchType.COMPLETED -> KitsuStatusType.Completed
SyncWatchType.ONHOLD -> KitsuStatusType.OnHold
SyncWatchType.DROPPED -> KitsuStatusType.Dropped
SyncWatchType.PLANTOWATCH -> KitsuStatusType.PlanToWatch
SyncWatchType.REWATCHING -> KitsuStatusType.Watching
}
}
enum class KitsuStatusType(var value: Int, @StringRes val stringRes: Int) {
Watching(0, R.string.type_watching),
Completed(1, R.string.type_completed),
OnHold(2, R.string.type_on_hold),
Dropped(3, R.string.type_dropped),
PlanToWatch(4, R.string.type_plan_to_watch),
None(-1, R.string.type_none)
}
private fun convertToStatus(string: String): KitsuStatusType {
return when (string) {
"current" -> KitsuStatusType.Watching
"completed" -> KitsuStatusType.Completed
"on_hold" -> KitsuStatusType.OnHold
"dropped" -> KitsuStatusType.Dropped
"planned" -> KitsuStatusType.PlanToWatch
else -> KitsuStatusType.None
}
}
}
}
// modified code from from https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/others/Kitsu.kt // modified code from from https://github.com/saikou-app/saikou/blob/main/app/src/main/java/ani/saikou/others/Kitsu.kt
// GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md // GNU General Public License v3.0 https://github.com/saikou-app/saikou/blob/main/LICENSE.md
@ -20,7 +670,7 @@ object Kitsu {
"https://kitsu.io/api/graphql", "https://kitsu.io/api/graphql",
headers = headers, headers = headers,
data = mapOf("query" to query) data = mapOf("query" to query)
).parsed() ).parsed<KitsuResponse>()
} }
private val cache: MutableMap<Pair<String, String>, Map<Int, KitsuResponse.Node>> = private val cache: MutableMap<Pair<String, String>, Map<Int, KitsuResponse.Node>> =
@ -102,44 +752,52 @@ query {
return map return map
} }
@Serializable
data class KitsuResponse( data class KitsuResponse(
val data: Data? = null @JsonProperty("data") @SerialName("data") val data: Data? = null,
) { ) {
@Serializable
data class Data( data class Data(
val lookupMapping: LookupMapping? = null @JsonProperty("lookupMapping") @SerialName("lookupMapping") val lookupMapping: LookupMapping? = null,
) )
@Serializable
data class LookupMapping( data class LookupMapping(
val id: String? = null, @JsonProperty("id") @SerialName("id") val id: String? = null,
val episodes: Episodes? = null @JsonProperty("episodes") @SerialName("episodes") val episodes: Episodes? = null,
) )
@Serializable
data class Episodes( data class Episodes(
val nodes: List<Node?>? = null @JsonProperty("nodes") @SerialName("nodes") val nodes: List<Node?>? = null,
) )
@Serializable
data class Node( data class Node(
@JsonProperty("number") @JsonProperty("number") @SerialName("number") val num: Int? = null,
val num: Int? = null, @JsonProperty("titles") @SerialName("titles") val titles: Titles? = null,
val titles: Titles? = null, @JsonProperty("description") @SerialName("description") val description: Description? = null,
val description: Description? = null, @JsonProperty("thumbnail") @SerialName("thumbnail") val thumbnail: Thumbnail? = null,
val thumbnail: Thumbnail? = null
) )
@Serializable
data class Description( data class Description(
val en: String? = null @JsonProperty("en") @SerialName("en") val en: String? = null,
) )
@Serializable
data class Thumbnail( data class Thumbnail(
val original: Original? = null @JsonProperty("original") @SerialName("original") val original: Original? = null,
) )
@Serializable
data class Original( data class Original(
val url: String? = null @JsonProperty("url") @SerialName("url") val url: String? = null,
) )
@Serializable
data class Titles( data class Titles(
val canonical: String? = null @JsonProperty("canonical") @SerialName("canonical") val canonical: String? = null,
) )
} }
} }

View file

@ -1,13 +1,11 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWork
@ -16,56 +14,19 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
import com.lagradost.cloudstream3.utils.txt
class LocalList : SyncAPI { class LocalList : SyncAPI() {
override val name = "Local" override val name = "Local"
override val idPrefix = "local"
override val icon: Int = R.drawable.ic_baseline_storage_24 override val icon: Int = R.drawable.ic_baseline_storage_24
override val requiresLogin = false override val requiresLogin = false
override val supportDeviceAuth = false override val createAccountUrl = null
override val createAccountUrl: Nothing? = null
override val idPrefix = "local"
override var requireLibraryRefresh = true override var requireLibraryRefresh = true
override fun loginInfo(): AuthAPI.LoginInfo {
return AuthAPI.LoginInfo(
null,
null,
0
)
}
override fun logOut() {
}
override val key: String = ""
override val redirectUrl = ""
override suspend fun handleRedirect(url: String): Boolean {
return true
}
override fun authenticate(activity: FragmentActivity?) {
}
override val mainUrl = ""
override val syncIdName = SyncIdName.LocalList override val syncIdName = SyncIdName.LocalList
override suspend fun score(id: String, status: SyncAPI.AbstractSyncStatus): Boolean {
return true
}
override suspend fun getStatus(id: String): SyncAPI.AbstractSyncStatus? { override suspend fun library(auth : AuthData?): SyncAPI.LibraryMetadata? {
return null
}
override suspend fun getResult(id: String): SyncAPI.SyncResult? {
return null
}
override suspend fun search(name: String): List<SyncAPI.SyncSearchResult>? {
return null
}
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata? {
val watchStatusIds = ioWork { val watchStatusIds = ioWork {
getAllWatchStateIds()?.map { id -> getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id)) Pair(id, getResultWatchState(id))
@ -102,7 +63,8 @@ class LocalList : SyncAPI {
val result = if (isTrueTv) { val result = if (isTrueTv) {
baseMap + watchStatusMap + favoritesMap baseMap + watchStatusMap + favoritesMap
} else { } else {
val subscriptionsMap = mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull { val subscriptionsMap =
mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem() it.toLibraryItem()
}) })
@ -112,8 +74,8 @@ class LocalList : SyncAPI {
result result
} }
return SyncAPI.LibraryMetadata( return LibraryMetadata(
list.map { SyncAPI.LibraryList(txt(it.key), it.value) }, list.map { LibraryList(txt(it.key), it.value) },
setOf( setOf(
ListSorting.AlphabeticalA, ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ, ListSorting.AlphabeticalZ,
@ -127,8 +89,4 @@ class LocalList : SyncAPI {
) )
) )
} }
override fun getIdFromUrl(url: String): String {
return url
}
} }

View file

@ -2,188 +2,112 @@ package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.ErrorLoadingException import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.syncproviders.AuthLoginResponse
import com.lagradost.cloudstream3.syncproviders.AuthToken
import com.lagradost.cloudstream3.syncproviders.AuthUser
import com.lagradost.cloudstream3.syncproviders.SubtitleAPI
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import okhttp3.Interceptor import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import okhttp3.Response import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToOpenSubtitlesTag
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { class OpenSubtitlesApi : SubtitleAPI() {
override val idPrefix = "opensubtitles"
override val name = "OpenSubtitles" override val name = "OpenSubtitles"
override val idPrefix = "opensubtitles"
override val icon = R.drawable.open_subtitles_icon override val icon = R.drawable.open_subtitles_icon
override val requiresPassword = true override val hasInApp = true
override val requiresUsername = true override val inAppLoginRequirement = AuthLoginRequirement(
password = true,
username = true,
)
override val createAccountUrl = "https://www.opensubtitles.com/en/users/sign_up" override val createAccountUrl = "https://www.opensubtitles.com/en/users/sign_up"
companion object { companion object {
const val OPEN_SUBTITLES_USER_KEY: String = "open_subtitles_user" // user data like profile
const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2" const val API_KEY = "uyBLgFD17MgrYmA0gSXoKllMJBelOYj2"
const val HOST = "https://api.opensubtitles.com/api/v1" const val HOST = "https://api.opensubtitles.com/api/v1"
const val TAG = "OPENSUBS" const val TAG = "OPENSUBS"
const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms const val COOLDOWN_DURATION: Long = 1000L * 30L // CoolDown if 429 error code in ms
var currentCoolDown: Long = 0L var currentCoolDown: Long = 0L
var currentSession: SubtitleOAuthEntity? = null const val userAgent = "Cloudstream3 v0.2"
} val headers = mapOf("user-agent" to userAgent, "Api-Key" to API_KEY)
private val headerInterceptor = OpenSubtitleInterceptor()
/** Automatically adds required api headers */
private class OpenSubtitleInterceptor : Interceptor {
/** Required user agent! */
private val userAgent = "Cloudstream3 v0.2"
override fun intercept(chain: Interceptor.Chain): Response {
return chain.proceed(
chain.request().newBuilder()
.removeHeader("user-agent")
.addHeader("user-agent", userAgent)
.addHeader("Api-Key", API_KEY)
.build()
)
}
} }
private fun canDoRequest(): Boolean { private fun canDoRequest(): Boolean {
return unixTimeMs > currentCoolDown return unixTimeMS > currentCoolDown
} }
private fun throwIfCantDoRequest() { private fun throwIfCantDoRequest() {
if (!canDoRequest()) { 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() { private fun throwGotTooManyRequests() {
currentCoolDown = unixTimeMs + COOLDOWN_DURATION currentCoolDown = unixTimeMS + COOLDOWN_DURATION
throw ErrorLoadingException("Too many requests") throw ErrorLoadingException("Too many requests")
} }
private fun getAuthKey(): SubtitleOAuthEntity? { override suspend fun refreshToken(token: AuthToken): AuthToken? {
return getKey(accountId, OPEN_SUBTITLES_USER_KEY) return login(parseJson<AuthLoginResponse>(token.payload ?: return null))
} }
private fun setAuthKey(data: SubtitleOAuthEntity?) { override suspend fun user(token: AuthToken?): AuthUser? {
if (data == null) removeKey(accountId, OPEN_SUBTITLES_USER_KEY) val user = parseJson<AuthLoginResponse>(token?.payload ?: return null)
currentSession = data val username = user.username ?: return null
setKey(accountId, OPEN_SUBTITLES_USER_KEY, data) return AuthUser(
} id = username.hashCode(),
name = username
override fun loginInfo(): AuthAPI.LoginInfo? {
getAuthKey()?.let { user ->
return AuthAPI.LoginInfo(
profilePicture = null,
name = user.user,
accountIndex = accountIndex
) )
} }
return null
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? { override suspend fun login(form: AuthLoginResponse): AuthToken? {
val current = getAuthKey() ?: return null val username = form.username ?: return null
return InAppAuthAPI.LoginData(username = current.user, current.pass) val password = form.password ?: return null
}
/*
Authorize app to connect to API, using username/password.
Required to run at startup.
Returns OAuth entity with valid access token.
*/
override suspend fun initialize() {
currentSession = getAuthKey() ?: return // just in case the following fails
initLogin(currentSession?.user ?: return, currentSession?.pass ?: return)
}
override fun logOut() {
setAuthKey(null)
removeAccountKeys()
currentSession = getAuthKey()
}
private suspend fun initLogin(username: String, password: String): Boolean {
//Log.i(TAG, "DATA = [$username] [$password]")
val response = app.post( val response = app.post(
url = "$HOST/login", url = "$HOST/login",
headers = mapOf( headers = mapOf(
"Content-Type" to "application/json", "Content-Type" to "application/json",
), ) + headers,
json = mapOf( json = mapOf(
"username" to username, "username" to username,
"password" to password "password" to password
), ),
interceptor = headerInterceptor ).parsed<OAuthToken>()
return AuthToken(
accessToken = response.token
?: throw ErrorLoadingException("Invalid password or username"),
/// JWT token is valid 24 hours after successfully authentication of user
accessTokenLifetime = APIHolder.unixTime + 60 * 60 * 24,
payload = form.toJson()
) )
//Log.i(TAG, "Responsecode = ${response.code}")
//Log.i(TAG, "Result => ${response.text}")
if (response.isSuccessful) {
AppUtils.tryParseJson<OAuthToken>(response.text)?.let { token ->
setAuthKey(
SubtitleOAuthEntity(
user = username,
pass = password,
accessToken = token.token ?: run {
return false
})
)
}
return true
}
return false
}
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
val username = data.username ?: throw ErrorLoadingException("Requires Username")
val password = data.password ?: throw ErrorLoadingException("Requires Password")
switchToNewAccount()
try {
if (initLogin(username, password)) {
registerAccount()
return true
}
} catch (e: Exception) {
logError(e)
switchToOldAccount()
}
switchToOldAccount()
return false
}
/**
* Some languages do not use the normal country codes on OpenSubtitles
* */
private val languageExceptions = mapOf<String, String>(
// "pt" to "pt-PT",
// "pt" to "pt-BR"
)
private fun fixLanguage(language: String?): String? {
return languageExceptions[language] ?: language
}
// O(n) but good enough, BiMap did not want to work properly
private fun fixLanguageReverse(language: String?): String? {
return languageExceptions.entries.firstOrNull { it.value == language }?.key ?: language
} }
/** /**
* Fetch subtitles using token authenticated on previous method (see authorize). * Fetch subtitles using token authenticated on previous method (see authorize).
* Returns list of Subtitles which user can select to download (see load). * Returns list of Subtitles which user can select to download (see load).
* */ */
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? { override suspend fun search(
auth : AuthData?,
query: AbstractSubtitleEntities.SubtitleSearch
): List<AbstractSubtitleEntities.SubtitleEntity>? {
throwIfCantDoRequest() throwIfCantDoRequest()
val fixedLang = fixLanguage(query.lang) val langOpenSubTag = fromCodeToOpenSubtitlesTag(query.lang) ?: query.lang ?: ""
val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0 val imdbId = query.imdbId?.replace("tt", "")?.toInt() ?: 0
val queryText = query.query val queryText = query.query
@ -196,17 +120,17 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val searchQueryUrl = when (imdbId > 0) { val searchQueryUrl = when (imdbId > 0) {
//Use imdb_id to search if its valid //Use imdb_id to search if its valid
true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" true -> "$HOST/subtitles?imdb_id=$imdbId&languages=${langOpenSubTag}$yearQuery$epQuery$seasonQuery"
false -> "$HOST/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" false -> "$HOST/subtitles?query=${queryText}&languages=${langOpenSubTag}$yearQuery$epQuery$seasonQuery"
} }
val req = app.get( val req = app.get(
url = searchQueryUrl, url = searchQueryUrl,
headers = mapOf( headers = mapOf(
Pair("Content-Type", "application/json") Pair("Content-Type", "application/json")
), ) + headers,
interceptor = headerInterceptor
) )
Log.i(TAG, "searchQueryUrl => ${searchQueryUrl}")
Log.i(TAG, "Search Req => ${req.text}") Log.i(TAG, "Search Req => ${req.text}")
if (!req.isSuccessful) { if (!req.isSuccessful) {
if (req.code == 429) if (req.code == 429)
@ -227,7 +151,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
//Use any valid name/title in hierarchy //Use any valid name/title in hierarchy
val name = filename ?: featureDetails?.movieName ?: featureDetails?.title val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: query.query ?: featureDetails?.parentTitle ?: attr.release ?: query.query
val lang = fixLanguageReverse(attr.language) ?: "" val langTagIETF = fromCodeToLangTagIETF(attr.language) ?: ""
val resEpNum = featureDetails?.episodeNumber ?: query.epNumber val resEpNum = featureDetails?.episodeNumber ?: query.epNumber
val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber val resSeasonNum = featureDetails?.seasonNumber ?: query.seasonNumber
val year = featureDetails?.year ?: query.year val year = featureDetails?.year ?: query.year
@ -241,7 +165,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
AbstractSubtitleEntities.SubtitleEntity( AbstractSubtitleEntities.SubtitleEntity(
idPrefix = this.idPrefix, idPrefix = this.idPrefix,
name = name, name = name,
lang = lang, lang = langTagIETF,
data = resultData, data = resultData,
type = type, type = type,
source = this.name, source = this.name,
@ -257,11 +181,15 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
return results return results
} }
/* /**
Process data returned from search. * Process data returned from search.
Returns string url for the subtitle file. * Returns string url for the subtitle file.
*/ */
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String? { override suspend fun load(
auth : AuthData?,
subtitle: AbstractSubtitleEntities.SubtitleEntity
): String? {
if (auth == null) return null
throwIfCantDoRequest() throwIfCantDoRequest()
val req = app.post( val req = app.post(
@ -269,15 +197,14 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
headers = mapOf( headers = mapOf(
Pair( Pair(
"Authorization", "Authorization",
"Bearer ${currentSession?.accessToken ?: throw ErrorLoadingException("No access token active in current session")}" "Bearer ${auth.token.accessToken ?: throw ErrorLoadingException("No access token active in current session")}"
), ),
Pair("Content-Type", "application/json"), Pair("Content-Type", "application/json"),
Pair("Accept", "*/*") Pair("Accept", "*/*")
), ) + headers,
data = mapOf( data = mapOf(
Pair("file_id", data.data) Pair("file_id", subtitle.data)
), )
interceptor = headerInterceptor
) )
Log.i(TAG, "Request result => (${req.code}) ${req.text}") Log.i(TAG, "Request result => (${req.code}) ${req.text}")
//Log.i(TAG, "Request headers => ${req.headers}") //Log.i(TAG, "Request headers => ${req.headers}")
@ -294,64 +221,64 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
return null return null
} }
@Serializable
data class SubtitleOAuthEntity(
var user: String,
var pass: String,
var accessToken: String,
)
data class OAuthToken( data class OAuthToken(
@JsonProperty("token") var token: String? = null, @JsonProperty("token") @SerialName("token") var token: String? = null,
@JsonProperty("status") var status: Int? = null @JsonProperty("status") @SerialName("status") var status: Int? = null,
) )
@Serializable
data class Results( data class Results(
@JsonProperty("data") var data: List<ResultData>? = listOf() @JsonProperty("data") @SerialName("data") var data: List<ResultData>? = listOf(),
) )
@Serializable
data class ResultData( data class ResultData(
@JsonProperty("id") var id: String? = null, @JsonProperty("id") @SerialName("id") var id: String? = null,
@JsonProperty("type") var type: String? = null, @JsonProperty("type") @SerialName("type") var type: String? = null,
@JsonProperty("attributes") var attributes: ResultAttributes? = ResultAttributes() @JsonProperty("attributes") @SerialName("attributes") var attributes: ResultAttributes? = ResultAttributes(),
) )
@Serializable
data class ResultAttributes( data class ResultAttributes(
@JsonProperty("subtitle_id") var subtitleId: String? = null, @JsonProperty("subtitle_id") @SerialName("subtitle_id") var subtitleId: String? = null,
@JsonProperty("language") var language: String? = null, @JsonProperty("language") @SerialName("language") var language: String? = null,
@JsonProperty("release") var release: String? = null, @JsonProperty("release") @SerialName("release") var release: String? = null,
@JsonProperty("url") var url: String? = null, @JsonProperty("url") @SerialName("url") var url: String? = null,
@JsonProperty("files") var files: List<ResultFiles>? = listOf(), @JsonProperty("files") @SerialName("files") var files: List<ResultFiles>? = listOf(),
@JsonProperty("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(), @JsonProperty("feature_details") @SerialName("feature_details") var featDetails: ResultFeatureDetails? = ResultFeatureDetails(),
@JsonProperty("hearing_impaired") var hearingImpaired: Boolean? = null, @JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Boolean? = null,
) )
@Serializable
data class ResultFiles( data class ResultFiles(
@JsonProperty("file_id") var fileId: Int? = null, @JsonProperty("file_id") @SerialName("file_id") var fileId: Int? = null,
@JsonProperty("file_name") var fileName: String? = null @JsonProperty("file_name") @SerialName("file_name") var fileName: String? = null,
) )
@Serializable
data class ResultDownloadLink( data class ResultDownloadLink(
@JsonProperty("link") var link: String? = null, @JsonProperty("link") @SerialName("link") var link: String? = null,
@JsonProperty("file_name") var fileName: String? = null, @JsonProperty("file_name") @SerialName("file_name") var fileName: String? = null,
@JsonProperty("requests") var requests: Int? = null, @JsonProperty("requests") @SerialName("requests") var requests: Int? = null,
@JsonProperty("remaining") var remaining: Int? = null, @JsonProperty("remaining") @SerialName("remaining") var remaining: Int? = null,
@JsonProperty("message") var message: String? = null, @JsonProperty("message") @SerialName("message") var message: String? = null,
@JsonProperty("reset_time") var resetTime: String? = null, @JsonProperty("reset_time") @SerialName("reset_time") var resetTime: String? = null,
@JsonProperty("reset_time_utc") var resetTimeUtc: String? = null @JsonProperty("reset_time_utc") @SerialName("reset_time_utc") var resetTimeUtc: String? = null,
) )
@Serializable
data class ResultFeatureDetails( data class ResultFeatureDetails(
@JsonProperty("year") var year: Int? = null, @JsonProperty("year") @SerialName("year") var year: Int? = null,
@JsonProperty("title") var title: String? = null, @JsonProperty("title") @SerialName("title") var title: String? = null,
@JsonProperty("movie_name") var movieName: String? = null, @JsonProperty("movie_name") @SerialName("movie_name") var movieName: String? = null,
@JsonProperty("imdb_id") var imdbId: Int? = null, @JsonProperty("imdb_id") @SerialName("imdb_id") var imdbId: Int? = null,
@JsonProperty("tmdb_id") var tmdbId: Int? = null, @JsonProperty("tmdb_id") @SerialName("tmdb_id") var tmdbId: Int? = null,
@JsonProperty("season_number") var seasonNumber: Int? = null, @JsonProperty("season_number") @SerialName("season_number") var seasonNumber: Int? = null,
@JsonProperty("episode_number") var episodeNumber: Int? = null, @JsonProperty("episode_number") @SerialName("episode_number") var episodeNumber: Int? = null,
@JsonProperty("parent_imdb_id") var parentImdbId: Int? = null, @JsonProperty("parent_imdb_id") @SerialName("parent_imdb_id") var parentImdbId: Int? = null,
@JsonProperty("parent_title") var parentTitle: String? = null, @JsonProperty("parent_title") @SerialName("parent_title") var parentTitle: String? = null,
@JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null, @JsonProperty("parent_tmdb_id") @SerialName("parent_tmdb_id") var parentTmdbId: Int? = null,
@JsonProperty("parent_feature_id") var parentFeatureId: Int? = null @JsonProperty("parent_feature_id") @SerialName("parent_feature_id") var parentFeatureId: Int? = null,
) )
} }

View file

@ -3,157 +3,202 @@ package com.lagradost.cloudstream3.syncproviders.providers
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubProvider
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.subtitles.SubtitleResource import com.lagradost.cloudstream3.subtitles.SubtitleResource
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.syncproviders.SubtitleAPI
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.concurrent.TimeUnit
class SubSourceApi : AbstractSubProvider { class SubSourceApi : SubtitleAPI() {
override val name = "SubSource"
override val idPrefix = "subsource" override val idPrefix = "subsource"
val name = "SubSource"
override val requiresLogin = false
companion object { companion object {
const val APIURL = "https://api.subsource.net/api" const val APIURL = "https://api.subsource.net/v1"
const val DOWNLOADENDPOINT = "https://api.subsource.net/api/downloadSub"
} }
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? { override suspend fun search(
auth: AuthData?,
query: AbstractSubtitleEntities.SubtitleSearch
): List<AbstractSubtitleEntities.SubtitleEntity>? {
//Only supports Imdb Id search for now //Only supports Imdb Id search for now
if (query.imdbId == null) return null if (query.imdbId == null) return null
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(query.lang!!) val queryLang = SubtitleHelper.fromTagToEnglishLanguageName(query.lang)
val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie val type = if ((query.seasonNumber ?: 0) > 0) TvType.TvSeries else TvType.Movie
val searchRes = app.post( val searchResponse = app.post(
url = "$APIURL/searchMovie", url = "$APIURL/movie/search",
data = mapOf( json = mapOf(
"query" to query.imdbId!! "includeSeasons" to false,
) "limit" to 15,
).parsedSafe<ApiSearch>() ?: return null "query" to query.imdbId!!,
"signal" to "{}"
),
cacheTime = 120,
cacheUnit = TimeUnit.MINUTES,
).parsedSafe<SearchRoot>() ?: return null
val postData = if (type == TvType.TvSeries) { val firstResult = searchResponse.results.firstOrNull() ?: return null
mapOf(
"langs" to "[]", val apiResponse = app.get(
"movieName" to searchRes.found.first().linkName, url = "$APIURL${firstResult.link.replace("series", "subtitles")}",
"season" to "season-${query.seasonNumber}" cacheTime = 120,
) cacheUnit = TimeUnit.MINUTES,
} else { ).parsedSafe<ItemRoot>() ?: return null
mapOf(
"langs" to "[]", val filteredSubtitles = apiResponse.subtitles.filter { sub ->
"movieName" to searchRes.found.first().linkName, sub.releaseType != "trailer" &&
) sub.language.equals(queryLang, true)
} }
val getMovieRes = app.post(
url = "$APIURL/getMovie",
data = postData
).parsedSafe<ApiResponse>().let {
// api doesn't has episode number or lang filtering // api doesn't has episode number or lang filtering
if (type == TvType.Movie) { val subtitles = if (type == TvType.Movie) {
it?.subs?.filter { sub -> filteredSubtitles
sub.lang == queryLang
}
} else { } else {
it?.subs?.filter { sub -> val shouldContain = String.format(
sub.releaseName!!.contains(
String.format(
null, null,
"E%02d", "E%02d",
query.epNumber query.epNumber
) )
) && sub.lang == queryLang filteredSubtitles.filter { sub ->
sub.releaseInfo.contains(
shouldContain
)
} }
} }
} ?: return null
return getMovieRes.map { subtitle -> return subtitles.map { subtitle ->
AbstractSubtitleEntities.SubtitleEntity( AbstractSubtitleEntities.SubtitleEntity(
idPrefix = this.idPrefix, idPrefix = this.idPrefix,
name = subtitle.releaseName!!, name = subtitle.releaseInfo,
lang = subtitle.lang!!, lang = subtitle.language,
data = SubData( data = subtitle.link,
movie = subtitle.linkName!!,
lang = subtitle.lang,
id = subtitle.subId.toString(),
).toJson(),
type = type, type = type,
source = this.name, source = this.name,
epNumber = query.epNumber, epNumber = query.epNumber,
seasonNumber = query.seasonNumber, seasonNumber = query.seasonNumber,
isHearingImpaired = subtitle.hi == 1, isHearingImpaired = subtitle.hearingImpaired == 1,
) )
} }
} }
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { override suspend fun SubtitleResource.getResources(
auth: AuthData?,
val parsedSub = parseJson<SubData>(data.data) subtitle: AbstractSubtitleEntities.SubtitleEntity
) {
val subRes = app.post( val data = app.get("$APIURL/subtitle/${subtitle.data}")
url = "$APIURL/getSub", .parsedSafe<DownloadRoot>()
data = mapOf( ?: return
"movie" to parsedSub.movie,
"lang" to data.lang,
"id" to parsedSub.id
)
).parsedSafe<SubTitleLink>() ?: return
this.addZipUrl( this.addZipUrl(
"$DOWNLOADENDPOINT/${subRes.sub.downloadToken}" "$APIURL/subtitle/download/${data.subtitle.downloadToken}"
) { name, _ -> ) { name, _ ->
name name
} }
} }
data class ApiSearch(
@JsonProperty("success") val success: Boolean, @Serializable
@JsonProperty("found") val found: List<Found>, data class SearchRoot(
@JsonProperty("success") @SerialName("success") var success: Boolean? = null,
@JsonProperty("results") @SerialName("results") var results: ArrayList<Results> = arrayListOf(),
@JsonProperty("users") @SerialName("users") var users: ArrayList<Users> = arrayListOf()
) )
data class Found( @Serializable
@JsonProperty("id") val id: Long, data class Users(
@JsonProperty("title") val title: String,
@JsonProperty("seasons") val seasons: Long, @JsonProperty("id") @SerialName("id") var id: Int? = null,
@JsonProperty("type") val type: String, @JsonProperty("displayname") @SerialName("displayname") var displayname: String? = null,
@JsonProperty("releaseYear") val releaseYear: Long, @JsonProperty("avatar") @SerialName("avatar") var avatar: String? = null,
@JsonProperty("linkName") val linkName: String, @JsonProperty("badges") @SerialName("badges") var badges: ArrayList<String> = arrayListOf()
) )
data class ApiResponse( @Serializable
@JsonProperty("success") val success: Boolean, data class Results(
@JsonProperty("movie") val movie: Movie, @JsonProperty("id") @SerialName("id") var id: Int? = null,
@JsonProperty("subs") val subs: List<Sub>, @JsonProperty("title") @SerialName("title") var title: String? = null,
@JsonProperty("type") @SerialName("type") var type: String? = null,
@JsonProperty("link") @SerialName("link") var link: String,
@JsonProperty("releaseYear") @SerialName("releaseYear") var releaseYear: Int? = null,
@JsonProperty("poster") @SerialName("poster") var poster: String? = null,
@JsonProperty("subtitleCount") @SerialName("subtitleCount") var subtitleCount: String? = null,
@JsonProperty("rating") @SerialName("rating") var rating: Double? = null,
@JsonProperty("cast") @SerialName("cast") var cast: ArrayList<String> = arrayListOf(),
@JsonProperty("genres") @SerialName("genres") var genres: ArrayList<String> = arrayListOf(),
@JsonProperty("score") @SerialName("score") var score: Double? = null
) )
data class Movie( @Serializable
@JsonProperty("id") val id: Long? = null,
@JsonProperty("type") val type: String? = null, data class ItemRoot(
@JsonProperty("year") val year: Long? = null,
@JsonProperty("fullName") val fullName: String? = null, // @SerialName("media_type" ) var mediaType : String? = null,
@JsonProperty("subtitles") @SerialName("subtitles") var subtitles: ArrayList<Subtitles>,
//@SerialName("movie" ) var movie : Movie? = Movie()
) )
data class Sub( @Serializable
@JsonProperty("hi") val hi: Int? = null, data class Subtitles(
@JsonProperty("fullLink") val fullLink: String? = null,
@JsonProperty("linkName") val linkName: String? = null, @JsonProperty("id") @SerialName("id") var id: Int? = null,
@JsonProperty("lang") val lang: String? = null, @JsonProperty("language") @SerialName("language") var language: String,
@JsonProperty("releaseName") val releaseName: String? = null, @JsonProperty("release_type") @SerialName("release_type") var releaseType: String? = null,
@JsonProperty("subId") val subId: Long? = null, @JsonProperty("release_info") @SerialName("release_info") var releaseInfo: String,
@JsonProperty("upload_date") @SerialName("upload_date") var uploadDate: String? = null,
@JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Int? = null,
@JsonProperty("caption") @SerialName("caption") var caption: String? = null,
@JsonProperty("rating") @SerialName("rating") var rating: String? = null,
@JsonProperty("uploader_id") @SerialName("uploader_id") var uploaderId: Int? = null,
@JsonProperty("uploader_displayname") @SerialName("uploader_displayname") var uploaderDisplayname: String? = null,
@JsonProperty("uploader_badges") @SerialName("uploader_badges") var uploaderBadges: ArrayList<String> = arrayListOf(),
@JsonProperty("link") @SerialName("link") var link: String,
@JsonProperty("production_type") @SerialName("production_type") var productionType: String? = null,
@JsonProperty("last_subtitle") @SerialName("last_subtitle") var lastSubtitle: Boolean? = null
) )
data class SubData( @Serializable
@JsonProperty("movie") val movie: String, data class DownloadRoot(
@JsonProperty("lang") val lang: String, @JsonProperty("subtitle") @SerialName("subtitle") var subtitle: Subtitle,
@JsonProperty("id") val id: String, //@SerializedName("movie" ) var movie : Movie? = Movie(),
//@SerializedName("donationLinks" ) var donationLinks : DonationLinks? = DonationLinks(),
//@SerializedName("isDownloaded" ) var isDownloaded : Boolean? = null,
//@SerializedName("user_rated" ) var userRated : String? = null
) )
data class SubTitleLink( @Serializable
@JsonProperty("sub") val sub: SubToken, data class Subtitle(
)
@JsonProperty("id") @SerialName("id") var id: Int? = null,
@JsonProperty("uploaded_at") @SerialName("uploaded_at") var uploadedAt: String? = null,
@JsonProperty("language") @SerialName("language") var language: String? = null,
@JsonProperty("rating") @SerialName("rating") var rating: String? = null,
//SerialName("rates" ) var rates : Rates? = Rates(),
@JsonProperty("uploaded_by") @SerialName("uploaded_by") var uploadedBy: Int? = null,
//@SerialName("contribs" ) var contribs : ArrayList<Contribs> = arrayListOf(),
@JsonProperty("release_info") @SerialName("release_info") var releaseInfo: ArrayList<String> = arrayListOf(),
@JsonProperty("commentary") @SerialName("commentary") var commentary: String? = null,
@JsonProperty("files") @SerialName("files") var files: String? = null,
@JsonProperty("size") @SerialName("size") var size: String? = null,
@JsonProperty("downloads") @SerialName("downloads") var downloads: Int? = null,
@JsonProperty("comments") @SerialName("comments") var comments: Int? = null,
@JsonProperty("production_type") @SerialName("production_type") var productionType: String? = null,
@JsonProperty("release_type") @SerialName("release_type") var releaseType: String? = null,
@JsonProperty("episode") @SerialName("episode") var episode: String? = null,
@JsonProperty("hearing_impaired") @SerialName("hearing_impaired") var hearingImpaired: Int? = null,
@JsonProperty("foreign_parts") @SerialName("foreign_parts") var foreignParts: String? = null,
@JsonProperty("framerate") @SerialName("framerate") var framerate: String? = null,
@JsonProperty("preview") @SerialName("preview") var preview: String? = null,
@JsonProperty("user_uploaded") @SerialName("user_uploaded") var userUploaded: Boolean? = null,
@JsonProperty("download_token") @SerialName("download_token") var downloadToken: String
data class SubToken(
@JsonProperty("downloadToken") val downloadToken: String,
) )
} }

View file

@ -1,89 +1,73 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.subtitles.SubtitleResource import com.lagradost.cloudstream3.subtitles.SubtitleResource
import com.lagradost.cloudstream3.syncproviders.AuthAPI.LoginInfo import com.lagradost.cloudstream3.syncproviders.AuthData
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.AuthLoginRequirement
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.syncproviders.AuthLoginResponse
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(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { class SubDlApi : SubtitleAPI() {
override val idPrefix = "subdl"
override val name = "SubDL" override val name = "SubDL"
override val idPrefix = "subdl"
override val icon = R.drawable.subdl_logo_big override val icon = R.drawable.subdl_logo_big
override val requiresPassword = true override val hasInApp = true
override val requiresEmail = true override val inAppLoginRequirement = AuthLoginRequirement(password = true, email = true)
override val requiresLogin = true
override val createAccountUrl = "https://subdl.com/panel/register" override val createAccountUrl = "https://subdl.com/panel/register"
companion object { 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 APIENDPOINT = "$APIURL/api/v1/subtitles"
const val DOWNLOADENDPOINT = "https://dl.subdl.com" const val DOWNLOADENDPOINT = "https://dl.subdl.com"
const val SUBDL_SUBTITLES_USER_KEY: String = "subdl_user"
var currentSession: SubtitleOAuthEntity? = null
} }
override suspend fun initialize() { override suspend fun login(form: AuthLoginResponse): AuthToken? {
currentSession = getAuthKey() val email = form.email ?: return null
} val password = form.password ?: return null
val tokenResponse = app.post(
override fun logOut() { url = "$APIURL/login",
setAuthKey(null) json = mapOf(
removeAccountKeys() "email" to email,
currentSession = getAuthKey() "password" to password
}
override suspend fun login(data: InAppAuthAPI.LoginData): Boolean {
val email = data.email ?: throw ErrorLoadingException("Requires Email")
val password = data.password ?: throw ErrorLoadingException("Requires Password")
switchToNewAccount()
try {
if (initLogin(email, password)) {
registerAccount()
return true
}
} catch (e: Exception) {
logError(e)
switchToOldAccount()
}
switchToOldAccount()
return false
}
override fun getLatestLoginData(): InAppAuthAPI.LoginData? {
val current = getAuthKey() ?: return null
return InAppAuthAPI.LoginData(
email = current.userEmail,
password = current.pass
) )
} ).parsed<OAuthTokenResponse>()
override fun loginInfo(): LoginInfo? { val apiResponse = app.get(
getAuthKey()?.let { user -> url = "$APIURL/user/userApi",
return LoginInfo( headers = mapOf(
profilePicture = null, "Authorization" to "Bearer ${tokenResponse.token}"
name = user.name ?: user.userEmail,
accountIndex = accountIndex
) )
} ).parsed<ApiKeyResponse>()
return null
return AuthToken(accessToken = apiResponse.apiKey, payload = email)
} }
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity>? { override suspend fun user(token: AuthToken?): AuthUser? {
val name = token?.payload ?: return null
return AuthUser(id = name.hashCode(), name = name)
}
override suspend fun search(
auth : AuthData?,
query: AbstractSubtitleEntities.SubtitleSearch
): List<AbstractSubtitleEntities.SubtitleEntity>? {
if (auth == null) return null
val apiKey = auth.token.accessToken ?: return null
val queryText = query.query val queryText = query.query
val epNum = query.epNumber ?: 0 val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0 val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0 val yearNum = query.year ?: 0
val langSubdlCode = langTagIETF2subdl[query.lang.toString()] ?: query.lang
val idQuery = when { val idQuery = when {
query.imdbId != null -> "&imdb_id=${query.imdbId}" query.imdbId != null -> "&imdb_id=${query.imdbId}"
@ -97,8 +81,8 @@ class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
val searchQueryUrl = when (idQuery) { val searchQueryUrl = when (idQuery) {
//Use imdb/tmdb id to search if its valid //Use imdb/tmdb id to search if its valid
null -> "$APIENDPOINT?api_key=${currentSession?.apiKey}&film_name=$queryText&languages=${query.lang}$epQuery$seasonQuery$yearQuery" null -> "$APIENDPOINT?api_key=${apiKey}&film_name=$queryText&languages=$langSubdlCode$epQuery$seasonQuery$yearQuery"
else -> "$APIENDPOINT?api_key=${currentSession?.apiKey}$idQuery&languages=${query.lang}$epQuery$seasonQuery$yearQuery" else -> "$APIENDPOINT?api_key=${apiKey}$idQuery&languages=$langSubdlCode$epQuery$seasonQuery$yearQuery"
} }
val req = app.get( val req = app.get(
@ -110,7 +94,9 @@ class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
return req.parsedSafe<ApiResponse>()?.subtitles?.map { subtitle -> return req.parsedSafe<ApiResponse>()?.subtitles?.map { subtitle ->
val lang = subtitle.lang.replaceFirstChar { it.uppercase() } val langTagIETF =
langTagIETF2subdl.entries.find { it.value == subtitle.lang }?.key ?:
subtitle.lang
val resEpNum = subtitle.episode ?: query.epNumber val resEpNum = subtitle.episode ?: query.epNumber
val resSeasonNum = subtitle.season ?: query.seasonNumber val resSeasonNum = subtitle.season ?: query.seasonNumber
val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie val type = if ((resSeasonNum ?: 0) > 0) TvType.TvSeries else TvType.Movie
@ -118,7 +104,7 @@ class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
AbstractSubtitleEntities.SubtitleEntity( AbstractSubtitleEntities.SubtitleEntity(
idPrefix = this.idPrefix, idPrefix = this.idPrefix,
name = subtitle.releaseName, name = subtitle.releaseName,
lang = lang, lang = langTagIETF,
data = "${DOWNLOADENDPOINT}${subtitle.url}", data = "${DOWNLOADENDPOINT}${subtitle.url}",
type = type, type = type,
source = this.name, source = this.name,
@ -129,120 +115,155 @@ class SubDlApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
} }
} }
override suspend fun SubtitleResource.getResources(data: AbstractSubtitleEntities.SubtitleEntity) { override suspend fun SubtitleResource.getResources(
this.addZipUrl(data.data) { name, _ -> auth: AuthData?,
subtitle: AbstractSubtitleEntities.SubtitleEntity
) {
this.addZipUrl(subtitle.data) { name, _ ->
name name
} }
} }
private suspend fun initLogin(useremail: String, password: String): Boolean { @Serializable
val tokenResponse = app.post(
url = "$APIURL/login",
json = mapOf(
"email" to useremail,
"password" to password
)
).parsedSafe<OAuthTokenResponse>()
if (tokenResponse?.token == null) return false
val apiResponse = app.get(
url = "$APIURL/user/userApi",
headers = mapOf(
"Authorization" to "Bearer ${tokenResponse.token}"
)
).parsedSafe<ApiKeyResponse>()
if (apiResponse?.ok == false) return false
setAuthKey(
SubtitleOAuthEntity(
userEmail = useremail,
pass = password,
name = tokenResponse.userData?.username ?: tokenResponse.userData?.name,
accessToken = tokenResponse.token,
apiKey = apiResponse?.apiKey
)
)
return true
}
private fun getAuthKey(): SubtitleOAuthEntity? {
return getKey(accountId, SUBDL_SUBTITLES_USER_KEY)
}
private fun setAuthKey(data: SubtitleOAuthEntity?) {
if (data == null) removeKey(
accountId,
SUBDL_SUBTITLES_USER_KEY
)
currentSession = data
setKey(accountId, SUBDL_SUBTITLES_USER_KEY, data)
}
data class SubtitleOAuthEntity( data class SubtitleOAuthEntity(
@JsonProperty("userEmail") var userEmail: String, @JsonProperty("userEmail") @SerialName("userEmail") var userEmail: String,
@JsonProperty("pass") var pass: String, @JsonProperty("pass") @SerialName("pass") var pass: String,
@JsonProperty("name") var name: String? = null, @JsonProperty("name") @SerialName("name") var name: String? = null,
@JsonProperty("accessToken") var accessToken: String? = null, @JsonProperty("accessToken") @SerialName("accessToken") var accessToken: String? = null,
@JsonProperty("apiKey") var apiKey: String? = null, @JsonProperty("apiKey") @SerialName("apiKey") var apiKey: String? = null,
) )
@Serializable
data class OAuthTokenResponse( data class OAuthTokenResponse(
@JsonProperty("token") val token: String? = null, @JsonProperty("token") @SerialName("token") val token: String,
@JsonProperty("userData") val userData: UserData? = null, @JsonProperty("userData") @SerialName("userData") val userData: UserData? = null,
@JsonProperty("status") val status: Boolean? = null, @JsonProperty("status") @SerialName("status") val status: Boolean? = null,
@JsonProperty("message") val message: String? = null, @JsonProperty("message") @SerialName("message") val message: String? = null,
) )
@Serializable
data class UserData( data class UserData(
@JsonProperty("email") val email: String, @JsonProperty("email") @SerialName("email") val email: String,
@JsonProperty("name") val name: String, @JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("country") val country: String, @JsonProperty("country") @SerialName("country") val country: String,
@JsonProperty("scStepCode") val scStepCode: String, @JsonProperty("scStepCode") @SerialName("scStepCode") val scStepCode: String,
@JsonProperty("scVerified") val scVerified: Boolean, @JsonProperty("scVerified") @SerialName("scVerified") val scVerified: Boolean,
@JsonProperty("username") val username: String? = null, @JsonProperty("username") @SerialName("username") val username: String? = null,
@JsonProperty("scUsername") val scUsername: String, @JsonProperty("scUsername") @SerialName("scUsername") val scUsername: String,
) )
@Serializable
data class ApiKeyResponse( data class ApiKeyResponse(
@JsonProperty("ok") val ok: Boolean? = false, @JsonProperty("ok") @SerialName("ok") val ok: Boolean? = false,
@JsonProperty("api_key") val apiKey: String? = null, @JsonProperty("api_key") @SerialName("api_key") val apiKey: String,
@JsonProperty("usage") val usage: Usage? = null, @JsonProperty("usage") @SerialName("usage") val usage: Usage? = null,
) )
@Serializable
data class Usage( data class Usage(
@JsonProperty("total") val total: Long? = 0, @JsonProperty("total") @SerialName("total") val total: Long? = 0,
@JsonProperty("today") val today: Long? = 0, @JsonProperty("today") @SerialName("today") val today: Long? = 0,
) )
@Serializable
data class ApiResponse( data class ApiResponse(
@JsonProperty("status") val status: Boolean? = null, @JsonProperty("status") @SerialName("status") val status: Boolean? = null,
@JsonProperty("results") val results: List<Result>? = null, @JsonProperty("results") @SerialName("results") val results: List<Result>? = null,
@JsonProperty("subtitles") val subtitles: List<Subtitle>? = null, @JsonProperty("subtitles") @SerialName("subtitles") val subtitles: List<Subtitle>? = null,
) )
@Serializable
data class Result( data class Result(
@JsonProperty("sd_id") val sdId: Int? = null, @JsonProperty("sd_id") @SerialName("sd_id") val sdId: Int? = null,
@JsonProperty("type") val type: String? = null, @JsonProperty("type") @SerialName("type") val type: String? = null,
@JsonProperty("name") val name: String? = null, @JsonProperty("name") @SerialName("name") val name: String? = null,
@JsonProperty("imdb_id") val imdbId: String? = null, @JsonProperty("imdb_id") @SerialName("imdb_id") val imdbId: String? = null,
@JsonProperty("tmdb_id") val tmdbId: Long? = null, @JsonProperty("tmdb_id") @SerialName("tmdb_id") val tmdbId: Long? = null,
@JsonProperty("first_air_date") val firstAirDate: String? = null, @JsonProperty("first_air_date") @SerialName("first_air_date") val firstAirDate: String? = null,
@JsonProperty("year") val year: Int? = null, @JsonProperty("year") @SerialName("year") val year: Int? = null,
) )
@Serializable
data class Subtitle( data class Subtitle(
@JsonProperty("release_name") val releaseName: String, @JsonProperty("release_name") @SerialName("release_name") val releaseName: String,
@JsonProperty("name") val name: String, @JsonProperty("name") @SerialName("name") val name: String,
@JsonProperty("lang") val lang: String, @JsonProperty("lang") @SerialName("lang") val lang: String, // subdl language code
@JsonProperty("author") val author: String? = null, @JsonProperty("author") @SerialName("author") val author: String? = null,
@JsonProperty("url") val url: String? = null, @JsonProperty("url") @SerialName("url") val url: String? = null,
@JsonProperty("subtitlePage") val subtitlePage: String? = null, @JsonProperty("subtitlePage") @SerialName("subtitlePage") val subtitlePage: String? = null,
@JsonProperty("season") val season: Int? = null, @JsonProperty("season") @SerialName("season") val season: Int? = null,
@JsonProperty("episode") val episode: Int? = null, @JsonProperty("episode") @SerialName("episode") val episode: Int? = null,
@JsonProperty("language") val language: String? = null, @JsonProperty("language") @SerialName("language") val language: String? = null, // full language name
@JsonProperty("hi") val hearingImpaired: Boolean? = null, @JsonProperty("hi") @SerialName("hi") val hearingImpaired: Boolean? = null,
)
// 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(
"en-bg" to "BG_EN", // "Bulgarian_English"
"en-de" to "EN_DE", // "English_German"
"en-hu" to "HU_EN", // "Hungarian_English"
"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"
) )
} }

View file

@ -9,23 +9,29 @@ import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainPageRequest import com.lagradost.cloudstream3.MainPageRequest
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponseList
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.fixUrl import com.lagradost.cloudstream3.fixUrl
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf import com.lagradost.cloudstream3.newSearchResponseList
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeout
class APIRepository(val api: MainAPI) { class APIRepository(val api: MainAPI) {
companion object { companion object {
// 2 minute timeout to prevent bad extensions/extractors from hogging the resources
// No real provider should take longer, so we hard kill them.
private const val DEFAULT_TIMEOUT = 120_000L
private const val MAX_TIMEOUT = 4 * DEFAULT_TIMEOUT
private const val MIN_TIMEOUT = 5_000L
var dubStatusActive = HashSet<DubStatus>() var dubStatusActive = HashSet<DubStatus>()
val noneApi = object : MainAPI() { val noneApi = object : MainAPI() {
@ -49,18 +55,20 @@ class APIRepository(val api: MainAPI) {
val hash: Pair<String, String> val hash: Pair<String, String>
) )
private val cache = threadSafeListOf<SavedLoadResponse>() private val cache = atomicListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0 private var cacheIndex: Int = 0
const val CACHE_SIZE = 20 const val CACHE_SIZE = 20
fun getTimeout(desired: Long?): Long {
return (desired ?: DEFAULT_TIMEOUT).coerceIn(MIN_TIMEOUT, MAX_TIMEOUT)
}
} }
private fun afterPluginsLoaded(forceReload: Boolean) { private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) { if (forceReload) {
synchronized(cache) {
cache.clear() cache.clear()
} }
} }
}
init { init {
afterPluginsLoadedEvent += ::afterPluginsLoaded afterPluginsLoadedEvent += ::afterPluginsLoaded
@ -76,25 +84,30 @@ class APIRepository(val api: MainAPI) {
suspend fun load(url: String): Resource<LoadResponse> { suspend fun load(url: String): Resource<LoadResponse> {
return safeApiCall { return safeApiCall {
withTimeout(getTimeout(api.loadTimeoutMs)) {
if (isInvalidData(url)) throw ErrorLoadingException() if (isInvalidData(url)) throw ErrorLoadingException()
val fixedUrl = api.fixUrl(url) val fixedUrl = api.fixUrl(url)
val lookingForHash = Pair(api.name, fixedUrl) val lookingForHash = Pair(api.name, fixedUrl)
synchronized(cache) { val cached = cache.withLock {
var found: LoadResponse? = null
for (item in cache) { for (item in cache) {
// 10 min save // 10 min save
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) { if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
return@safeApiCall item.response found = item.response
break
} }
} }
found
} }
if (cached != null) return@withTimeout cached
api.load(fixedUrl)?.also { response -> api.load(fixedUrl)?.also { response ->
// Remove all blank tags as early as possible // Remove all blank tags as early as possible
response.tags = response.tags?.filter { it.isNotBlank() } response.tags = response.tags?.filter { it.isNotBlank() }
val add = SavedLoadResponse(unixTime, response, lookingForHash) val add = SavedLoadResponse(unixTime, response, lookingForHash)
synchronized(cache) { cache.withLock {
if (cache.size > CACHE_SIZE) { if (cache.size > CACHE_SIZE) {
cache[cacheIndex] = add // rolling cache cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % CACHE_SIZE cacheIndex = (cacheIndex + 1) % CACHE_SIZE
@ -105,25 +118,32 @@ class APIRepository(val api: MainAPI) {
} ?: throw ErrorLoadingException() } ?: throw ErrorLoadingException()
} }
} }
}
suspend fun search(query: String): Resource<List<SearchResponse>> { suspend fun search(query: String, page: Int): Resource<SearchResponseList> {
if (query.isEmpty()) if (query.isEmpty())
return Resource.Success(emptyList()) return Resource.Success(newSearchResponseList(emptyList()))
return safeApiCall { return safeApiCall {
return@safeApiCall (api.search(query) withTimeout(getTimeout(api.searchTimeoutMs)) {
(api.search(query, page)
?: throw ErrorLoadingException()) ?: throw ErrorLoadingException())
// .filter { typesActive.contains(it.type) } // .filter { typesActive.contains(it.type) }
.toList() }
} }
} }
suspend fun quickSearch(query: String): Resource<List<SearchResponse>> { suspend fun quickSearch(query: String): Resource<SearchResponseList> {
if (query.isEmpty()) if (query.isEmpty())
return Resource.Success(emptyList()) return Resource.Success(newSearchResponseList(emptyList()))
return safeApiCall { return safeApiCall {
api.quickSearch(query) ?: throw ErrorLoadingException() withTimeout(getTimeout(api.quickSearchTimeoutMs)) {
newSearchResponseList(
api.quickSearch(query) ?: throw ErrorLoadingException(),
false
)
}
} }
} }
@ -133,9 +153,9 @@ class APIRepository(val api: MainAPI) {
delay(delta) delay(delta)
} }
@OptIn(DelicateCoroutinesApi::class)
suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> { suspend fun getMainPage(page: Int, nameIndex: Int? = null): Resource<List<HomePageResponse?>> {
return safeApiCall { return safeApiCall {
withTimeout(getTimeout(api.getMainPageTimeoutMs)) {
api.lastHomepageRequest = unixTimeMS api.lastHomepageRequest = unixTimeMS
nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data -> nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data ->
@ -173,6 +193,7 @@ class APIRepository(val api: MainAPI) {
} }
} }
} }
}
suspend fun extractorVerifierJob(extractorData: String?) { suspend fun extractorVerifierJob(extractorData: String?) {
safeApiCall { safeApiCall {
@ -188,7 +209,9 @@ class APIRepository(val api: MainAPI) {
): Boolean { ): Boolean {
if (isInvalidData(data)) return false // this makes providers cleaner if (isInvalidData(data)) return false // this makes providers cleaner
return try { return try {
withTimeout(getTimeout(api.loadLinksTimeoutMs)) {
api.loadLinks(data, isCasting, subtitleCallback, callback) api.loadLinks(data, isCasting, subtitleCallback, callback)
}
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
logError(throwable) logError(throwable)
return false return false

View file

@ -1,34 +1,55 @@
package com.lagradost.cloudstream3.ui package com.lagradost.cloudstream3.ui
import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModel
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import coil3.dispose
import java.util.WeakHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
open class ViewHolderState<T>(val view: ViewBinding) : ViewHolder(view.root) { open class ViewHolderState<T>(val view: ViewBinding) : ViewHolder(view.root) {
open fun save(): T? = null open fun save(): T? = null
open fun restore(state: T) = Unit open fun restore(state: T) = Unit
open fun onViewAttachedToWindow() = Unit
open fun onViewDetachedFromWindow() = Unit
open fun onViewRecycled() = Unit
} }
abstract class NoStateAdapter<T : Any>(
diffCallback: DiffUtil.ItemCallback<T> = BaseDiffCallback()
) : BaseAdapter<T, Any>(0, diffCallback)
// Based of the concept https://github.com/brahmkshatriya/echo/blob/main/app%2Fsrc%2Fmain%2Fjava%2Fdev%2Fbrahmkshatriya%2Fecho%2Fui%2Fadapters%2FMediaItemsContainerAdapter.kt#L108-L154 /** Creates a new shared pool, using the supplied lambda as a constructor.
class StateViewModel : ViewModel() { *
val layoutManagerStates = hashMapOf<Int, HashMap<Int, Any?>>() * The reason for this complicated structure is that a pool should not be shared between contexts
* as it makes coil fuck up, and theming.
* */
fun newSharedPool(lambda: RecyclerView.RecycledViewPool.() -> Unit = { }): Pair<WeakHashMap<Context, RecyclerView.RecycledViewPool>, RecyclerView.RecycledViewPool.() -> Unit> =
WeakHashMap<Context, RecyclerView.RecycledViewPool>() to lambda
/** Sets the shared pool of the recyclerview */
fun RecyclerView.setRecycledViewPool(pool: Pair<WeakHashMap<Context, RecyclerView.RecycledViewPool>, RecyclerView.RecycledViewPool.() -> Unit>) {
val ctx = context ?: return
synchronized(pool.first) {
this.setRecycledViewPool(pool.first.getOrPut(ctx) {
RecyclerView.RecycledViewPool().apply(pool.second)
})
}
} }
abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>(fragment, 0) /** Clears the shared pool of views */
fun Pair<WeakHashMap<Context, RecyclerView.RecycledViewPool>, RecyclerView.RecycledViewPool.() -> Unit>.clear() {
synchronized(this.first) {
for (pool in this.first.values) {
pool?.clear()
}
}
}
/** /**
* BaseAdapter is a persistent state stored adapter that supports headers and footers. * BaseAdapter is a persistent state stored adapter that supports headers and footers.
@ -49,13 +70,14 @@ abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>
abstract class BaseAdapter< abstract class BaseAdapter<
T : Any, T : Any,
S : Any>( S : Any>(
fragment: Fragment,
val id: Int = 0, val id: Int = 0,
diffCallback: DiffUtil.ItemCallback<T> = BaseDiffCallback() diffCallback: DiffUtil.ItemCallback<T> = BaseDiffCallback()
) : RecyclerView.Adapter<ViewHolderState<S>>() { ) : RecyclerView.Adapter<ViewHolderState<S>>() {
open val footers: Int = 0 open val footers: Int = 0
open val headers: Int = 0 open val headers: Int = 0
val immutableCurrentList: List<T> get() = mDiffer.currentList
fun getItem(position: Int): T { fun getItem(position: Int): T {
return mDiffer.currentList[position] return mDiffer.currentList[position]
} }
@ -85,9 +107,33 @@ abstract class BaseAdapter<
AsyncDifferConfig.Builder(diffCallback).build() AsyncDifferConfig.Builder(diffCallback).build()
) )
open fun submitList(list: List<T>?) { /**
* Instantly submits a **new and fresh** list. This means that no changes like moves are done as
* we assume the new list is not the same thing as the old list, nothing is shared.
*
* The views are rendered instantly as a result, so no fade/pop-ins or similar.
*
* Use `submitList` for general use, as that can reuse old views.
* */
open fun submitIncomparableList(list: List<T>?, commitCallback : Runnable? = null) {
// This leverages a quirk in the submitList function that has a fast case for null arrays
// What this implies is that as long as we do a double submit we can ensure no pop-ins,
// as the changes are the entire list instead of calculating deltas
submitList(null)
submitList(list, commitCallback)
}
/**
* @param commitCallback Optional runnable that is executed when the List is committed, if it is committed.
* This is needed for some tasks as submitList will use a background thread for diff
* */
open fun submitList(list: Collection<T>?, commitCallback : Runnable? = null) {
// deep copy at least the top list, because otherwise adapter can go crazy // deep copy at least the top list, because otherwise adapter can go crazy
mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) if (list.isNullOrEmpty()) {
mDiffer.submitList(null, commitCallback) // It is "faster" to submit null than emptyList()
} else {
mDiffer.submitList(CopyOnWriteArrayList(list), commitCallback)
}
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
@ -101,16 +147,25 @@ abstract class BaseAdapter<
open fun onBindFooter(holder: ViewHolderState<S>) = Unit open fun onBindFooter(holder: ViewHolderState<S>) = Unit
open fun onBindHeader(holder: ViewHolderState<S>) = Unit open fun onBindHeader(holder: ViewHolderState<S>) = Unit
open fun onCreateContent(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError() open fun onCreateContent(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateCustomContent(
parent: ViewGroup,
viewType: Int
) = onCreateContent(parent)
open fun onCreateFooter(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError() open fun onCreateFooter(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateCustomFooter(
parent: ViewGroup,
viewType: Int
) = onCreateFooter(parent)
open fun onCreateHeader(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError() open fun onCreateHeader(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateCustomHeader(
parent: ViewGroup,
viewType: Int
) = onCreateHeader(parent)
override fun onViewAttachedToWindow(holder: ViewHolderState<S>) { override fun onViewAttachedToWindow(holder: ViewHolderState<S>) {}
holder.onViewAttachedToWindow() override fun onViewDetachedFromWindow(holder: ViewHolderState<S>) {}
}
override fun onViewDetachedFromWindow(holder: ViewHolderState<S>) {
holder.onViewDetachedFromWindow()
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun save(recyclerView: RecyclerView) { fun save(recyclerView: RecyclerView) {
@ -121,21 +176,20 @@ abstract class BaseAdapter<
} }
} }
fun clear() { fun clearState() {
stateViewModel.layoutManagerStates[id]?.clear() layoutManagerStates[id]?.clear()
} }
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
private fun getState(holder: ViewHolderState<S>): S? = private fun getState(holder: ViewHolderState<S>): S? =
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
private fun setState(holder: ViewHolderState<S>) { private fun setState(holder: ViewHolderState<S>) {
if (id == 0) return if (id == 0) return
if (!layoutManagerStates.contains(id)) {
if (!stateViewModel.layoutManagerStates.contains(id)) { layoutManagerStates[id] = HashMap()
stateViewModel.layoutManagerStates[id] = HashMap()
} }
stateViewModel.layoutManagerStates[id]?.let { map -> layoutManagerStates[id]?.let { map ->
map[holder.absoluteAdapterPosition] = holder.save() map[holder.absoluteAdapterPosition] = holder.save()
} }
} }
@ -158,30 +212,40 @@ abstract class BaseAdapter<
super.onDetachedFromRecyclerView(recyclerView) super.onDetachedFromRecyclerView(recyclerView)
} }
open fun customContentViewType(item: T): Int = 0
open fun customFooterViewType(): Int = 0
open fun customHeaderViewType(): Int = 0
final override fun getItemViewType(position: Int): Int { final override fun getItemViewType(position: Int): Int {
if (position < headers) { if (position < headers) {
return HEADER return HEADER or customHeaderViewType()
} }
if (position - headers >= mDiffer.currentList.size) { val realPosition = position - headers
return FOOTER if (realPosition >= mDiffer.currentList.size) {
return FOOTER or customFooterViewType()
} }
return CONTENT or customContentViewType(getItem(realPosition))
return CONTENT
} }
private val stateViewModel: StateViewModel by fragment.viewModels()
final override fun onViewRecycled(holder: ViewHolderState<S>) { final override fun onViewRecycled(holder: ViewHolderState<S>) {
setState(holder) setState(holder)
holder.onViewRecycled() onClearView(holder)
super.onViewRecycled(holder) super.onViewRecycled(holder)
} }
/** Same as onViewRecycled, but for the purpose of cleaning the view of any relevant data.
*
* If an item view has large or expensive data bound to it such as large bitmaps, this may be a good place to release those resources.
*
* Use this with `clearImage`
* */
open fun onClearView(holder: ViewHolderState<S>) {}
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState<S> { final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState<S> {
return when (viewType) { return when (viewType and TYPE_MASK) {
CONTENT -> onCreateContent(parent) CONTENT -> onCreateCustomContent(parent, viewType and CUSTOM_MASK)
HEADER -> onCreateHeader(parent) HEADER -> onCreateCustomHeader(parent, viewType and CUSTOM_MASK)
FOOTER -> onCreateFooter(parent) FOOTER -> onCreateCustomFooter(parent, viewType and CUSTOM_MASK)
else -> throw NotImplementedError() else -> throw NotImplementedError()
} }
} }
@ -196,7 +260,7 @@ abstract class BaseAdapter<
super.onBindViewHolder(holder, position, payloads) super.onBindViewHolder(holder, position, payloads)
return return
} }
when (getItemViewType(position)) { when (getItemViewType(position) and TYPE_MASK) {
CONTENT -> { CONTENT -> {
val realPosition = position - headers val realPosition = position - headers
val item = getItem(realPosition) val item = getItem(realPosition)
@ -214,7 +278,7 @@ abstract class BaseAdapter<
} }
final override fun onBindViewHolder(holder: ViewHolderState<S>, position: Int) { final override fun onBindViewHolder(holder: ViewHolderState<S>, position: Int) {
when (getItemViewType(position)) { when (getItemViewType(position) and TYPE_MASK) {
CONTENT -> { CONTENT -> {
val realPosition = position - headers val realPosition = position - headers
val item = getItem(realPosition) val item = getItem(realPosition)
@ -236,9 +300,20 @@ abstract class BaseAdapter<
} }
companion object { companion object {
private const val HEADER: Int = 1 val layoutManagerStates = hashMapOf<Int, HashMap<Int, Any?>>()
private const val FOOTER: Int = 2 fun clearImage(image: ImageView?) {
private const val CONTENT: Int = 0 image?.dispose()
}
// Use the lowermost MASK_SIZE bits for the custom content,
// use the uppermost 32 - MASK_SIZE to the type
private const val MASK_SIZE = 28
private const val CUSTOM_MASK = (1 shl MASK_SIZE) - 1
private const val TYPE_MASK = CUSTOM_MASK.inv()
const val HEADER: Int = 3 shl MASK_SIZE
const val FOOTER: Int = 2 shl MASK_SIZE
/** For custom content, write `CONTENT or X` when calling setMaxRecycledViews */
const val CONTENT: Int = 1 shl MASK_SIZE
} }
} }
@ -248,5 +323,5 @@ class BaseDiffCallback<T : Any>(
) : DiffUtil.ItemCallback<T>() { ) : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem) override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem)
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem) override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem)
override fun getChangePayload(oldItem: T, newItem: T): Any = Any() override fun getChangePayload(oldItem: T, newItem: T): Any? = Any()
} }

View file

@ -0,0 +1,278 @@
package com.lagradost.cloudstream3.ui
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceFragmentCompat
import androidx.viewbinding.ViewBinding
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setSystemBarsPadding
import com.lagradost.cloudstream3.utils.txt
/**
* A base Fragment class that simplifies ViewBinding usage and handles view inflation safely.
*
* This class allows two modes of creating ViewBinding:
* 1. Inflate: Using the standard `inflate()` method provided by generated ViewBinding classes.
* 2. Bind: Using `bind()` on an existing root view.
*
* It also provides hooks for:
* - Safe initialization of the binding (`onBindingCreated`)
* - Automatic padding adjustment for system bars (`fixPadding`)
* - Optional layout resource selection via `pickLayout()`
*
* @param T The type of ViewBinding for this Fragment.
* @param bindingCreator The strategy used to create the binding instance.
*/
private interface BaseFragmentHelper<T : ViewBinding> {
val bindingCreator: BaseFragment.BindingCreator<T>
var _binding: T?
val binding: T? get() = _binding
fun createBinding(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val layoutId = pickLayout()
val root: View? = layoutId?.let { inflater.inflate(it, container, false) }
_binding = try {
when (val creator = bindingCreator) {
is BaseFragment.BindingCreator.Inflate -> creator.fn(inflater, container, false)
is BaseFragment.BindingCreator.Bind -> {
if (root != null) creator.fn(root)
else throw IllegalStateException("Root view is null for bind()")
}
}
} catch (t: Throwable) {
showToast(
txt(R.string.unable_to_inflate, t.message ?: ""),
Toast.LENGTH_LONG
)
logError(t)
null
}
return _binding?.root ?: root
}
/**
* Called after the fragment's view has been created.
*
* This method is `final` to ensure that the binding is properly initialized and
* system bar padding adjustments are applied before any subclass logic runs.
* Subclasses should use [onBindingCreated] instead of overriding this method directly.
*/
fun onViewReady(view: View, savedInstanceState: Bundle?) {
fixLayout(view)
binding?.let { onBindingCreated(it, savedInstanceState) }
}
/**
* Called when the binding is safely created and view is ready.
* Can be overridden to provide fragment-specific initialization.
*
* @param binding The safely created ViewBinding.
* @param savedInstanceState Saved state bundle or null.
*/
fun onBindingCreated(binding: T, savedInstanceState: Bundle?) {
onBindingCreated(binding)
}
/**
* Called when the binding is safely created and view is ready.
* Overload without savedInstanceState for convenience.
*
* @param binding The safely created ViewBinding.
*/
fun onBindingCreated(binding: T) {}
/**
* Pick a layout resource ID for the fragment.
*
* Return `null` by default. Override to provide a layout resource when using
* `BindingCreator.Bind`. Not needed if using `BindingCreator.Inflate`.
*
* @return Layout resource ID or null.
*/
@LayoutRes
fun pickLayout(): Int? = null
/**
* Ensures the layout of the root view is correctly adjusted for the current configuration.
*
* This may include applying padding for system bars, adjusting insets, or performing other
* layout updates. `fixLayout` should remain idempotent, as it can be called multiple
* times on the same view, such as during configuration changes (e.g. device rotation) or when
* the view is recreated.
*
* @param view The root view to adjust.
*/
fun fixLayout(view: View)
}
abstract class BaseFragment<T : ViewBinding>(
override val bindingCreator: BindingCreator<T>
) : Fragment(), BaseFragmentHelper<T> {
override var _binding: T? = null
/** Safer activity?.onBackPressedDispatcher?.onBackPressed() with fallback behavior instead of app crash */
fun dispatchBackPressed() {
try {
activity?.onBackPressedDispatcher?.onBackPressed()
} catch (_: IllegalStateException) {
// FragmentManager is already executing transactions, so try again
delayedDispatchBackPressed(5)
} catch (t: Throwable) {
logError(t)
}
}
/** Recursive back press when available */
private fun delayedDispatchBackPressed(remaining: Int) {
if (remaining <= 0) return
binding?.root?.postDelayed({
try {
activity?.onBackPressedDispatcher?.onBackPressed()
} catch (_: IllegalStateException) {
// FragmentManager is already executing transactions, so try again
delayedDispatchBackPressed(remaining - 1)
} catch (t: Throwable) {
logError(t)
}
}, 200)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = createBinding(inflater, container, savedInstanceState)
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onViewReady(view, savedInstanceState)
}
/**
* Called when the device configuration changes (e.g., orientation).
* Re-applies system bar padding fixes to the root view to ensure it
* readjusts for orientation changes.
*/
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
view?.let { fixLayout(it) }
}
/** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* Sealed class representing the two strategies for creating a ViewBinding instance.
*/
sealed class BindingCreator<T : ViewBinding> {
/**
* Use the standard inflate() method for creating the binding.
*
* @param fn Lambda that inflates the binding.
*/
class Inflate<T : ViewBinding>(
val fn: (LayoutInflater, ViewGroup?, Boolean) -> T
) : BindingCreator<T>()
/**
* Use bind() on an existing root view to create the binding. This should
* be used if you are differing per device layouts, such as different
* layouts for TV and Phone.
*
* @param fn Lambda that binds the root view.
*/
class Bind<T : ViewBinding>(
val fn: (View) -> T
) : BindingCreator<T>()
}
}
abstract class BaseDialogFragment<T : ViewBinding>(
override val bindingCreator: BaseFragment.BindingCreator<T>
) : DialogFragment(), BaseFragmentHelper<T> {
override var _binding: T? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = createBinding(inflater, container, savedInstanceState)
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onViewReady(view, savedInstanceState)
}
/** @see [BaseFragment.onConfigurationChanged] for documentation. */
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
view?.let { fixLayout(it) }
}
/** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
abstract class BaseBottomSheetDialogFragment<T : ViewBinding>(
override val bindingCreator: BaseFragment.BindingCreator<T>
) : BottomSheetDialogFragment(), BaseFragmentHelper<T> {
override var _binding: T? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = createBinding(inflater, container, savedInstanceState)
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onViewReady(view, savedInstanceState)
}
/** @see [BaseFragment.onConfigurationChanged] for documentation. */
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
view?.let { fixLayout(it) }
}
/** Cleans up the binding reference when the view is destroyed to avoid memory leaks. */
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
abstract class BasePreferenceFragmentCompat() : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setSystemBarsPadding()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setSystemBarsPadding()
}
}

View file

@ -12,9 +12,7 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListView import android.widget.ListView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.annotation.JsonProperty
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.MediaLoadOptions
import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaSeekOptions import com.google.android.gms.cast.MediaSeekOptions
@ -37,35 +35,24 @@ import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultEpisode import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks import com.lagradost.cloudstream3.utils.CastHelper.awaitLinks
import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.json.JSONObject import org.json.JSONObject
/*class SkipOpController(val view: ImageView) : UIController() {
init {
view.setImageResource(R.drawable.exo_controls_fastforward)
view.setOnClickListener {
remoteMediaClient?.let {
val options = MediaSeekOptions.Builder()
.setPosition(it.approximateStreamPosition + 85000)
it.seek(options.build())
}
}
}
}*/
private fun RemoteMediaClient.getItemIndex(): Int? { private fun RemoteMediaClient.getItemIndex(): Int? {
return try { return try {
val index = this.mediaQueue.itemIds.indexOf(this.currentItem?.itemId ?: 0) val index = this.mediaQueue.itemIds.indexOf(this.currentItem?.itemId ?: 0)
if (index < 0) null else index if (index < 0) null else index
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} }
@ -92,51 +79,41 @@ class SkipNextEpisodeController(val view: ImageView) : UIController() {
} }
} }
@Serializable
data class MetadataHolder( data class MetadataHolder(
val apiName: String, @JsonProperty("apiName") @SerialName("apiName") val apiName: String,
val isMovie: Boolean, @JsonProperty("isMovie") @SerialName("isMovie") val isMovie: Boolean,
val title: String?, @JsonProperty("title") @SerialName("title") val title: String?,
val poster: String?, @JsonProperty("poster") @SerialName("poster") val poster: String?,
val currentEpisodeIndex: Int, @JsonProperty("currentEpisodeIndex") @SerialName("currentEpisodeIndex") val currentEpisodeIndex: Int,
val episodes: List<ResultEpisode>, @JsonProperty("episodes") @SerialName("episodes") val episodes: List<ResultEpisode>,
val currentLinks: List<ExtractorLink>, @JsonProperty("currentLinks") @SerialName("currentLinks") val currentLinks: List<ExtractorLink>,
val currentSubtitles: List<SubtitleData> @JsonProperty("currentSubtitles") @SerialName("currentSubtitles") val currentSubtitles: List<SubtitleData>,
) )
class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() {
UIController() {
private val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
init { init {
view.setImageResource(R.drawable.ic_baseline_playlist_play_24) view.setImageResource(R.drawable.ic_baseline_playlist_play_24)
view.setOnClickListener { view.setOnClickListener {
// lateinit var dialog: AlertDialog
val holder = getCurrentMetaData() val holder = getCurrentMetaData()
if (holder != null) { if (holder != null) {
val items = holder.currentLinks val items = holder.currentLinks
if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) { if (items.isNotEmpty() && remoteMediaClient?.currentItem != null) {
val subTracks = val subTracks = remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT }
remoteMediaClient?.mediaInfo?.mediaTracks?.filter { it.type == MediaTrack.TYPE_TEXT }
?: ArrayList() ?: ArrayList()
val bottomSheetDialogBuilder = val bottomSheetDialogBuilder = AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
AlertDialog.Builder(view.context, R.style.AlertDialogCustomBlack)
bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet) bottomSheetDialogBuilder.setView(R.layout.sort_bottom_sheet)
val bottomSheetDialog = bottomSheetDialogBuilder.create() val bottomSheetDialog = bottomSheetDialogBuilder.create()
bottomSheetDialog.show() bottomSheetDialog.show()
// bottomSheetDialog.setContentView(R.layout.sort_bottom_sheet)
val providerList = val providerList = bottomSheetDialog.findViewById<ListView>(R.id.sort_providers)!!
bottomSheetDialog.findViewById<ListView>(R.id.sort_providers)!! val subtitleList = bottomSheetDialog.findViewById<ListView>(R.id.sort_subtitles)!!
val subtitleList =
bottomSheetDialog.findViewById<ListView>(R.id.sort_subtitles)!!
if (subTracks.isEmpty()) { if (subTracks.isEmpty()) {
bottomSheetDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = bottomSheetDialog.findViewById<LinearLayout>(R.id.sort_subtitles_holder)?.visibility = GONE
GONE
} else { } else {
val arrayAdapter = val arrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
arrayAdapter.add(view.context.getString(R.string.no_subtitles)) arrayAdapter.add(view.context.getString(R.string.no_subtitles))
arrayAdapter.addAll(subTracks.mapNotNull { it.name }) arrayAdapter.addAll(subTracks.mapNotNull { it.name })
@ -144,9 +121,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
subtitleList.adapter = arrayAdapter subtitleList.adapter = arrayAdapter
val currentTracks = remoteMediaClient?.mediaStatus?.activeTrackIds val currentTracks = remoteMediaClient?.mediaStatus?.activeTrackIds
val subtitleIndex = if (currentTracks == null) 0 else subTracks.map { it.id }
val subtitleIndex =
if (currentTracks == null) 0 else subTracks.map { it.id }
.indexOfFirst { currentTracks.contains(it) } + 1 .indexOfFirst { currentTracks.contains(it) } + 1
subtitleList.setSelection(subtitleIndex) subtitleList.setSelection(subtitleIndex)
@ -159,9 +134,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply { ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
val font = TextTrackStyle() val font = TextTrackStyle()
font.setFontFamily(fontFamily ?: "Google Sans") font.setFontFamily(fontFamily ?: "Google Sans")
fontGenericFamily?.let { fontGenericFamily?.let { font.fontGenericFamily = it }
font.fontGenericFamily = it
}
font.windowColor = windowColor font.windowColor = windowColor
font.backgroundColor = backgroundColor font.backgroundColor = backgroundColor
@ -191,13 +164,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
?: remoteMediaClient?.currentItem?.media?.contentId) ?: remoteMediaClient?.currentItem?.media?.contentId)
val sortingMethods = val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
.toTypedArray() .toTypedArray()
val sotringIndex = items.indexOfFirst { it.url == contentUrl } val sotringIndex = items.indexOfFirst { it.url == contentUrl }
val arrayAdapter = val arrayAdapter = ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
ArrayAdapter<String>(view.context, R.layout.sort_bottom_single_choice)
arrayAdapter.addAll(sortingMethods.toMutableList()) arrayAdapter.addAll(sortingMethods.toMutableList())
providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE providerList.choiceMode = AbsListView.CHOICE_MODE_SINGLE
@ -207,10 +178,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
providerList.setOnItemClickListener { _, _, which, _ -> providerList.setOnItemClickListener { _, _, which, _ ->
val epData = holder.episodes[holder.currentEpisodeIndex] val epData = holder.episodes[holder.currentEpisodeIndex]
fun loadMirror(index: Int) { fun loadMirror(index: Int) {
if (holder.currentLinks.size <= index) return if (holder.currentLinks.size <= index) return
val mediaItem = getMediaInfo( val mediaItem = getMediaInfo(
epData, epData,
holder, holder,
@ -220,36 +189,35 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
) )
val startAt = remoteMediaClient?.approximateStreamPosition ?: 0 val startAt = remoteMediaClient?.approximateStreamPosition ?: 0
//remoteMediaClient.load(mediaItem, true, startAt)
try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE try { // THIS IS VERY IMPORTANT BECAUSE WE NEVER WANT TO AUTOLOAD THE NEXT EPISODE
val currentIdIndex = remoteMediaClient?.getItemIndex() val currentIdIndex = remoteMediaClient?.getItemIndex()
val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get( val nextId = remoteMediaClient?.mediaQueue?.itemIds?.get(
currentIdIndex?.plus(1) ?: 0 currentIdIndex?.plus(1) ?: 0
) )
if (currentIdIndex == null && nextId != null) { if (currentIdIndex == null && nextId != null) {
awaitLinks( awaitLinks(
remoteMediaClient?.queueInsertAndPlayItem( remoteMediaClient?.queueInsertAndPlayItem(
MediaQueueItem.Builder(mediaItem).build(), MediaQueueItem.Builder(mediaItem).build(),
nextId, nextId,
startAt, startAt,
JSONObject() JSONObject(),
) )
) { ) { loadMirror(index + 1) }
loadMirror(index + 1)
}
} else { } else {
val mediaLoadOptions = val mediaLoadOptions =
MediaLoadOptions.Builder() MediaLoadOptions.Builder()
.setPlayPosition(startAt) .setPlayPosition(startAt)
.setAutoplay(true) .setAutoplay(true)
.build() .build()
awaitLinks(remoteMediaClient?.load(mediaItem, mediaLoadOptions)) { awaitLinks(
loadMirror(index + 1) remoteMediaClient?.load(
mediaItem,
mediaLoadOptions
)
) { loadMirror(index + 1) }
} }
} } catch (_: Exception) {
} catch (e: Exception) {
val mediaLoadOptions = val mediaLoadOptions =
MediaLoadOptions.Builder() MediaLoadOptions.Builder()
.setPlayPosition(startAt) .setPlayPosition(startAt)
@ -260,8 +228,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
} }
} }
} }
loadMirror(which)
loadMirror(which)
bottomSheetDialog.dismissSafe(activity) bottomSheetDialog.dismissSafe(activity)
} }
} }
@ -271,23 +239,19 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
private fun getCurrentMetaData(): MetadataHolder? { private fun getCurrentMetaData(): MetadataHolder? {
return try { return try {
val data = remoteMediaClient?.mediaInfo?.customData?.toString() val data = remoteMediaClient?.mediaInfo?.customData?.toString() ?: return null
data?.toKotlinObject() parseJson<MetadataHolder>(data)
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} }
var isLoadingMore = false var isLoadingMore = false
override fun onMediaStatusUpdated() { override fun onMediaStatusUpdated() {
super.onMediaStatusUpdated() super.onMediaStatusUpdated()
val meta = getCurrentMetaData() val meta = getCurrentMetaData()
view.visibility = if ((meta?.currentLinks?.size ?: 0) > 1) VISIBLE else INVISIBLE
view.visibility = if ((meta?.currentLinks?.size
?: 0) > 1
) VISIBLE else INVISIBLE
try { try {
if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) { if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) {
val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return val currentIdIndex = remoteMediaClient?.getItemIndex() ?: return
@ -299,7 +263,13 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val currentDuration = remoteMediaClient?.streamDuration val currentDuration = remoteMediaClient?.streamDuration
val currentPosition = remoteMediaClient?.approximateStreamPosition val currentPosition = remoteMediaClient?.approximateStreamPosition
if (currentDuration != null && currentPosition != null) if (currentDuration != null && currentPosition != null)
DataStoreHelper.setViewPos(epData.id, currentPosition, currentDuration) DataStoreHelper.setViewPosAndResume(
epData.id,
currentPosition,
currentDuration,
epData,
meta.episodes.getOrNull(index + 1),
)
} catch (t: Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
} }
@ -309,13 +279,11 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
ioSafe { ioSafe {
val currentLinks = mutableSetOf<ExtractorLink>() val currentLinks = mutableSetOf<ExtractorLink>()
val currentSubs = mutableSetOf<SubtitleData>() val currentSubs = mutableSetOf<SubtitleData>()
val generator = RepoLinkGenerator(listOf(epData)) val generator = RepoLinkGenerator(listOf(epData))
val isSuccessful = safeApiCall { val isSuccessful = safeApiCall {
generator.generateLinks( generator.generateLinks(
clearCache = false, clearCache = false,
allowedTypes = LOADTYPE_CHROMECAST, sourceTypes = LOADTYPE_CHROMECAST,
callback = { callback = {
it.first?.let { link -> it.first?.let { link ->
currentLinks.add(link) currentLinks.add(link)
@ -323,7 +291,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
}, subtitleCallback = { }, subtitleCallback = {
currentSubs.add(it) currentSubs.add(it)
}, },
isCasting = true) offset = 0,
isCasting = true,
)
} }
val sortedLinks = sortUrls(currentLinks) val sortedLinks = sortUrls(currentLinks)
@ -333,32 +303,18 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val jsonCopy = meta.copy( val jsonCopy = meta.copy(
currentLinks = sortedLinks, currentLinks = sortedLinks,
currentSubtitles = sortedSubs, currentSubtitles = sortedSubs,
currentEpisodeIndex = index currentEpisodeIndex = index,
) )
val done = val done = JSONObject(jsonCopy.toJson())
JSONObject(jsonCopy.toJson())
val mediaInfo = getMediaInfo( val mediaInfo = getMediaInfo(
epData, epData,
jsonCopy, jsonCopy,
0, 0,
done, done,
sortedSubs sortedSubs,
) )
/*fun loadIndex(index: Int) {
println("LOAD INDEX::::: $index")
if (meta.currentLinks.size <= index) return
val info = getMediaInfo(
epData,
meta,
index,
done)
awaitLinks(remoteMediaClient?.load(info, true, 0)) {
loadIndex(index + 1)
}
}*/
activity.runOnUiThread { activity.runOnUiThread {
awaitLinks( awaitLinks(
remoteMediaClient?.queueAppendItem( remoteMediaClient?.queueAppendItem(
@ -367,7 +323,6 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
) )
) { ) {
println("FAILED TO LOAD NEXT ITEM") println("FAILED TO LOAD NEXT ITEM")
// loadIndex(1)
} }
isLoadingMore = false isLoadingMore = false
} }
@ -390,10 +345,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
class SkipTimeController(val view: ImageView, forwards: Boolean) : UIController() { class SkipTimeController(val view: ImageView, forwards: Boolean) : UIController() {
init { init {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences()
//val time = settingsManager?.getInt("chromecast_tap_time", 30) ?: 30
val time = 30 val time = 30
//view.setImageResource(if (forwards) R.drawable.netflix_skip_forward else R.drawable.netflix_skip_back)
view.setImageResource(if (forwards) R.drawable.go_forward_30 else R.drawable.go_back_30) view.setImageResource(if (forwards) R.drawable.go_forward_30 else R.drawable.go_back_30)
view.setOnClickListener { view.setOnClickListener {
remoteMediaClient?.let { remoteMediaClient?.let {

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.core.content.withStyledAttributes
import androidx.core.view.children import androidx.core.view.children
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -154,10 +155,9 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att
init { init {
if (attrs != null) { if (attrs != null) {
val attrsArray = intArrayOf(android.R.attr.columnWidth) context.withStyledAttributes(attrs, intArrayOf(android.R.attr.columnWidth)) {
val array = context.obtainStyledAttributes(attrs, attrsArray) columnWidth = getDimensionPixelSize(0, -1)
columnWidth = array.getDimensionPixelSize(0, -1) }
array.recycle()
} }
layoutManager = manager layoutManager = manager

View file

@ -1,97 +0,0 @@
// https://github.com/googlecodelabs/android-kotlin-animation-property-animation/tree/master/begin
package com.lagradost.cloudstream3.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.os.Bundle
import android.os.Handler
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.LinearInterpolator
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ActivityEasterEggMonkeBinding
class EasterEggMonke : AppCompatActivity() {
lateinit var binding : ActivityEasterEggMonkeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityEasterEggMonkeBinding.inflate(layoutInflater)
setContentView(binding.root)
val handler = Handler(mainLooper)
lateinit var runnable: Runnable
runnable = Runnable {
shower()
handler.postDelayed(runnable, 300)
}
handler.postDelayed(runnable, 1000)
}
private fun shower() {
val containerW = binding.frame.width
val containerH = binding.frame.height
var starW: Float = binding.monke.width.toFloat()
var starH: Float = binding.monke.height.toFloat()
val newStar = AppCompatImageView(this)
val idx = (monkeys.size * Math.random()).toInt()
newStar.setImageResource(monkeys[idx])
newStar.isVisible = true
newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT)
binding.frame.addView(newStar)
newStar.scaleX += Math.random().toFloat() * 1.5f
newStar.scaleY = newStar.scaleX
starW *= newStar.scaleX
starH *= newStar.scaleY
newStar.translationX = Math.random().toFloat() * containerW - starW / 2
val mover = ObjectAnimator.ofFloat(newStar, View.TRANSLATION_Y, -starH, containerH + starH)
mover.interpolator = AccelerateInterpolator(1f)
val rotator = ObjectAnimator.ofFloat(newStar, View.ROTATION,
(Math.random() * 1080).toFloat())
rotator.interpolator = LinearInterpolator()
val set = AnimatorSet()
set.playTogether(mover, rotator)
set.duration = (Math.random() * 1500 + 2500).toLong()
set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.frame.removeView(newStar)
}
})
set.start()
}
companion object {
val monkeys = listOf(
R.drawable.monke_benene,
R.drawable.monke_burrito,
R.drawable.monke_coco,
R.drawable.monke_cookie,
R.drawable.monke_flusdered,
R.drawable.monke_funny,
R.drawable.monke_like,
R.drawable.monke_party,
R.drawable.monke_sob,
R.drawable.monke_drink,
R.drawable.benene,
R.drawable.ic_launcher_foreground
)
}
}

View file

@ -0,0 +1,177 @@
package com.lagradost.cloudstream3.ui
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.View
import android.view.animation.AccelerateInterpolator
import android.view.animation.LinearInterpolator
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentEasterEggMonkeBinding
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.random.Random
class EasterEggMonkeFragment : BaseFragment<FragmentEasterEggMonkeBinding>(
BaseFragment.BindingCreator.Inflate(FragmentEasterEggMonkeBinding::inflate)
) {
// planet of monks
private val monkeys: List<Int> = listOf(
R.drawable.monke_benene,
R.drawable.monke_burrito,
R.drawable.monke_coco,
R.drawable.monke_cookie,
R.drawable.monke_flusdered,
R.drawable.monke_funny,
R.drawable.monke_like,
R.drawable.monke_party,
R.drawable.monke_sob,
R.drawable.monke_drink,
R.drawable.benene,
R.drawable.ic_launcher_foreground,
R.drawable.quick_novel_icon,
)
private val activeMonkeys = mutableListOf<ImageView>()
private var spawningJob: Job? = null
override fun fixLayout(view: View) = Unit
override fun onBindingCreated(binding: FragmentEasterEggMonkeBinding) {
activity?.hideSystemUI()
spawningJob = lifecycleScope.launch {
delay(1000)
while (isActive) {
spawnMonkey(binding)
delay(500)
}
}
}
private fun spawnMonkey(binding: FragmentEasterEggMonkeBinding) {
val newMonkey = ImageView(context ?: return).apply {
setImageResource(monkeys.random())
isVisible = true
}
val initialScale = Random.nextFloat() * 1.5f + 0.5f
newMonkey.scaleX = initialScale
newMonkey.scaleY = initialScale
newMonkey.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
val monkeyW = newMonkey.measuredWidth * initialScale
val monkeyH = newMonkey.measuredHeight * initialScale
newMonkey.x = Random.nextFloat() * (binding.frame.width.toFloat() - monkeyW)
newMonkey.y = Random.nextFloat() * (binding.frame.height.toFloat() - monkeyH)
binding.frame.addView(newMonkey, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT
))
activeMonkeys.add(newMonkey)
newMonkey.alpha = 0f
ObjectAnimator.ofFloat(newMonkey, View.ALPHA, 0f, 1f).apply {
duration = Random.nextLong(1000, 2500)
interpolator = AccelerateInterpolator()
start()
}
@SuppressLint("ClickableViewAccessibility")
newMonkey.setOnTouchListener { view, event -> handleTouch(view, event, binding) }
startFloatingAnimation(newMonkey, binding)
}
private fun startFloatingAnimation(monkey: ImageView, binding: FragmentEasterEggMonkeBinding) {
val floatUpAnimator = ObjectAnimator.ofFloat(
monkey, View.TRANSLATION_Y, monkey.y, -monkey.height.toFloat()
).apply {
duration = Random.nextLong(8000, 15000)
interpolator = LinearInterpolator()
}
floatUpAnimator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.frame.removeView(monkey)
activeMonkeys.remove(monkey)
}
})
floatUpAnimator.start()
monkey.tag = floatUpAnimator
}
private fun handleTouch(
view: View,
event: MotionEvent,
binding: FragmentEasterEggMonkeBinding
): Boolean {
val monkey = view as ImageView
when (event.action) {
MotionEvent.ACTION_DOWN -> {
(monkey.tag as? ObjectAnimator)?.pause()
return true
}
MotionEvent.ACTION_MOVE -> {
// Update both X and Y positions properly
monkey.x = event.rawX - monkey.width / 2
monkey.y = event.rawY - monkey.height / 2
// Check if monkey touches the screen edge
if (isTouchingEdge(monkey, binding)) {
removeMonkey(monkey, binding)
}
return true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
if (isTouchingEdge(monkey, binding)) {
removeMonkey(monkey, binding)
} else {
startFloatingAnimation(monkey, binding)
}
return true
}
}
return false
}
private fun isTouchingEdge(monkey: ImageView, binding: FragmentEasterEggMonkeBinding): Boolean {
return monkey.x <= 0 || monkey.x + monkey.width >= binding.frame.width ||
monkey.y <= 0 || monkey.y + monkey.height >= binding.frame.height
}
private fun removeMonkey(monkey: ImageView, binding: FragmentEasterEggMonkeBinding) {
// Fade out and remove the monkey
ObjectAnimator.ofFloat(monkey, View.ALPHA, 1f, 0f).apply {
duration = 300
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.frame.removeView(monkey)
activeMonkeys.remove(monkey)
}
})
start()
}
}
override fun onDestroyView() {
super.onDestroyView()
activity?.showSystemUI()
spawningJob?.cancel()
}
}

View file

@ -1,42 +0,0 @@
package com.lagradost.cloudstream3.ui
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class HeaderViewDecoration(private val customView: View) : RecyclerView.ItemDecoration() {
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
customView.layout(parent.left, 0, parent.right, customView.measuredHeight)
for (i in 0 until parent.childCount) {
val view = parent.getChildAt(i)
if (parent.getChildAdapterPosition(view) == 0) {
c.save()
val height = customView.measuredHeight
val top = view.top - height
c.translate(0f, top.toFloat())
customView.draw(c)
c.restore()
break
}
}
}
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
if (parent.getChildAdapterPosition(view) == 0) {
customView.measure(
View.MeasureSpec.makeMeasureSpec(parent.measuredWidth, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(parent.measuredHeight, View.MeasureSpec.AT_MOST)
)
outRect.set(0, customView.measuredHeight, 0, 0)
} else {
outRect.setEmpty()
}
}
}

View file

@ -7,12 +7,12 @@ import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.RelativeLayout import android.widget.RelativeLayout
import androidx.core.content.withStyledAttributes
import com.google.android.gms.cast.framework.media.widget.MiniControllerFragment import com.google.android.gms.cast.framework.media.widget.MiniControllerFragment
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.adjustAlpha import com.lagradost.cloudstream3.utils.UIHelper.adjustAlpha
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import java.lang.ref.WeakReference
class MyMiniControllerFragment : MiniControllerFragment() { class MyMiniControllerFragment : MiniControllerFragment() {
@ -25,28 +25,17 @@ class MyMiniControllerFragment : MiniControllerFragment() {
// I KNOW, KINDA SPAGHETTI SOLUTION, BUT IT WORKS // I KNOW, KINDA SPAGHETTI SOLUTION, BUT IT WORKS
override fun onInflate(context: Context, attributeSet: AttributeSet, bundle: Bundle?) { override fun onInflate(context: Context, attributeSet: AttributeSet, bundle: Bundle?) {
super.onInflate(context, attributeSet, bundle)
// somehow this leaks and I really dont know why, it seams like if you go back to a fragment with this, it leaks????
if (currentColor == 0) { if (currentColor == 0) {
WeakReference( context.withStyledAttributes(attributeSet, R.styleable.CustomCast, 0, 0) {
context.obtainStyledAttributes( if (hasValue(R.styleable.CustomCast_customCastBackgroundColor)) {
attributeSet, currentColor = getColor(R.styleable.CustomCast_customCastBackgroundColor, 0)
R.styleable.CustomCast
)
).apply {
if (get()
?.hasValue(R.styleable.CustomCast_customCastBackgroundColor) == true
) {
currentColor =
get()
?.getColor(R.styleable.CustomCast_customCastBackgroundColor, 0) ?: 0
} }
get()?.recycle()
}.clear()
} }
} }
super.onInflate(context, attributeSet, bundle)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -18,15 +18,6 @@ enum class WatchType(val internalId: Int, @StringRes val stringRes: Int, @Drawab
} }
enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) { enum class SyncWatchType(val internalId: Int, @StringRes val stringRes: Int, @DrawableRes val iconRes: Int) {
/*
-1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
*/
NONE(-1, R.string.type_none, R.drawable.ic_baseline_add_24), NONE(-1, R.string.type_none, R.drawable.ic_baseline_add_24),
WATCHING(0, R.string.type_watching, R.drawable.ic_baseline_bookmark_24), WATCHING(0, R.string.type_watching, R.drawable.ic_baseline_bookmark_24),
COMPLETED(1, R.string.type_completed, R.drawable.ic_baseline_bookmark_24), COMPLETED(1, R.string.type_completed, R.drawable.ic_baseline_bookmark_24),

View file

@ -1,17 +1,12 @@
package com.lagradost.cloudstream3.ui package com.lagradost.cloudstream3.ui
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.media3.common.util.UnstableApi
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
@ -19,19 +14,18 @@ import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository import com.lagradost.cloudstream3.utils.AppContextUtils.loadRepository
class WebviewFragment : BaseFragment<FragmentWebviewBinding>(
BaseFragment.BindingCreator.Inflate(FragmentWebviewBinding::inflate)
) {
class WebviewFragment : Fragment() { override fun fixLayout(view: View) = Unit
var binding: FragmentWebviewBinding? = null override fun onBindingCreated(binding: FragmentWebviewBinding) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val url = arguments?.getString(WEBVIEW_URL) ?: "".also { val url = arguments?.getString(WEBVIEW_URL) ?: "".also {
findNavController().popBackStack() findNavController().popBackStack()
} }
binding?.webView?.webViewClient = object : WebViewClient() { binding.webView.webViewClient = object : WebViewClient() {
@OptIn(UnstableApi::class)
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
request: WebResourceRequest? request: WebResourceRequest?
@ -46,28 +40,17 @@ class WebviewFragment : Fragment() {
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
} }
} }
binding?.webView?.apply {
binding.webView.apply {
WebViewResolver.webViewUserAgent = settings.userAgentString WebViewResolver.webViewUserAgent = settings.userAgentString
addJavascriptInterface(RepoApi(activity), "RepoApi") addJavascriptInterface(RepoApi(activity), "RepoApi")
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
settings.userAgentString = USER_AGENT settings.userAgentString = USER_AGENT
settings.domStorageEnabled = true settings.domStorageEnabled = true
// WebView.setWebContentsDebuggingEnabled(true)
loadUrl(url) loadUrl(url)
} }
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val localBinding = FragmentWebviewBinding.inflate(inflater, container, false)
binding = localBinding
// Inflate the layout for this fragment
return localBinding.root//inflater.inflate(R.layout.fragment_webview, container, false)
} }
companion object { companion object {

View file

@ -1,16 +1,17 @@
package com.lagradost.cloudstream3.ui.account package com.lagradost.cloudstream3.ui.account
import android.os.Build
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import coil3.transform.RoundedCornersTransformation import coil3.transform.RoundedCornersTransformation
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountListItemAddBinding import com.lagradost.cloudstream3.databinding.AccountListItemAddBinding
import com.lagradost.cloudstream3.databinding.AccountListItemBinding import com.lagradost.cloudstream3.databinding.AccountListItemBinding
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
import com.lagradost.cloudstream3.ui.NoStateAdapter
import com.lagradost.cloudstream3.ui.ViewHolderState
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.TV
@ -19,34 +20,39 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
class AccountAdapter( class AccountAdapter(
private val accounts: List<DataStoreHelper.Account>,
private val accountSelectCallback: (DataStoreHelper.Account) -> Unit, private val accountSelectCallback: (DataStoreHelper.Account) -> Unit,
private val accountCreateCallback: (DataStoreHelper.Account) -> Unit, private val accountCreateCallback: (DataStoreHelper.Account) -> Unit,
private val accountEditCallback: (DataStoreHelper.Account) -> Unit, private val accountEditCallback: (DataStoreHelper.Account) -> Unit,
private val accountDeleteCallback: (DataStoreHelper.Account) -> Unit private val accountDeleteCallback: (DataStoreHelper.Account) -> Unit
) : RecyclerView.Adapter<AccountAdapter.AccountViewHolder>() { ) : NoStateAdapter<DataStoreHelper.Account>() {
companion object { companion object {
const val VIEW_TYPE_SELECT_ACCOUNT = 0 const val VIEW_TYPE_SELECT_ACCOUNT = 0
const val VIEW_TYPE_ADD_ACCOUNT = 1
const val VIEW_TYPE_EDIT_ACCOUNT = 2 const val VIEW_TYPE_EDIT_ACCOUNT = 2
} }
inner class AccountViewHolder(private val binding: ViewBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(account: DataStoreHelper.Account?) { override val footers: Int = 1
when (binding) { var viewType = VIEW_TYPE_SELECT_ACCOUNT
override fun customContentViewType(item: DataStoreHelper.Account): Int {
return viewType
}
override fun onBindContent(
holder: ViewHolderState<Any>,
item: DataStoreHelper.Account,
position: Int
) {
when (val binding = holder.view) {
is AccountListItemBinding -> binding.apply { is AccountListItemBinding -> binding.apply {
if (account == null) return@apply
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex val isLastUsedAccount = item.keyIndex == DataStoreHelper.selectedKeyIndex
accountName.text = account.name accountName.text = item.name
accountImage.loadImage(account.image) accountImage.loadImage(item.image)
lockIcon.isVisible = account.lockPin != null lockIcon.isVisible = item.lockPin != null
outline.isVisible = !isTv && isLastUsedAccount outline.isVisible = !isTv && isLastUsedAccount
if (isTv) { if (isTv) {
@ -56,18 +62,28 @@ class AccountAdapter(
root.requestFocus() root.requestFocus()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
root.foreground = ContextCompat.getDrawable( root.foreground = ContextCompat.getDrawable(
root.context, root.context,
R.drawable.outline_drawable R.drawable.outline_drawable
) )
}
} else { } else {
root.setOnLongClickListener { root.setOnLongClickListener {
showAccountEditDialog( showAccountEditDialog(
context = root.context, context = root.context,
account = account, account = item,
isNewAccount = false, isNewAccount = false,
accountEditCallback = { account -> accountEditCallback.invoke(account) }, accountEditCallback = { account ->
accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) } accountEditCallback.invoke(
account
)
},
accountDeleteCallback = { account ->
accountDeleteCallback.invoke(
account
)
}
) )
true true
@ -75,22 +91,20 @@ class AccountAdapter(
} }
root.setOnClickListener { root.setOnClickListener {
accountSelectCallback.invoke(account) accountSelectCallback.invoke(item)
} }
} }
is AccountListItemEditBinding -> binding.apply { is AccountListItemEditBinding -> binding.apply {
if (account == null) return@apply
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex val isLastUsedAccount = item.keyIndex == DataStoreHelper.selectedKeyIndex
accountName.text = account.name accountName.text = item.name
accountImage.loadImage(account.image) { accountImage.loadImage(item.image) {
RoundedCornersTransformation(10f) RoundedCornersTransformation(10f)
} }
lockIcon.isVisible = account.lockPin != null lockIcon.isVisible = item.lockPin != null
outline.isVisible = !isTv && isLastUsedAccount outline.isVisible = !isTv && isLastUsedAccount
if (isTv) { if (isTv) {
@ -100,31 +114,47 @@ class AccountAdapter(
root.requestFocus() root.requestFocus()
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
root.foreground = ContextCompat.getDrawable( root.foreground = ContextCompat.getDrawable(
root.context, root.context,
R.drawable.outline_drawable R.drawable.outline_drawable
) )
} }
}
root.setOnClickListener { root.setOnClickListener {
showAccountEditDialog( showAccountEditDialog(
context = root.context, context = root.context,
account = account, account = item,
isNewAccount = false, isNewAccount = false,
accountEditCallback = { account -> accountEditCallback.invoke(account) }, accountEditCallback = { account -> accountEditCallback.invoke(account) },
accountDeleteCallback = { account -> accountDeleteCallback.invoke(account) } accountDeleteCallback = { account ->
accountDeleteCallback.invoke(
account
)
}
) )
} }
} }
}
}
is AccountListItemAddBinding -> binding.apply { override fun onBindFooter(holder: ViewHolderState<Any>) {
val binding = holder.view as? AccountListItemAddBinding ?: return
binding.apply {
root.setOnClickListener { root.setOnClickListener {
val accounts = this@AccountAdapter.immutableCurrentList
val remainingImages = val remainingImages =
DataStoreHelper.profileImages.toSet() - accounts.filter { it.customImage == null } DataStoreHelper.profileImages.toSet() - accounts.filter { it.customImage == null }
.mapNotNull { DataStoreHelper.profileImages.getOrNull(it.defaultImageIndex) }.toSet() .mapNotNull { DataStoreHelper.profileImages.getOrNull(it.defaultImageIndex) }
.toSet()
val image = val image =
DataStoreHelper.profileImages.indexOf(remainingImages.randomOrNull() ?: DataStoreHelper.profileImages.random()) DataStoreHelper.profileImages.indexOf(
remainingImages.randomOrNull()
?: DataStoreHelper.profileImages.random()
)
val keyIndex = (accounts.maxOfOrNull { it.keyIndex } ?: 0) + 1 val keyIndex = (accounts.maxOfOrNull { it.keyIndex } ?: 0) + 1
val accountName = root.context.getString(R.string.account) val accountName = root.context.getString(R.string.account)
@ -144,12 +174,20 @@ class AccountAdapter(
} }
} }
} }
}
override fun onCreateFooter(parent: ViewGroup): ViewHolderState<Any> {
return ViewHolderState(
AccountListItemAddBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder = override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
AccountViewHolder( return ViewHolderState(
binding = when (viewType) { when (viewType) {
VIEW_TYPE_SELECT_ACCOUNT -> { VIEW_TYPE_SELECT_ACCOUNT -> {
AccountListItemBinding.inflate( AccountListItemBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@ -157,13 +195,7 @@ class AccountAdapter(
false false
) )
} }
VIEW_TYPE_ADD_ACCOUNT -> {
AccountListItemAddBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
}
VIEW_TYPE_EDIT_ACCOUNT -> { VIEW_TYPE_EDIT_ACCOUNT -> {
AccountListItemEditBinding.inflate( AccountListItemEditBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@ -171,28 +203,9 @@ class AccountAdapter(
false false
) )
} }
else -> throw IllegalArgumentException("Invalid view type") else -> throw IllegalArgumentException("Invalid view type")
} }
) )
override fun onBindViewHolder(holder: AccountViewHolder, position: Int) {
holder.bind(accounts.getOrNull(position))
}
var viewType = 0
override fun getItemViewType(position: Int): Int {
if (viewType != 0 && position != accounts.count()) {
return viewType
}
return when (position) {
accounts.count() -> VIEW_TYPE_ADD_ACCOUNT
else -> VIEW_TYPE_SELECT_ACCOUNT
}
}
override fun getItemCount(): Int {
return accounts.count() + 1
} }
} }

View file

@ -3,11 +3,12 @@ package com.lagradost.cloudstream3.ui.account
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone import androidx.core.view.isGone
@ -16,12 +17,17 @@ import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil3.ImageLoader
import coil3.request.ImageRequest
import coil3.request.allowHardware
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountEditDialogBinding import com.lagradost.cloudstream3.databinding.AccountEditDialogBinding
import com.lagradost.cloudstream3.databinding.AccountSelectLinearBinding import com.lagradost.cloudstream3.databinding.AccountSelectLinearBinding
import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.LockPinDialogBinding import com.lagradost.cloudstream3.databinding.LockPinDialogBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
@ -31,7 +37,10 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount import com.lagradost.cloudstream3.utils.DataStoreHelper.getDefaultAccount
import com.lagradost.cloudstream3.utils.ImageLoader.loadImage import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideProgress
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod
import com.lagradost.cloudstream3.utils.UIHelper.showProgress
object AccountHelper { object AccountHelper {
fun showAccountEditDialog( fun showAccountEditDialog(
@ -93,6 +102,7 @@ object AccountHelper {
binding.accountImage.loadImage(account.image) binding.accountImage.loadImage(account.image)
binding.accountImage.setOnClickListener { binding.accountImage.setOnClickListener {
// Roll the image forwards once // Roll the image forwards once
currentEditAccount = currentEditAccount.copy(customImage = null)
currentEditAccount = currentEditAccount =
currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % DataStoreHelper.profileImages.size) currentEditAccount.copy(defaultImageIndex = (currentEditAccount.defaultImageIndex + 1) % DataStoreHelper.profileImages.size)
binding.accountImage.loadImage(currentEditAccount.image) binding.accountImage.loadImage(currentEditAccount.image)
@ -155,6 +165,57 @@ object AccountHelper {
} }
canSetPin = true canSetPin = true
binding.editProfilePhotoButton.setOnClickListener {
val bottomSheetDialog = BottomSheetDialog(context)
val sheetBinding = BottomInputDialogBinding.inflate(LayoutInflater.from(context))
bottomSheetDialog.setContentView(sheetBinding.root)
bottomSheetDialog.show()
sheetBinding.apply {
text1.text = context.getString(R.string.edit_profile_image_title)
nginxTextInput.hint = context.getString(R.string.edit_profile_image_hint)
applyBtt.setOnClickListener {
val url = sheetBinding.nginxTextInput.text.toString()
if (url.isEmpty()) {
showToast(R.string.edit_profile_image_error_empty, Toast.LENGTH_SHORT)
return@setOnClickListener
}
applyBtt.showProgress()
val imageLoader = ImageLoader(context)
val request = ImageRequest.Builder(context)
.data(url)
.allowHardware(false)
.listener(
onSuccess = { _, _ ->
currentEditAccount = currentEditAccount.copy(customImage = url)
binding.accountImage.loadImage(url)
showToast(
R.string.edit_profile_image_success,
Toast.LENGTH_SHORT
)
bottomSheetDialog.dismissSafe()
},
onError = { _, _ ->
showToast(
R.string.edit_profile_image_error_invalid,
Toast.LENGTH_SHORT
)
applyBtt.hideProgress()
},
onCancel = {
applyBtt.hideProgress()
}
)
.build()
imageLoader.enqueue(request)
}
sheetBinding.cancelBtt.setOnClickListener {
bottomSheetDialog.dismissSafe()
}
}
}
} }
fun showPinInputDialog( fun showPinInputDialog(
@ -217,7 +278,7 @@ object AccountHelper {
val activity = context.getActivity() val activity = context.getActivity()
if (activity is AccountSelectActivity) { if (activity is AccountSelectActivity) {
isPinValid = true isPinValid = true
activity.viewModel.handleAccountSelect(getDefaultAccount(context), activity) activity.accountViewModel.handleAccountSelect(getDefaultAccount(context), activity)
} }
} }
} }
@ -307,9 +368,10 @@ object AccountHelper {
builder.show() builder.show()
binding.manageAccountsButton.setOnClickListener { binding.manageAccountsButton.setOnClickListener {
val accountSelectIntent = Intent(activity, AccountSelectActivity::class.java) activity.navigate(
accountSelectIntent.putExtra("isEditingFromMainActivity", true) R.id.accountSelectActivity,
activity.startActivity(accountSelectIntent) Bundle().apply { putBoolean("isEditingFromMainActivity", true) }
)
builder.dismissSafe() builder.dismissSafe()
} }
@ -336,7 +398,6 @@ object AccountHelper {
activity.observe(viewModel.accounts) { liveAccounts -> activity.observe(viewModel.accounts) { liveAccounts ->
recyclerView.adapter = AccountAdapter( recyclerView.adapter = AccountAdapter(
liveAccounts,
accountSelectCallback = { account -> accountSelectCallback = { account ->
viewModel.handleAccountSelect(account, activity) viewModel.handleAccountSelect(account, activity)
builder.dismissSafe() builder.dismissSafe()
@ -344,7 +405,9 @@ object AccountHelper {
accountCreateCallback = { viewModel.handleAccountUpdate(it, activity) }, accountCreateCallback = { viewModel.handleAccountUpdate(it, activity) },
accountEditCallback = { viewModel.handleAccountUpdate(it, activity) }, accountEditCallback = { viewModel.handleAccountUpdate(it, activity) },
accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) } accountDeleteCallback = { viewModel.handleAccountDelete(it, activity) }
) ).apply {
submitList(liveAccounts)
}
activity.observe(viewModel.selectedKeyIndex) { selectedKeyIndex -> activity.observe(viewModel.selectedKeyIndex) { selectedKeyIndex ->
// Scroll to current account (which is focused by default) // Scroll to current account (which is focused by default)

View file

@ -1,11 +1,10 @@
package com.lagradost.cloudstream3.ui.account package com.lagradost.cloudstream3.ui.account
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider import androidx.activity.viewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity
@ -32,19 +31,22 @@ import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAut
import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts import com.lagradost.cloudstream3.utils.DataStoreHelper.accounts
import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex import com.lagradost.cloudstream3.utils.DataStoreHelper.selectedKeyIndex
import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount import com.lagradost.cloudstream3.utils.DataStoreHelper.setAccount
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.enableEdgeToEdgeCompat
import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding
import com.lagradost.cloudstream3.utils.UIHelper.openActivity
import com.lagradost.cloudstream3.utils.UIHelper.setNavigationBarColorCompat
class AccountSelectActivity : AppCompatActivity(), BiometricCallback { class AccountSelectActivity : FragmentActivity(), BiometricCallback {
lateinit var viewModel: AccountViewModel companion object {
var hasLoggedIn: Boolean = false
}
val accountViewModel: AccountViewModel by viewModels()
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
loadThemes(this)
@Suppress("DEPRECATION")
window.navigationBarColor = colorFromAttribute(R.attr.primaryBlackBackground)
// Are we editing and coming from MainActivity? // Are we editing and coming from MainActivity?
val isEditingFromMainActivity = intent.getBooleanExtra( val isEditingFromMainActivity = intent.getBooleanExtra(
@ -52,11 +54,23 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
false false
) )
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) // Sometimes we start this activity when we have already logged in
val skipStartup = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false // For example when using cloudstreamsearch://
) || accounts.count() <= 1 // In those cases we want to just go to the main activity instantly
if (hasLoggedIn && !isEditingFromMainActivity) {
navigateToMainActivity()
return
}
viewModel = ViewModelProvider(this)[AccountViewModel::class.java] loadThemes(this)
enableEdgeToEdgeCompat()
setNavigationBarColorCompat(R.attr.primaryBlackBackground)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val skipStartup = settingsManager.getBoolean(
getString(R.string.skip_startup_account_select_key), false
) || accounts.count() <= 1
fun askBiometricAuth() { fun askBiometricAuth() {
@ -75,7 +89,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
} }
} }
observe(viewModel.isAllowedLogin) { isAllowedLogin -> observe(accountViewModel.isAllowedLogin) { isAllowedLogin ->
if (isAllowedLogin) { if (isAllowedLogin) {
// We are allowed to continue to MainActivity // We are allowed to continue to MainActivity
navigateToMainActivity() navigateToMainActivity()
@ -88,13 +102,15 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
val currentAccount = accounts.firstOrNull { it.keyIndex == selectedKeyIndex } val currentAccount = accounts.firstOrNull { it.keyIndex == selectedKeyIndex }
if (currentAccount?.lockPin != null) { if (currentAccount?.lockPin != null) {
CommonActivity.init(this) CommonActivity.init(this)
viewModel.handleAccountSelect(currentAccount, this, true) accountViewModel.handleAccountSelect(currentAccount, this, true)
} else { } else {
if (accounts.count() > 1) { if (accounts.count() > 1) {
showToast(this, getString( showToast(
this, getString(
R.string.logged_account, R.string.logged_account,
currentAccount?.name currentAccount?.name
)) )
)
} }
navigateToMainActivity() navigateToMainActivity()
@ -107,20 +123,19 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
val binding = ActivityAccountSelectBinding.inflate(layoutInflater) val binding = ActivityAccountSelectBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
fixSystemBarsPadding(binding.root, padTop = false)
val recyclerView: AutofitRecyclerView = binding.accountRecyclerView val recyclerView: AutofitRecyclerView = binding.accountRecyclerView
observe(viewModel.accounts) { liveAccounts -> observe(accountViewModel.accounts) { liveAccounts ->
val adapter = AccountAdapter( val adapter = AccountAdapter(
liveAccounts,
// Handle the selected account // Handle the selected account
accountSelectCallback = { accountSelectCallback = {
viewModel.handleAccountSelect(it, this) accountViewModel.handleAccountSelect(it, this)
}, },
accountCreateCallback = { viewModel.handleAccountUpdate(it, this) }, accountCreateCallback = { accountViewModel.handleAccountUpdate(it, this) },
accountEditCallback = { accountEditCallback = {
viewModel.handleAccountUpdate(it, this) accountViewModel.handleAccountUpdate(it, this)
// We came from MainActivity, return there // We came from MainActivity, return there
// and switch to the edited account // and switch to the edited account
if (isEditingFromMainActivity) { if (isEditingFromMainActivity) {
@ -128,8 +143,10 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
navigateToMainActivity() navigateToMainActivity()
} }
}, },
accountDeleteCallback = { viewModel.handleAccountDelete(it,this) } accountDeleteCallback = { accountViewModel.handleAccountDelete(it, this) }
) ).apply {
submitList(liveAccounts)
}
recyclerView.adapter = adapter recyclerView.adapter = adapter
@ -139,13 +156,13 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
) )
} }
observe(viewModel.selectedKeyIndex) { selectedKeyIndex -> observe(accountViewModel.selectedKeyIndex) { selectedKeyIndex ->
// Scroll to current account (which is focused by default) // Scroll to current account (which is focused by default)
val layoutManager = recyclerView.layoutManager as GridLayoutManager val layoutManager = recyclerView.layoutManager as GridLayoutManager
layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0) layoutManager.scrollToPositionWithOffset(selectedKeyIndex, 0)
} }
observe(viewModel.isEditing) { isEditing -> observe(accountViewModel.isEditing) { isEditing ->
if (isEditing) { if (isEditing) {
binding.editAccountButton.setImageResource(R.drawable.ic_baseline_close_24) binding.editAccountButton.setImageResource(R.drawable.ic_baseline_close_24)
binding.title.setText(R.string.manage_accounts) binding.title.setText(R.string.manage_accounts)
@ -160,7 +177,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
} }
if (isEditingFromMainActivity) { if (isEditingFromMainActivity) {
viewModel.setIsEditing(true) accountViewModel.setIsEditing(true)
} }
binding.editAccountButton.setOnClickListener { binding.editAccountButton.setOnClickListener {
@ -171,7 +188,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
return@setOnClickListener return@setOnClickListener
} }
viewModel.toggleIsEditing() accountViewModel.toggleIsEditing()
} }
if (isLayout(TV or EMULATOR)) { if (isLayout(TV or EMULATOR)) {
@ -184,9 +201,11 @@ class AccountSelectActivity : AppCompatActivity(), BiometricCallback {
askBiometricAuth() askBiometricAuth()
} }
@SuppressLint("UnsafeIntentLaunch")
private fun navigateToMainActivity() { private fun navigateToMainActivity() {
val mainIntent = Intent(this, MainActivity::class.java) hasLoggedIn = true
startActivity(mainIntent) // We want to propagate any intent we get here to MainActivity since this is just an intermediary
openActivity(MainActivity::class.java, baseIntent = intent)
finish() // Finish the account selection activity finish() // Finish the account selection activity
} }

View file

@ -4,8 +4,8 @@ import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.CloudStreamApp.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKeys
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.ui.account.AccountHelper.showPinInputDialog import com.lagradost.cloudstream3.ui.account.AccountHelper.showPinInputDialog
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper

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