Compare commits

...

298 commits

Author SHA1 Message Date
LagradOst
67b0549fd2
remove images 2023-03-21 21:01:47 +00:00
Osten
52d495f425
Update README.md 2023-03-21 20:50:13 +00:00
Cloudburst
0cbee70683
[skip ci] Update locales.py 2023-03-19 12:51:54 +01:00
Lag
4235c826a5 Better focus on Android TV
(Thank you ocean for reporting)
2023-03-18 23:55:58 +01:00
Cloudburst
5245eff6e1
[skip ci] fix xml header being slightly wrong 2023-03-18 09:22:07 +01:00
Lag
9c40abc4d3 Added player intent 2023-03-17 22:15:25 +01:00
Lag
019399952f Better subtitle decoding :) 2023-03-17 16:23:03 +01:00
Lag
cc99899cf1 Merge remote-tracking branch 'origin/master' 2023-03-17 16:07:33 +01:00
Lag
8fff809b79 Revert ffmpeg as it causes issues with subtitles :( 2023-03-17 16:07:28 +01:00
recloudstream[bot]
67318a62a3 chore(locales): fix locale issues 2023-03-17 15:04:00 +00:00
Hosted Weblate
288c5ffa39 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (German)

Currently translated at 100.0% (610 of 610 strings)

Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2023-03-17 16:03:40 +01:00
Lag
8ebf5185a3 Add ffmpeg audio decoding 2023-03-17 15:46:11 +01:00
Cloudburst
7bfcf25df4 add a way to autofix weblate's issue with @string 2023-03-14 18:50:13 +00:00
Lag
2d7126d71f Fix for fix for translations 2023-03-14 13:12:34 +01:00
Lag
40a4f319b6 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	app/src/main/res/values-uk/strings.xml
2023-03-14 13:01:08 +01:00
Lag
19dc1a2456 Un-bruh-momented some translations 2023-03-14 12:59:32 +01:00
Cloudburst
ac1012bcb8
Merge pull request #420 from recloudstream/weblate-guh 2023-03-13 18:32:15 +01:00
Cloudburst
ec3950ed4f Merge branch 'master' of https://hosted.weblate.org/git/cloudstream/app 2023-03-13 18:18:13 +01:00
Hosted Weblate
3e2b0f2a17 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Dutch)

Currently translated at 74.0% (452 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Added translation using Weblate (Malay)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 99.1% (605 of 610 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Frank Gerritsen Mulkes <frankgmwerk@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Samuel Gadiel <samuelgadiel@gmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: TZVS <akyasan@tuta.io>
Co-authored-by: Tang Yin <bingyuanshiye@126.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
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/id/
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/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/
Translation: Cloudstream/App
2023-03-13 18:18:01 +01:00
LikDev-256
29174dbb30
Feat: fix Streamsb (#417)
* Fix Streamsb

* feat(StreamSB) stream break: support audiotracks

* Revert "feat(StreamSB) stream break: support audiotracks"

This reverts commit 078caf9f88.

* Feat: fix Streamsb

They normally update source numbers like 50, 51 but instead of 52 they totally dumped everything and just flipped the number into 15
2023-03-13 16:11:35 +00:00
Lag
7b47f93190 Merge remote-tracking branch 'origin/master' 2023-03-10 21:33:27 +01:00
Lag
13ee8e21d0 Semi-unfucked VLC on A13+ 2023-03-10 21:33:13 +01:00
recloudstream[bot]
3a5d872545 update list of locales 2023-03-10 20:01:20 +00:00
Hosted Weblate
fab55d82c4 Translated using Weblate (Portuguese)
Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Dutch)

Currently translated at 74.0% (452 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Added translation using Weblate (Malay)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 99.1% (605 of 610 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Frank Gerritsen Mulkes <frankgmwerk@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Samuel Gadiel <samuelgadiel@gmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: TZVS <akyasan@tuta.io>
Co-authored-by: Tang Yin <bingyuanshiye@126.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
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/id/
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/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/
Translation: Cloudstream/App
2023-03-10 21:01:04 +01:00
Hosted Weblate
8b2881f5f6
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Dutch)

Currently translated at 74.0% (452 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Added translation using Weblate (Malay)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 99.1% (605 of 610 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (English)

Currently translated at 100.0% (610 of 610 strings)

Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Frank Gerritsen Mulkes <frankgmwerk@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Samuel Gadiel <samuelgadiel@gmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: TZVS <akyasan@tuta.io>
Co-authored-by: Tang Yin <bingyuanshiye@126.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
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/id/
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/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/
Translation: Cloudstream/App
2023-03-10 20:45:19 +01:00
PokerFace
37244ab0f7
Intertal Player: Added MPD support (#402)
* added isDash in ExtractorLink
2023-03-10 19:45:11 +00:00
Lag
e85b31c35d Fixing rouge pixels in settings 2023-03-07 17:36:53 +01:00
Hosted Weblate
1eaa4620dc Translated using Weblate (qt (generated) (qt))
Currently translated at 54.5% (333 of 610 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (French)

Currently translated at 98.8% (603 of 610 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (German)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (608 of 610 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.0% (519 of 610 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Vietnamese)

Currently translated at 96.8% (591 of 610 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (610 of 610 strings)

Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Duc Nguyen Tien <ducnt123@gmail.com>
Co-authored-by: Felipe Nogueira <contato.fnog@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Samuel Gadiel <samuelgadiel@gmail.com>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
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/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/qt/
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/app/zh_Hans/
Translation: Cloudstream/App
2023-03-03 18:16:40 +01:00
no-commit
76545f55c3 Standardized some home screen padding and made subtitle delay persistent. Fixes #405 2023-03-03 17:45:26 +01:00
Stormunblessed
f0515c4dc9
Support qualities for Dailymotion (#407)
* Dailymotion qualities
2023-03-03 09:24:02 +00:00
no-commit
ab324b93e8 Small fixes to Intents and Subscriptions 2023-02-28 01:19:59 +01:00
Stormunblessed
d6df24eff2
Fixes on filesim and added filemoon, ztreamhub (#397)
* fix fastream, tomatomatela, and added okrulink

* forgot this

* sendvid extractor

* sendvid extractor

* fixes

* Filesim fix, added filemoon and ztreamhub
2023-02-27 20:05:42 +00:00
Hosted Weblate
e5834d485b Translated using Weblate (German)
Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (610 of 610 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.0% (493 of 608 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (605 of 608 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (608 of 608 strings)

Translated using Weblate (Polish)

Currently translated at 97.3% (592 of 608 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Japanese)

Currently translated at 44.5% (268 of 602 strings)

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

Currently translated at 50.4% (304 of 602 strings)

Translated using Weblate (Slovak)

Currently translated at 31.7% (191 of 602 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.9% (463 of 602 strings)

Translated using Weblate (Somali)

Currently translated at 94.3% (568 of 602 strings)

Translated using Weblate (Somali)

Currently translated at 94.3% (568 of 602 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.5% (268 of 602 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.5% (268 of 602 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.5% (268 of 602 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Esperanto)

Currently translated at 27.5% (166 of 602 strings)

Translated using Weblate (Esperanto)

Currently translated at 27.5% (166 of 602 strings)

Translated using Weblate (Persian)

Currently translated at 20.0% (121 of 602 strings)

Translated using Weblate (Hungarian)

Currently translated at 55.6% (335 of 602 strings)

Translated using Weblate (German)

Currently translated at 99.1% (597 of 602 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (597 of 602 strings)

Translated using Weblate (Kannada)

Currently translated at 35.2% (212 of 602 strings)

Translated using Weblate (Urdu)

Currently translated at 72.2% (435 of 602 strings)

Translated using Weblate (Tamil)

Currently translated at 18.2% (110 of 602 strings)

Translated using Weblate (Tamil)

Currently translated at 18.2% (110 of 602 strings)

Translated using Weblate (Hebrew)

Currently translated at 97.1% (585 of 602 strings)

Translated using Weblate (Bengali)

Currently translated at 38.7% (233 of 602 strings)

Translated using Weblate (Bengali)

Currently translated at 38.7% (233 of 602 strings)

Translated using Weblate (Bengali)

Currently translated at 38.7% (233 of 602 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.1% (567 of 602 strings)

Translated using Weblate (Vietnamese)

Currently translated at 96.8% (583 of 602 strings)

Translated using Weblate (Turkish)

Currently translated at 97.1% (585 of 602 strings)

Translated using Weblate (Tagalog)

Currently translated at 56.1% (338 of 602 strings)

Translated using Weblate (Tagalog)

Currently translated at 56.1% (338 of 602 strings)

Translated using Weblate (Tagalog)

Currently translated at 56.1% (338 of 602 strings)

Translated using Weblate (Swedish)

Currently translated at 74.9% (451 of 602 strings)

Translated using Weblate (Swedish)

Currently translated at 74.9% (451 of 602 strings)

Translated using Weblate (Swedish)

Currently translated at 74.9% (451 of 602 strings)

Translated using Weblate (Romanian)

Currently translated at 73.0% (440 of 602 strings)

Translated using Weblate (Romanian)

Currently translated at 73.0% (440 of 602 strings)

Translated using Weblate (Romanian)

Currently translated at 73.0% (440 of 602 strings)

Translated using Weblate (Polish)

Currently translated at 98.0% (590 of 602 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 88.3% (532 of 602 strings)

Translated using Weblate (Dutch)

Currently translated at 75.0% (452 of 602 strings)

Translated using Weblate (Dutch)

Currently translated at 75.0% (452 of 602 strings)

Translated using Weblate (Dutch)

Currently translated at 75.0% (452 of 602 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.2% (224 of 602 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.2% (224 of 602 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.2% (224 of 602 strings)

Translated using Weblate (Macedonian)

Currently translated at 48.6% (293 of 602 strings)

Translated using Weblate (Macedonian)

Currently translated at 48.6% (293 of 602 strings)

Translated using Weblate (Macedonian)

Currently translated at 48.6% (293 of 602 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (597 of 602 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Hindi)

Currently translated at 37.7% (227 of 602 strings)

Translated using Weblate (Hindi)

Currently translated at 37.7% (227 of 602 strings)

Translated using Weblate (Hindi)

Currently translated at 37.7% (227 of 602 strings)

Translated using Weblate (French)

Currently translated at 97.3% (586 of 602 strings)

Translated using Weblate (Greek)

Currently translated at 97.0% (584 of 602 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 77.4% (466 of 602 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 77.4% (466 of 602 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 77.4% (466 of 602 strings)

Translated using Weblate (Bulgarian)

Currently translated at 94.5% (569 of 602 strings)

Translated using Weblate (Bulgarian)

Currently translated at 94.5% (569 of 602 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (602 of 602 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Japanese)

Currently translated at 44.5% (268 of 602 strings)

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

Currently translated at 50.4% (304 of 602 strings)

Translated using Weblate (Slovak)

Currently translated at 31.7% (191 of 602 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.9% (463 of 602 strings)

Translated using Weblate (Somali)

Currently translated at 94.3% (568 of 602 strings)

Translated using Weblate (Somali)

Currently translated at 94.3% (568 of 602 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.5% (268 of 602 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.5% (268 of 602 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 44.5% (268 of 602 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Esperanto)

Currently translated at 27.5% (166 of 602 strings)

Translated using Weblate (Esperanto)

Currently translated at 27.5% (166 of 602 strings)

Translated using Weblate (Persian)

Currently translated at 20.0% (121 of 602 strings)

Translated using Weblate (Hungarian)

Currently translated at 55.6% (335 of 602 strings)

Translated using Weblate (German)

Currently translated at 99.1% (597 of 602 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (597 of 602 strings)

Translated using Weblate (Kannada)

Currently translated at 35.2% (212 of 602 strings)

Translated using Weblate (Kannada)

Currently translated at 35.2% (212 of 602 strings)

Translated using Weblate (Urdu)

Currently translated at 72.2% (435 of 602 strings)

Translated using Weblate (Tamil)

Currently translated at 18.2% (110 of 602 strings)

Translated using Weblate (Tamil)

Currently translated at 18.2% (110 of 602 strings)

Translated using Weblate (Hebrew)

Currently translated at 97.1% (585 of 602 strings)

Translated using Weblate (Bengali)

Currently translated at 38.7% (233 of 602 strings)

Translated using Weblate (Bengali)

Currently translated at 38.7% (233 of 602 strings)

Translated using Weblate (Bengali)

Currently translated at 38.7% (233 of 602 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.1% (567 of 602 strings)

Translated using Weblate (Vietnamese)

Currently translated at 96.8% (583 of 602 strings)

Translated using Weblate (Turkish)

Currently translated at 97.1% (585 of 602 strings)

Translated using Weblate (Tagalog)

Currently translated at 56.1% (338 of 602 strings)

Translated using Weblate (Tagalog)

Currently translated at 56.1% (338 of 602 strings)

Translated using Weblate (Tagalog)

Currently translated at 56.1% (338 of 602 strings)

Translated using Weblate (Swedish)

Currently translated at 74.9% (451 of 602 strings)

Translated using Weblate (Swedish)

Currently translated at 74.9% (451 of 602 strings)

Translated using Weblate (Swedish)

Currently translated at 74.9% (451 of 602 strings)

Translated using Weblate (Romanian)

Currently translated at 73.0% (440 of 602 strings)

Translated using Weblate (Romanian)

Currently translated at 73.0% (440 of 602 strings)

Translated using Weblate (Romanian)

Currently translated at 73.0% (440 of 602 strings)

Translated using Weblate (Polish)

Currently translated at 98.0% (590 of 602 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 88.3% (532 of 602 strings)

Translated using Weblate (Dutch)

Currently translated at 75.0% (452 of 602 strings)

Translated using Weblate (Dutch)

Currently translated at 75.0% (452 of 602 strings)

Translated using Weblate (Dutch)

Currently translated at 75.0% (452 of 602 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.2% (224 of 602 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.2% (224 of 602 strings)

Translated using Weblate (Malayalam)

Currently translated at 37.2% (224 of 602 strings)

Translated using Weblate (Macedonian)

Currently translated at 48.6% (293 of 602 strings)

Translated using Weblate (Macedonian)

Currently translated at 48.6% (293 of 602 strings)

Translated using Weblate (Macedonian)

Currently translated at 48.6% (293 of 602 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (597 of 602 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Hindi)

Currently translated at 37.7% (227 of 602 strings)

Translated using Weblate (Hindi)

Currently translated at 37.7% (227 of 602 strings)

Translated using Weblate (Hindi)

Currently translated at 37.7% (227 of 602 strings)

Translated using Weblate (French)

Currently translated at 97.3% (586 of 602 strings)

Translated using Weblate (Greek)

Currently translated at 97.0% (584 of 602 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 77.4% (466 of 602 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 77.4% (466 of 602 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 77.4% (466 of 602 strings)

Translated using Weblate (Bulgarian)

Currently translated at 94.5% (569 of 602 strings)

Translated using Weblate (Bulgarian)

Currently translated at 94.5% (569 of 602 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (602 of 602 strings)

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Geovani Amaral <geovani.af4@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: MedRAM <mohammad7ram@users.noreply.hosted.weblate.org>
Co-authored-by: Prathap Rathod <prathap0144@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sandyran <sandyran@protonmail.com>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.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/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bp/
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/fr/
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/mk/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ml/
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/nn/
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/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/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
2023-02-25 22:21:25 +01:00
Sarlay
6524eb220b
Added some extractors mirrors and added Vido Extractor (#393)
* Added some mirrors and fixed some extractors

* Fixed Vido extractor (for MesFilms and Wiflix)
2023-02-25 21:18:48 +00:00
Allen Baby
2926dc6c8e
Issue #376: Added new feature for separate watch quality on mobile data. (#391)
* Issue #376: Added new feature for separate watch quality on mobile data.
2023-02-24 18:51:03 +00:00
Hexated
f722785a37
fixed Linkbox (#390) 2023-02-24 18:49:53 +00:00
Lag
aeab423d29 Excluded the referer header when empty 2023-02-24 18:47:54 +01:00
recloudstream[bot]
1da6a92569 update list of locales 2023-02-21 18:07:04 +00:00
Cloudburst
b2fa765a2d
Merge pull request #364 from recloudstream/weblate 2023-02-21 19:06:48 +01:00
Cloudburst
bec0a2e7b9
Merge branch 'master' into weblate 2023-02-21 19:06:13 +01:00
Cloudburst
51137701f2
add proxy to raw.githubusercontent.com (#368) 2023-02-21 18:43:35 +01:00
Hosted Weblate
5f12d067f9
Translated using Weblate (Indonesian)
Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Slovak)

Currently translated at 31.7% (191 of 602 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (602 of 602 strings)

Translated using Weblate (German)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Japanese)

Currently translated at 44.8% (268 of 597 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Japanese)

Currently translated at 40.5% (242 of 597 strings)

Translated using Weblate (Japanese)

Currently translated at 34.5% (206 of 597 strings)

Translated using Weblate (Japanese)

Currently translated at 33.1% (198 of 597 strings)

Translated using Weblate (Kannada)

Currently translated at 35.5% (212 of 597 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Japanese)

Currently translated at 25.7% (154 of 597 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (German)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Romanian)

Currently translated at 73.7% (440 of 597 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (597 of 597 strings)

Translated using Weblate (Japanese)

Currently translated at 22.7% (134 of 589 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Japanese)

Currently translated at 17.8% (105 of 589 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.3% (585 of 589 strings)

Translated using Weblate (Japanese)

Currently translated at 16.6% (98 of 589 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (587 of 589 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (586 of 589 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.0% (566 of 589 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (German)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (French)

Currently translated at 99.3% (585 of 589 strings)

Added translation using Weblate (Japanese)

Translated using Weblate (Croatian)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (583 of 589 strings)

Translated using Weblate (French)

Currently translated at 98.6% (581 of 589 strings)

Translated using Weblate (French)

Currently translated at 96.6% (569 of 589 strings)

Translated using Weblate (Greek)

Currently translated at 98.9% (583 of 589 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (589 of 589 strings)

jsdelivr wrapper to githubusercontent

Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Andrey Zapolsky <zapoland@gmail.com>
Co-authored-by: Cliff Heraldo <123844876+clxf12@users.noreply.github.com>
Co-authored-by: Cliff Heraldo <cliffkbrt11@gmail.com>
Co-authored-by: Cliff Heraldo <clxf12@users.noreply.hosted.weblate.org>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Deleted User <noreply+57159@weblate.org>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Gabriel Cnudde <gabriel.cnudde59@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: Juraj Liso <lisojuraj@gmail.com>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: MedRAM <mohammad7ram@users.noreply.hosted.weblate.org>
Co-authored-by: Piotr Strebski <strebski@gmail.com>
Co-authored-by: Prathap Rathod <prathap0144@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Co-authored-by: abcabcc <xmmandxpp@outlook.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
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/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/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/kn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
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/uk/
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
2023-02-21 18:28:21 +01:00
no-commit
00a91ca5fb Added Subscriptions (pinged every ~6 hours) 2023-02-19 19:27:40 +01:00
LikDev-256
33aecfbba5
Fix Streamsb (#380) 2023-02-18 12:15:50 +00:00
no-commit
0185854682 Merge remote-tracking branch 'origin/master' 2023-02-17 23:06:16 +01:00
no-commit
b4065b69be Added dropdown indicators
Solves #375
2023-02-17 23:05:11 +01:00
MhmdIbrahim1
b6ac155350
update VideoDownloadService (#377) 2023-02-17 21:42:20 +00:00
Lag
aacd57cb5d Fixed scrolling up on bottom dialogs and removing stuff from AniList 2023-02-16 01:15:30 +01:00
Lag
3dd0fc6c8e add view_test 2023-02-15 22:09:08 +01:00
Lag
135f63afff Merge remote-tracking branch 'origin/master' 2023-02-15 21:41:28 +01:00
Lag
789cd14ef6 remove placeholder 2023-02-15 21:41:20 +01:00
recloudstream[bot]
9d0cce47a6 update list of locales 2023-02-15 20:40:50 +00:00
Lag
4a8ee55018 Added provider tests 2023-02-15 21:40:10 +01:00
Stormunblessed
df6c395acb
Sendvid extractor (#365)
* fix fastream, tomatomatela, and added okrulink

* forgot this

* sendvid extractor

* sendvid extractor

* fixes
2023-02-14 15:11:20 +00:00
Cloudburst
1117271a71
[skip ci] português brasileiro 2023-02-10 15:16:58 +01:00
Cloudburst
7b11b9b585
fix wrong hebrew lang code 2023-02-09 20:27:37 +01:00
recloudstream[bot]
5c20b479e5 update list of locales 2023-02-09 19:25:34 +00:00
Hosted Weblate
0d2613d183 Translated using Weblate (qt (generated) (qt))
Currently translated at 51.4% (303 of 589 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.3% (585 of 589 strings)

Translated using Weblate (Persian)

Currently translated at 20.3% (120 of 589 strings)

Translated using Weblate (Russian)

Currently translated at 98.9% (583 of 589 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (589 of 589 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (589 of 589 strings)

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

Currently translated at 50.3% (293 of 582 strings)

Translated using Weblate (Persian)

Currently translated at 18.7% (109 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (577 of 582 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Soroush <skaveh1384@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: sina <cnababaie@gmail.com>
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/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/qt/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translation: Cloudstream/App
2023-02-09 20:25:19 +01:00
Cloudburst
dd38556102
address issue #339
Co-authored-by: eightyy8 <64216434+eightyy8@users.noreply.github.com>
2023-02-09 20:25:00 +01:00
reduplicated
84493b7f3b mini fix 2023-02-09 01:46:07 +01:00
reduplicated
4596afee06 auto track anilist/mal 2023-02-09 01:32:48 +01:00
Sir Aguacata
3e2c2a5c86
Added a way for easy mal and anilist tracker (#359)
* Added a way for easy mal and anilist tracker, All credit gos to Hexated for helping me

* Made CodeFactor Fucking Happy

* prettified the getTracker method

* remove parenthesis

* fixed

---------

Co-authored-by: Blatzar <46196380+Blatzar@users.noreply.github.com>
Co-authored-by: reduplicated <110570621+reduplicated@users.noreply.github.com>
2023-02-08 23:58:15 +00:00
Terry Hanoman
6c646d65a8
Use user setting for seeking on Android TV (#342)
* Use user setting for seeking on Android TV

* Fixed left dpad seeking

* Added a Android TV section for seeking

* Fixed text

* Removed semi-colons
2023-02-08 15:46:39 +00:00
LagradOst
329966732f
increased app update buffer size 2023-02-07 16:01:14 +00:00
Hosted Weblate
19b2cae851 Translated using Weblate (English (en_MO))
Currently translated at 47.9% (279 of 582 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en_MO/
Translation: Cloudstream/App
2023-02-07 12:53:13 +01:00
recloudstream[bot]
45eb9758e3 update list of locales 2023-02-07 11:52:20 +00:00
Cloudburst
f6be6081dc use qt for monke language 2023-02-07 11:52:02 +00:00
recloudstream[bot]
80f22cea16 update list of locales 2023-02-07 10:21:26 +00:00
Cloudburst
bf78fc95c2 use en-rMO 2023-02-07 10:21:07 +00:00
Cloudburst
a148f347cd
[skip ci] issue action.yml 2023-02-07 11:15:00 +01:00
Cloudburst
ff9942407b use language a better language code for the easter egg 2023-02-07 10:11:23 +00:00
Hosted Weblate
0ea624ff14 Translated using Weblate (Russian)
Currently translated at 98.9% (576 of 582 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (575 of 582 strings)

Translated using Weblate (Hebrew)

Currently translated at 87.4% (509 of 582 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 98.4% (573 of 582 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 98.4% (573 of 582 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (German)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 98.2% (572 of 582 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Bulgarian)

Currently translated at 97.2% (566 of 582 strings)

Translated using Weblate (English)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Translated using Weblate (German)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (570 of 582 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 97.5% (568 of 582 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (582 of 582 strings)

Co-authored-by: Alexey <aleksejfedorov963@gmail.com>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Cliff Heraldo <cliffkbrt11@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Fjuro <ifjuro@proton.me>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joel Brink <joel.brink.handy@gmail.com>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Co-authored-by: Muhammet <zumruduanka0013@gmail.com>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: The Initiator <eithansten@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
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/en/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/
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/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/zh_Hans/
Translation: Cloudstream/App
2023-02-07 11:05:37 +01:00
Cloudburst
f939e4cff2 update langs to use native names fix #339 2023-02-07 10:04:27 +00:00
no-commit
2ff90c03ca Moved backup restore to IO thread. 2023-02-04 15:32:04 +01:00
no-commit
9988753432 Library and Light mode improvements. 2023-02-02 01:15:24 +01:00
LagradOst
b0921161a3
Nicehttp version bump 2023-01-31 23:43:29 +01:00
Cloudburst
490381451b
[skip ci] label issues if provider mentioned 2023-01-31 10:57:11 +01:00
hexated
b26a41bdaf fixed VidSrcExtractor 2023-01-31 09:13:46 +01:00
Hosted Weblate
c7c5fa250e Translated using Weblate (Polish)
Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 96.5% (562 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 91.4% (532 of 582 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 90.3% (526 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 90.3% (526 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 88.3% (514 of 582 strings)

Translated using Weblate (Russian)

Currently translated at 87.9% (512 of 582 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (German)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (582 of 582 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Vietnamese)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (581 of 581 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (581 of 581 strings)

Co-authored-by: Aitor Salaberria <trslbrr@gmail.com>
Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Co-authored-by: Cliff Heraldo <cliffkbrt11@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: JL Pilgram <twich_89@hotmail.it>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: NickSkier <nikita.vasiliev.02@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: gnu-ewm <gnu.ewm@protonmail.com>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: tuan041 <tuananh163025ttt@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
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/id/
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/ru/
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
2023-01-31 09:13:15 +01:00
Blatzar
6e9b1cb855 Made source dialog fullscreen and added some Extractors 2023-01-29 23:51:25 +01:00
Blatzar
fd2648df45 made the checkSafeModeFile() crash-proof 2023-01-29 16:31:16 +01:00
Blatzar
9905618a47 Added safe mode file as a last resort 2023-01-29 16:15:28 +01:00
LagradOst
2771dcb612
update version code 2023-01-28 22:38:55 +00:00
LagradOst
3c82548c20
Library merge (#343) 2023-01-28 22:38:02 +00:00
recloudstream[bot]
9d11dc76a1 update list of locales 2023-01-28 18:43:31 +00:00
Hosted Weblate
2a1311673a Translated using Weblate (Russian)
Currently translated at 88.0% (499 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 86.7% (492 of 567 strings)

Translated using Weblate (Romanian)

Currently translated at 75.6% (429 of 567 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Russian)

Currently translated at 86.5% (491 of 567 strings)

Translated using Weblate (Vietnamese)

Currently translated at 99.2% (563 of 567 strings)

Translated using Weblate (Slovak)

Currently translated at 33.1% (188 of 567 strings)

Translated using Weblate (Somali)

Currently translated at 99.6% (565 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 84.1% (477 of 567 strings)

Translated using Weblate (German)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 84.1% (477 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 84.1% (477 of 567 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (567 of 567 strings)

Added translation using Weblate (Slovak)

Translated using Weblate (Vietnamese)

Currently translated at 99.4% (564 of 567 strings)

Co-authored-by: Alexey <aleksejfedorov963@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: John Doe <vj4ud1mc@sonofdavid.anonaddy.me>
Co-authored-by: Juraj Liso <lisojuraj@gmail.com>
Co-authored-by: Shafici Isxariifshe <mega12xhaphiee@gmail.com>
Co-authored-by: alex <hdhdhfhfbbffhhfhfjfjf@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: tuan041 <30403510+tuan041@users.noreply.github.com>
Co-authored-by: tuan041 <tuananh163025ttt@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
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/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/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/vi/
Translation: Cloudstream/App
2023-01-28 19:43:15 +01:00
Blatzar
83d2e692e0 Made player_video_title_rez disappear if blank 2023-01-28 19:39:12 +01:00
Blatzar
b2389bf14c Add padding in info & download to remove obtrusion by FAB 2023-01-28 19:00:10 +01:00
LiJu09
b2b16fccc5
[extractor] added ByteShare (#337)
* add byteshare extractor

* reformat code

* make it simple

* no regex
2023-01-27 23:44:00 +00:00
Blatzar
01f1edab3c webview crash fix 2023-01-26 00:34:55 +01:00
reduplicated
5050ff65c0 disabled crash reporting because yall keep crashing 2023-01-25 15:06:48 +01:00
Hosted Weblate
de720983a6 Translated using Weblate (Russian)
Currently translated at 83.7% (475 of 567 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Portuguese)

Currently translated at 81.3% (461 of 567 strings)

Translated using Weblate (Somali)

Currently translated at 99.6% (565 of 567 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 46.7% (265 of 567 strings)

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 46.7% (265 of 567 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Esperanto)

Currently translated at 28.7% (163 of 567 strings)

Translated using Weblate (Persian)

Currently translated at 19.0% (108 of 567 strings)

Translated using Weblate (Hungarian)

Currently translated at 58.7% (333 of 567 strings)

Translated using Weblate (German)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 83.5% (474 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 83.5% (474 of 567 strings)

Translated using Weblate (Kannada)

Currently translated at 14.9% (85 of 567 strings)

Translated using Weblate (Urdu)

Currently translated at 76.3% (433 of 567 strings)

Translated using Weblate (Tamil)

Currently translated at 18.8% (107 of 567 strings)

Translated using Weblate (Hebrew)

Currently translated at 37.5% (213 of 567 strings)

Translated using Weblate (Bengali)

Currently translated at 40.7% (231 of 567 strings)

Translated using Weblate (Bengali)

Currently translated at 40.7% (231 of 567 strings)

Translated using Weblate (Bengali)

Currently translated at 40.7% (231 of 567 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.2% (563 of 567 strings)

Translated using Weblate (Vietnamese)

Currently translated at 87.6% (497 of 567 strings)

Translated using Weblate (Vietnamese)

Currently translated at 87.6% (497 of 567 strings)

Translated using Weblate (Vietnamese)

Currently translated at 87.6% (497 of 567 strings)

Translated using Weblate (Turkish)

Currently translated at 97.0% (550 of 567 strings)

Translated using Weblate (Tagalog)

Currently translated at 59.2% (336 of 567 strings)

Translated using Weblate (Tagalog)

Currently translated at 59.2% (336 of 567 strings)

Translated using Weblate (Tagalog)

Currently translated at 59.2% (336 of 567 strings)

Translated using Weblate (Swedish)

Currently translated at 79.1% (449 of 567 strings)

Translated using Weblate (Swedish)

Currently translated at 79.1% (449 of 567 strings)

Translated using Weblate (Swedish)

Currently translated at 79.1% (449 of 567 strings)

Translated using Weblate (Romanian)

Currently translated at 74.7% (424 of 567 strings)

Translated using Weblate (Romanian)

Currently translated at 74.7% (424 of 567 strings)

Translated using Weblate (Romanian)

Currently translated at 74.7% (424 of 567 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.4% (530 of 567 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (450 of 567 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (450 of 567 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (450 of 567 strings)

Translated using Weblate (Malayalam)

Currently translated at 38.9% (221 of 567 strings)

Translated using Weblate (Malayalam)

Currently translated at 38.9% (221 of 567 strings)

Translated using Weblate (Malayalam)

Currently translated at 38.9% (221 of 567 strings)

Translated using Weblate (Macedonian)

Currently translated at 51.1% (290 of 567 strings)

Translated using Weblate (Macedonian)

Currently translated at 51.1% (290 of 567 strings)

Translated using Weblate (Macedonian)

Currently translated at 51.1% (290 of 567 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.6% (565 of 567 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Hindi)

Currently translated at 39.5% (224 of 567 strings)

Translated using Weblate (Hindi)

Currently translated at 39.5% (224 of 567 strings)

Translated using Weblate (Hindi)

Currently translated at 39.5% (224 of 567 strings)

Translated using Weblate (French)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Greek)

Currently translated at 99.8% (566 of 567 strings)

Translated using Weblate (Czech)

Currently translated at 72.6% (412 of 567 strings)

Translated using Weblate (Czech)

Currently translated at 72.6% (412 of 567 strings)

Translated using Weblate (Czech)

Currently translated at 72.6% (412 of 567 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 81.8% (464 of 567 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 81.8% (464 of 567 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 81.8% (464 of 567 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (566 of 567 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 83.4% (473 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 83.2% (472 of 567 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 81.6% (463 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 81.6% (463 of 567 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.6% (565 of 567 strings)

Translated using Weblate (Somali)

Currently translated at 99.6% (565 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (343 of 567 strings)

Translated using Weblate (Vietnamese)

Currently translated at 87.6% (497 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 55.9% (317 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 55.9% (317 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 55.9% (317 of 567 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Russian)

Currently translated at 53.2% (302 of 567 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Turkish)

Currently translated at 97.0% (550 of 567 strings)

Translated using Weblate (Somali)

Currently translated at 69.6% (395 of 567 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (567 of 567 strings)

new string translations

Co-authored-by: Alexey <aleksejfedorov963@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Fikri Akbar <akbarfikri1221@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jonas Kahnwald <elcan.osmanov123@gmail.com>
Co-authored-by: Piotr Z <pzdanowiczp@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sandyran <sandyran@protonmail.com>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Shafici Isxariifshe <mega12xhaphiee@gmail.com>
Co-authored-by: Sina Sharifkazemi <negatics@gmail.com>
Co-authored-by: SleepyOwl <artem726artem@gmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: kaajjo <claymanoff@gmail.com>
Co-authored-by: tuan041 <tuananh163025ttt@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/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bp/
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/fr/
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/kn/
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/nb_NO/
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/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pt/
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/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/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
2023-01-24 19:18:41 +01:00
Blatzar
0b4de81811 (hopefully) Fix home search and OpenSubtitles 2023-01-23 00:29:14 +01:00
reduplicated
60aca3ebdc very nice long hold popups 2023-01-21 23:22:48 +01:00
reduplicated
65fda1889c expandable resume watching go brrr 2023-01-21 20:05:37 +01:00
Hosted Weblate
b2b894caa9 Translated using Weblate (Somali)
Currently translated at 17.9% (102 of 567 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (567 of 567 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/so/
Translation: Cloudstream/App
2023-01-21 13:11:59 +01:00
Cloudburst
c7e2a19f5d
fix build error 2023-01-21 13:04:53 +01:00
Hosted Weblate
9f18cbbc20 Translated using Weblate (Portuguese)
Currently translated at 81.3% (461 of 567 strings)

Translated using Weblate (German)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (French)

Currently translated at 100.0% (567 of 567 strings)

Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: tachyglossues <tachyglossues@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/de/
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/pt/
Translation: Cloudstream/App
2023-01-21 13:04:26 +01:00
recloudstream[bot]
1994edb96c update list of locales 2023-01-21 09:29:07 +00:00
Hosted Weblate
c058409f9d Translated using Weblate (Somali)
Currently translated at 17.9% (102 of 567 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (450 of 567 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (566 of 567 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (567 of 567 strings)

Translated using Weblate (Portuguese)

Currently translated at 16.6% (94 of 566 strings)

Translated using Weblate (Romanian)

Currently translated at 74.9% (424 of 566 strings)

Added translation using Weblate (Portuguese)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Somali)

Currently translated at 16.0% (91 of 566 strings)

Added translation using Weblate (Somali)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.6% (530 of 566 strings)

Translated using Weblate (German)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Turkish)

Currently translated at 96.6% (547 of 566 strings)

Translated using Weblate (Dutch)

Currently translated at 79.5% (450 of 566 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 49.4% (280 of 566 strings)

Translated using Weblate (Bengali)

Currently translated at 40.8% (231 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 40.4% (229 of 566 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 40.2% (228 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 35.5% (201 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 31.0% (176 of 566 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ananas <dawidveimer@outlook.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Dan <denqwerta@gmail.com>
Co-authored-by: Feroli <feroli@tuta.io>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Inside <mihairobu123@gmail.com>
Co-authored-by: Jonas Kahnwald <elcan.osmanov123@gmail.com>
Co-authored-by: Kardi Demha <kardi.demha@gmail.com>
Co-authored-by: Martijn Slob <info@algebrakit.nl>
Co-authored-by: Michael <zuoyfxieglwgurqgic@tmmcv.net>
Co-authored-by: Shafici Isxariifshe <mega12xhaphiee@gmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: SleepyOwl <artem726artem@gmail.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
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/nb_NO/
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/ro/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/so/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translation: Cloudstream/App
2023-01-21 10:28:53 +01:00
Stormunblessed
3ecaf47c9e
fix fastream, tomatomatela, and added okrulink (#320) 2023-01-21 10:25:06 +01:00
Blatzar
b8248d1053 Added Mark as watched and fixed clicking episode synopsis 2023-01-20 23:26:46 +01:00
Blatzar
89c5cb8a46 Added better focus on dialogs on TV & fixed stream button on emulator layout 2023-01-20 01:16:05 +01:00
Hexated
9fd2e84c7a
fixed Linkbox (#319) 2023-01-20 00:44:26 +01:00
Hosted Weblate
8e928a8a2b Translated using Weblate (Spanish)
Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 49.4% (280 of 566 strings)

Translated using Weblate (Bengali)

Currently translated at 40.8% (231 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 40.4% (229 of 566 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.8% (565 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 40.2% (228 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 35.5% (201 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 31.0% (176 of 566 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kardi Demha <kardi.demha@gmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: SleepyOwl <artem726artem@gmail.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/uk/
Translation: Cloudstream/App
2023-01-18 19:55:15 +01:00
Blatzar
49d672718d Fix titles not showing in some cases 2023-01-18 19:03:50 +01:00
Blatzar
a8352d3f64 Prettified the subtitle year dialog 2023-01-17 16:57:46 +01:00
Jace
42f90a79c4
[Feature] Allow input of Year on Subtitle search (#232)
* Use query text as default filename of subtitle instead of empty string.

* [Feature] Allow input of Year on Subtitle search
2023-01-17 15:11:49 +00:00
Blatzar
cd8c5966e6 Made player_view unfocusable to prevent white overlay on Chromebooks 2023-01-16 23:49:59 +01:00
Cloudburst
307d4dd494
Update SettingsGeneral.kt 2023-01-15 12:07:54 +01:00
recloudstream[bot]
d606f84545 update list of locales 2023-01-15 11:05:23 +00:00
Cloudburst
60c1eb2579
Update update_locales.yml 2023-01-15 12:04:57 +01:00
hexated
5c8a667e9e fixed StreamSB 2023-01-15 11:58:41 +01:00
Hosted Weblate
2674d370a2 Translated using Weblate (Italian)
Currently translated at 100.0% (566 of 566 strings)

Update translation files

Updated by "Remove blank strings" hook in Weblate.

Translated using Weblate (Norwegian Nynorsk)

Currently translated at 46.8% (265 of 566 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.2% (528 of 566 strings)

Added translation using Weblate (Norwegian Nynorsk)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sandyran <sandyran@protonmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nn/
Translation: Cloudstream/App
2023-01-15 11:58:25 +01:00
recloudstream[bot]
868bb8500f update list of locales 2023-01-14 12:08:00 +00:00
Hosted Weblate
a87bbd3cfc Translated using Weblate (Ukrainian)
Currently translated at 83.0% (470 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 28.9% (164 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 21.3% (121 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 20.8% (118 of 566 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.4% (563 of 566 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 62.0% (351 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 61.1% (346 of 566 strings)

Translated using Weblate (Ukrainian)

Currently translated at 37.1% (210 of 566 strings)

Translated using Weblate (Russian)

Currently translated at 19.4% (110 of 566 strings)

Translated using Weblate (Turkish)

Currently translated at 92.4% (523 of 566 strings)

Added translation using Weblate (Ukrainian)

Translated using Weblate (Hungarian)

Currently translated at 58.8% (333 of 566 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (French)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (566 of 566 strings)

Translated using Weblate (French)

Currently translated at 100.0% (565 of 565 strings)

Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Anna Teplaya <na-nebesax-anna@yandex.ru>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jonas Kahnwald <elcan.osmanov123@gmail.com>
Co-authored-by: Nepx <anandabaskara@outlook.com>
Co-authored-by: Radoslav Vasilev Vasilev <fifata@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Skrripy <rozihrash.ya6w7@simplelogin.com>
Co-authored-by: Valere <weblare@hostux.net>
Co-authored-by: d4f5409d <d4f5409d-4b6a-4640-9ff3-155ebcdc7ab7@anonaddy.me>
Co-authored-by: eightyy8 <oliver.kha@pm.me>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: h <tachyglossues@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
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/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
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/zh_Hans/
Translation: Cloudstream/App
2023-01-14 13:07:39 +01:00
Horis
92d03fc163
Add Dailymotion Extractor (#312) 2023-01-14 13:07:21 +01:00
Blatzar
06c2cf86ec Various Android TV homepage fixes 2023-01-09 02:15:06 +01:00
Blatzar
0ebc12e29b Merge remote-tracking branch 'origin/master' 2023-01-07 14:57:50 +01:00
Blatzar
308affb6aa Fixed loading disabled plugins on download 2023-01-07 14:56:52 +01:00
Cloudburst
75cc4f6dfa
trigger rerun ci 2023-01-06 22:28:16 +01:00
Cloudburst
61ab957e35
make the update_locales not use github actions token
gh actions token supposedly makes it so the next action doesnt trigger on push
2023-01-06 22:27:23 +01:00
GitHub Actions
36e780f7c9 update list of locales 2023-01-06 16:56:31 +00:00
Hosted Weblate
e7d37aa07c Translated using Weblate (German)
Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Hungarian)

Currently translated at 56.6% (320 of 565 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (565 of 565 strings)

Translated using Weblate (Esperanto)

Currently translated at 28.9% (163 of 563 strings)

Added translation using Weblate (Esperanto)

Translated using Weblate (Hungarian)

Currently translated at 37.4% (211 of 563 strings)

Translated using Weblate (Hebrew)

Currently translated at 37.8% (213 of 563 strings)

Translated using Weblate (Hungarian)

Currently translated at 21.3% (120 of 563 strings)

Translated using Weblate (Persian)

Currently translated at 18.4% (104 of 563 strings)

Translated using Weblate (Russian)

Currently translated at 19.0% (107 of 563 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Swedish)

Currently translated at 79.2% (446 of 563 strings)

Translated using Weblate (Persian)

Currently translated at 17.0% (96 of 563 strings)

Translated using Weblate (Russian)

Currently translated at 17.7% (100 of 563 strings)

Translated using Weblate (Russian)

Currently translated at 17.7% (100 of 563 strings)

Translated using Weblate (Swedish)

Currently translated at 79.0% (445 of 563 strings)

Added translation using Weblate (Persian)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: LagradOst <blatzar@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Márkó <gost1336@gmail.com>
Co-authored-by: Piotr Strebski <strebski@gmail.com>
Co-authored-by: Pmmmp frd <Pmmmpfrd@hi2.in>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Co-authored-by: Vandam <goatli@danwin1210.de>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: inkarnation <94744834+inkarnation@users.noreply.github.com>
Co-authored-by: phlostically <phlostically@mailinator.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
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/es/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fa/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/
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/it/
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/sv/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2023-01-06 17:56:15 +01:00
reduplicated
c57fce2abc save state of popup menu 2023-01-06 17:51:34 +01:00
Blatzar
657971d008 Added delayed updating on A13 to make it less jarring 2023-01-03 20:56:03 +01:00
Cloudburst
0afb6b62aa
Update update_locales.yml 2023-01-02 12:44:23 +01:00
GitHub Actions
e362795493 chore: update list of locales 2023-01-02 11:43:28 +00:00
Hosted Weblate
8712f08bb1 Translated using Weblate (Bengali)
Currently translated at 39.9% (225 of 563 strings)

Translated using Weblate (Hungarian)

Currently translated at 17.7% (100 of 563 strings)

Translated using Weblate (Bengali)

Currently translated at 39.7% (224 of 563 strings)

Translated using Weblate (Turkish)

Currently translated at 88.9% (501 of 563 strings)

Translated using Weblate (Dutch)

Currently translated at 78.8% (444 of 563 strings)

Translated using Weblate (Hungarian)

Currently translated at 17.5% (99 of 563 strings)

Translated using Weblate (Hungarian)

Currently translated at 17.5% (99 of 563 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (563 of 563 strings)

Added translation using Weblate (Hungarian)

Translated using Weblate (Russian)

Currently translated at 17.5% (99 of 563 strings)

Translated using Weblate (Russian)

Currently translated at 15.6% (88 of 563 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Eff ji <Paper.Pepperoni@protonmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: g0szt <gost1336@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hu/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ru/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
Translation: Cloudstream/App
2023-01-02 12:43:15 +01:00
Cloudburst
591ac137f9
Add ebd.cda.pl and automatically set legacy for miui (#296)
Co-authored-by: codefactor-io <support@codefactor.io>
2023-01-02 12:42:58 +01:00
Hosted Weblate
dee269ce5e Translated using Weblate (German)
Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.4% (560 of 563 strings)

Translated using Weblate (Hindi)

Currently translated at 39.7% (224 of 563 strings)

Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: sonacore <sonacore@gmail.com>
Co-authored-by: translate <boledo7225@khaxan.com>
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/id/
Translation: Cloudstream/App
2022-12-29 11:24:06 +01:00
Horis
4926c91f6c
Fix .json file cant select on restore. (#292) 2022-12-29 11:19:08 +01:00
LagradOst
2b43342854 Merge branch 'master' of https://github.com/recloudstream/cloudstream
# Conflicts:
#	app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt
2022-12-29 03:11:08 +01:00
LagradOst
6e61fe5f3e mini fix 2022-12-29 03:08:08 +01:00
Cloudburst
1e8277b087
[skip ci] Apply fixes from CodeFactor (#289)
Co-authored-by: codefactor-io <support@codefactor.io>
2022-12-28 18:41:50 +01:00
LagradOst
710885a3b7 none fix 2022-12-28 15:25:22 +01:00
LagradOst
fbb7046390 Merge branch 'master' of https://github.com/recloudstream/cloudstream 2022-12-28 13:29:30 +01:00
LagradOst
714062c6d4 removed bloat 2022-12-28 13:29:23 +01:00
Cloudburst
83132f183a
fix translations 2022-12-28 13:21:46 +01:00
Hosted Weblate
79c8b4e523 Translated using Weblate (German)
Currently translated at 94.6% (533 of 563 strings)

Translated using Weblate (German)

Currently translated at 94.1% (530 of 563 strings)

Translated using Weblate (German)

Currently translated at 94.1% (530 of 563 strings)

Translated using Weblate (German)

Currently translated at 15.4% (87 of 563 strings)

Added translation using Weblate (German)

Translated using Weblate (Spanish)

Currently translated at 90.4% (509 of 563 strings)

Translated using Weblate (Spanish)

Currently translated at 90.4% (509 of 563 strings)

Translated using Weblate (Spanish)

Currently translated at 71.9% (405 of 563 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.2% (536 of 563 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Julian <hello@apollo.moe>
Co-authored-by: Sonaji Yusup <sonacore@gmail.com>
Co-authored-by: TubaApollo <86665265+TubaApollo@users.noreply.github.com>
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/id/
Translation: Cloudstream/App
2022-12-28 13:20:54 +01:00
codefactor-io
c6749bf988 [CodeFactor] Apply fixes to commit 4440096 2022-12-28 13:19:13 +01:00
LagradOst
7019631146 Merge branch 'master' of https://github.com/recloudstream/cloudstream 2022-12-28 13:10:54 +01:00
LagradOst
4440096ea4 fixed scroll 2022-12-28 13:09:00 +01:00
LagradOst
2a32f62fe3 tv UI change + homepage optimization 2022-12-28 12:51:55 +01:00
Cloudburst
7982f8c491
localization fixes 2022-12-28 11:52:56 +01:00
GitHub Actions
d6af1e4ab6 chore: update list of locales 2022-12-28 10:48:52 +00:00
Hosted Weblate
53b06612c1 Translated using Weblate (Spanish)
Currently translated at 68.0% (383 of 563 strings)

Translated using Weblate (Urdu)

Currently translated at 76.9% (433 of 563 strings)

Translated using Weblate (Spanish)

Currently translated at 18.8% (106 of 563 strings)

Added translation using Weblate (Spanish)

Added translation using Weblate (Russian)

Translated using Weblate (Urdu)

Currently translated at 53.4% (301 of 563 strings)

Translated using Weblate (Urdu)

Currently translated at 46.8% (264 of 563 strings)

Translated using Weblate (Tamil)

Currently translated at 19.0% (107 of 563 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Hindi)

Currently translated at 39.6% (223 of 563 strings)

Added translation using Weblate (Kannada)

Added translation using Weblate (Urdu)

Added translation using Weblate (Tamil)

Translated using Weblate (Hebrew)

Currently translated at 35.8% (202 of 563 strings)

Translated using Weblate (Bengali)

Currently translated at 34.9% (197 of 563 strings)

Translated using Weblate (Vietnamese)

Currently translated at 84.9% (478 of 563 strings)

Translated using Weblate (Turkish)

Currently translated at 87.9% (495 of 563 strings)

Translated using Weblate (Tagalog)

Currently translated at 59.6% (336 of 563 strings)

Translated using Weblate (Swedish)

Currently translated at 64.1% (361 of 563 strings)

Translated using Weblate (Romanian)

Currently translated at 74.6% (420 of 563 strings)

Translated using Weblate (Dutch)

Currently translated at 78.6% (443 of 563 strings)

Translated using Weblate (Malayalam)

Currently translated at 39.2% (221 of 563 strings)

Translated using Weblate (Macedonian)

Currently translated at 51.5% (290 of 563 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.1% (502 of 563 strings)

Translated using Weblate (Hindi)

Currently translated at 39.0% (220 of 563 strings)

Translated using Weblate (Czech)

Currently translated at 73.1% (412 of 563 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.1% (502 of 563 strings)

Translated using Weblate (Swedish)

Currently translated at 64.1% (361 of 563 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.3% (441 of 563 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (563 of 563 strings)

Co-authored-by: Abinanthankv <abinanthankv@protonmail.com>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: John Smith <r9hwlp66@anonaddy.me>
Co-authored-by: Kolon Brin <brinkolon@gmail.com>
Co-authored-by: LagradOst <blatzar@gmail.com>
Co-authored-by: Muhammad Fahad Khan <itxmfahadkhan@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sonaji Yusup <sonacore@gmail.com>
Co-authored-by: Tempo <lafemot433@octovie.com>
Co-authored-by: Walter H <walter75@gmail.com>
Co-authored-by: jhihyu lin <thomas.jy.lin@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
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/es/
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/id/
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/nl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ro/
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/tl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tr/
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_Hant/
Translation: Cloudstream/App
2022-12-28 11:48:39 +01:00
Hosted Weblate
9fc5c5352e Translated using Weblate (Croatian)
Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Croatian)

Currently translated at 97.8% (551 of 563 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.6% (527 of 563 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Bulgarian)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (563 of 563 strings)

Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Anarchydr <patrikpik879@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Radoslav Vasilev Vasilev <fifata@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/hr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/nb_NO/
Translation: Cloudstream/App
2022-12-26 20:55:50 +01:00
Horis
5f1e790163
Fix backup on A13 (#280) 2022-12-26 19:55:23 +00:00
Cloudburst
0073ad8c81
fix zh-TW 2022-12-25 19:56:59 +01:00
Cloudburst
6db688e0bf
only run on master 2022-12-25 19:52:30 +01:00
GitHub Actions
c11bab4a51 chore: update list of locales 2022-12-25 19:50:50 +01:00
Hosted Weblate
e71b70b6a0 Translated using Weblate (Swedish)
Currently translated at 58.0% (327 of 563 strings)

Translated using Weblate (Hebrew)

Currently translated at 35.5% (200 of 563 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (French)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Hebrew)

Currently translated at 29.8% (168 of 563 strings)

Translated using Weblate (Swedish)

Currently translated at 56.1% (316 of 563 strings)

Added translation using Weblate (Hebrew)

Translated using Weblate (Greek)

Currently translated at 100.0% (563 of 563 strings)

Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: LagradOst <blatzar@gmail.com>
Co-authored-by: The Initiator <eithansten@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/he/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/sv/
Translation: Cloudstream/App
2022-12-25 19:50:50 +01:00
GitHub Actions
7cf9c640b8 chore: update list of locales 2022-12-25 18:50:12 +00:00
Cloudburst
9c956f68f9
make CI add langs from weblate 2022-12-25 19:49:59 +01:00
LagradOst
2ba78eb37e
Revert "Update AndroidManifest.xml (#273)" (#275)
This reverts commit 871dcf7171.
2022-12-24 17:11:52 +01:00
Hosted Weblate
9e059af0bb Translated using Weblate (Greek)
Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (563 of 563 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (562 of 562 strings)

Co-authored-by: Alex Georgiou <alexandrosgeorgiou35@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
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/zh_Hans/
Translation: Cloudstream/App
2022-12-24 16:37:42 +01:00
krrishkap
871dcf7171
Update AndroidManifest.xml (#273)
Enabling Api for predictive back gesture for A13 .
2022-12-24 16:05:20 +01:00
Blatzar
4f4061961a Merge remote-tracking branch 'origin/master' 2022-12-23 22:54:09 +01:00
Blatzar
5b26c998b4 Added trailers and poster to TV 2022-12-23 22:53:51 +01:00
Hosted Weblate
5953420774 Translated using Weblate (Polish)
Currently translated at 100.0% (562 of 562 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translation: Cloudstream/App
2022-12-23 20:54:43 +01:00
Cloudburst
f3e7a5daa6
Add categories to settings pages 2022-12-23 20:50:05 +01:00
Blatzar
b6b7cceea5 Changed SeekParameters to allow for faster seeking + fixed apk installer text 2022-12-23 17:33:54 +01:00
Deepak Patil
23973042f4
feat(utils): add Jshunter deobfuscator (#250)
* feat(utils): add Jshunter deobfuscator

Co-authored-by: contusionglory <102427829+contusionglory@users.noreply.github.com>
2022-12-23 12:55:27 +01:00
Hosted Weblate
a2dbabdb6e Translated using Weblate (Polish)
Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (French)

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.8% (536 of 548 strings)

Translated using Weblate (French)

Currently translated at 87.7% (481 of 548 strings)

Translated using Weblate (Czech)

Currently translated at 75.1% (412 of 548 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (548 of 548 strings)

Co-authored-by: Bitpaint <bitpaintclub@gmail.com>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sarlay <raphmd0@gmail.com>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Václav Ornst <mplay.octopus@gmail.com>
Co-authored-by: jhihyu lin <thomas.jy.lin@gmail.com>
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/pl/
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
2022-12-23 12:48:15 +01:00
Blatzar
53519381d7 Set default start season to 1 2022-12-22 13:11:37 +01:00
Hosted Weblate
a522ef0edb Translated using Weblate (Polish)
Currently translated at 98.7% (541 of 548 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (548 of 548 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 93.5% (508 of 543 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (538 of 543 strings)

Translated using Weblate (French)

Currently translated at 79.7% (433 of 543 strings)

Translated using Weblate (French)

Currently translated at 79.7% (433 of 543 strings)

Translated using Weblate (Arabic)

Currently translated at 99.4% (540 of 543 strings)

Translated using Weblate (English)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 87.8% (477 of 543 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 86.9% (472 of 543 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sarlay <raphmd0@gmail.com>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/pl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2022-12-21 18:45:30 +01:00
Blatzar
d727099c29 add apk installer options for memeUI 2022-12-21 15:19:49 +01:00
Hosted Weblate
e2fc946d91 Translated using Weblate (Tagalog)
Currently translated at 62.2% (338 of 543 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.3% (534 of 543 strings)

Translated using Weblate (Tagalog)

Currently translated at 53.7% (292 of 543 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 86.9% (472 of 543 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (English)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 84.5% (459 of 543 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (French)

Currently translated at 72.9% (396 of 543 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (543 of 543 strings)

Translated using Weblate (English)

Currently translated at 100.0% (543 of 543 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jace Orwell <jaceorwell@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: Sdarfeesh <Sdarfeesh@protonmail.com>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/en/
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/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/tl/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hans/
Translation: Cloudstream/App
2022-12-20 10:11:57 +01:00
Hosted Weblate
a1f5786f02 Translated using Weblate (Polish)
Currently translated at 95.0% (516 of 543 strings)

Translated using Weblate (English)

Currently translated at 100.0% (542 of 542 strings)

Translated using Weblate (Bengali)

Currently translated at 36.1% (196 of 542 strings)

Translated using Weblate (Vietnamese)

Currently translated at 90.5% (491 of 542 strings)

Translated using Weblate (Turkish)

Currently translated at 93.9% (509 of 542 strings)

Translated using Weblate (Tagalog)

Currently translated at 52.9% (287 of 542 strings)

Translated using Weblate (Romanian)

Currently translated at 78.9% (428 of 542 strings)

Translated using Weblate (Polish)

Currently translated at 94.6% (513 of 542 strings)

Translated using Weblate (Polish)

Currently translated at 94.6% (513 of 542 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 83.5% (453 of 542 strings)

Translated using Weblate (Dutch)

Currently translated at 83.2% (451 of 542 strings)

Translated using Weblate (Italian)

Currently translated at 99.2% (538 of 542 strings)

Translated using Weblate (Czech)

Currently translated at 77.4% (420 of 542 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 87.6% (475 of 542 strings)

Translated using Weblate (Bulgarian)

Currently translated at 98.8% (536 of 542 strings)

Translated using Weblate (Bulgarian)

Currently translated at 98.8% (536 of 542 strings)

Translated using Weblate (Arabic)

Currently translated at 98.8% (536 of 542 strings)

Translated using Weblate (Arabic)

Currently translated at 98.8% (536 of 542 strings)

Translated using Weblate (English)

Currently translated at 100.0% (542 of 542 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jace Orwell <jaceorwell@gmail.com>
Co-authored-by: Radoslav Vasilev Vasilev <fifata@gmail.com>
Co-authored-by: Rex_sa <rex.sa@pm.me>
Co-authored-by: duckling <salmanfc.bd@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/ar/
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/bp/
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/it/
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/ro/
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/vi/
Translation: Cloudstream/App
2022-12-19 10:13:47 +01:00
Cloudburst
363906cf3b add summary to automatic_plugin_download 2022-12-19 10:10:29 +01:00
Hosted Weblate
50fc8d0ffb Translated using Weblate (Bengali)
Currently translated at 25.0% (136 of 542 strings)

Translated using Weblate (Bengali)

Currently translated at 25.0% (136 of 542 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (542 of 542 strings)

Translated using Weblate (Greek)

Currently translated at 82.4% (447 of 542 strings)

Translated using Weblate (Bengali)

Currently translated at 19.9% (108 of 542 strings)

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Thanasis Trispiotis <sdi2000194@di.uoa.gr>
Co-authored-by: Translator-3000 <weblate.m1d0h@8shield.net>
Co-authored-by: duckling <salmanfc.bd@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bn/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/el/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/it/
Translation: Cloudstream/App
2022-12-18 16:50:18 +01:00
Cloudburst
1e636c8b08
add bengali 2022-12-18 14:31:44 +01:00
Hosted Weblate
492c950b7a Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.4% (371 of 542 strings)

Translated using Weblate (English)

Currently translated at 100.0% (542 of 542 strings)

Added translation using Weblate (Bengali)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.9% (531 of 542 strings)

Translated using Weblate (Vietnamese)

Currently translated at 92.4% (501 of 542 strings)

Translated using Weblate (Polish)

Currently translated at 95.9% (520 of 542 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 54.6% (296 of 542 strings)

Translated using Weblate (Dutch)

Currently translated at 84.5% (458 of 542 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.1% (413 of 542 strings)

Translated using Weblate (French)

Currently translated at 70.4% (382 of 542 strings)

Translated using Weblate (Greek)

Currently translated at 81.5% (442 of 542 strings)

Translated using Weblate (Czech)

Currently translated at 78.4% (425 of 542 strings)

Translated using Weblate (bp (generated) (bp))

Currently translated at 89.2% (484 of 542 strings)

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (541 of 542 strings)

Translated using Weblate (English)

Currently translated at 100.0% (542 of 542 strings)

Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bg/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/bp/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/cs/
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/fr/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/id/
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/vi/
Translate-URL: https://hosted.weblate.org/projects/cloudstream/app/zh_Hant/
Translation: Cloudstream/App
2022-12-18 14:30:38 +01:00
Weblate (bot)
4d13494a93
Translations update from Hosted Weblate (#254)
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: sfaresee <salmanfc.bd@gmail.com>
Co-authored-by: Radoslav Vasilev Vasilev <fifata@gmail.com>
2022-12-18 13:26:42 +01:00
Cloudburst
956c693d1b
[skip ci] Update README.md 2022-12-18 12:59:04 +01:00
contusionglory
6246d984a1
Fix uprot.net (#251)
Fix regex for upront.net it was giving the wrong url.
2022-12-16 23:45:23 +00:00
Blatzar
495d02d583 Merge remote-tracking branch 'origin/master' 2022-12-15 21:00:17 +01:00
Blatzar
304b103e32 Removed theme from some dialogs as the button text became invisible 2022-12-15 21:00:09 +01:00
Sdarfeesh
5af1a0e433
Update Simplified Chinese (#243) 2022-12-14 22:10:44 +00:00
Blatzar
3fdf41869e Added hot reloading for plugins when using deployWithAdb 2022-12-13 23:28:31 +01:00
Hexated
7362ac9f64
fixed StreamSB (#244) 2022-12-13 15:20:43 +00:00
Blatzar
751175b3f9 Updated the in app updater to include notifications and updates without user action on Android 12+ 2022-12-11 18:14:09 +01:00
reduplicated
c11f0c101b small trailer fix 2022-12-09 20:10:10 +01:00
Sdarfeesh
7c4f177e47
Update Simplified Chinese (Correction) (#240) 2022-12-09 17:35:16 +00:00
Blatzar
0d7c20e3bd Merge remote-tracking branch 'origin/master' 2022-12-09 18:20:19 +01:00
Blatzar
95f4a15864 + Added Confirm exit on Android TV
+ Added support for Play Next on Android TV
2022-12-09 18:20:10 +01:00
reduplicated
20ac21c25f small UI change 2022-12-09 18:12:51 +01:00
Blatzar
4f54bf3ae4 Added outlines to more things to increase accessibility on Android TV 2022-12-09 00:37:59 +01:00
Blatzar
f7b623ffc7 Merge remote-tracking branch 'origin/master' 2022-12-08 16:18:14 +01:00
Blatzar
4b0b6f6f20 Reworked the internal subtitle API to fix edge-cases when importing subtitles 2022-12-08 16:18:03 +01:00
Deepak Patil
0b17862049
feat(subtitle): add addic7ed.com (#221) 2022-12-07 19:10:19 +00:00
Jace
56c79e3b6a
[Bugfix] Re arrange Regex for Duration, fixing issue if inaccurate results. (#239) 2022-12-07 18:39:59 +00:00
Blatzar
6d13cf0b01 Fixed Android 13 notifications (bruh) 2022-12-06 19:41:09 +01:00
Blatzar
70dcc96026 Fixed AniList api 2022-12-06 11:52:58 +01:00
Blatzar
3fa82cdba7 Probably fixed a very weird exception 2022-12-05 16:09:51 +01:00
Blatzar
e7d7639776 Merge remote-tracking branch 'origin/master' 2022-12-05 12:39:44 +01:00
Blatzar
514e250d68 Update dependencies, lets hope it does not fuck up anything 2022-12-05 12:39:34 +01:00
nvticzek
5c3652d1e9
Polish translation update (#235) 2022-12-04 16:39:04 +01:00
reduplicated
2222a1b07b fixed rolling cache + update UI text color 2022-12-03 22:55:53 +01:00
Jace
42d1dd9f7d
[Feature] Use subtitle filename, instead of movie title for Opensubtitles search. (#223)
* [Feature] Use subtitle filename, instead of movie title for Opensubtitles search.

* Optimize iteration for first not null value
2022-12-03 14:48:56 +00:00
SANCTI-afk
eb90b79bf9
Arabic language update for latest keys (#231)
* arabicLanguage100%

* Update strings.xml

* Arabic Full

* translated(preffVplayerBtn)

* renamed homeBtn for arabic layout

* Arabic language update 

No more typical ×
Common language instead ✓

* Arabic translation minor update

Last commit ready to be merged

* arabic language update for latest keys

* ready to merge

* last minor edit and ready to merge
2022-12-03 13:17:33 +00:00
Blatzar
b79e2d768f Fixed subtitle elevation again 2022-12-03 02:42:16 +01:00
Sdarfeesh
e215747749
Update Simplified Chinese Translation (#229) 2022-12-01 21:07:47 +00:00
Blatzar
3f658a375e Fixed MPV return intent 2022-11-30 21:23:19 +01:00
Jace
723c554bc8
[Feature] Automatically download plugin, based on language setting (#172) 2022-11-29 19:46:31 +00:00
jhih_yu
58593ac8da
Add zh_TW (#202)
* Add zh_TW
2022-11-29 19:45:00 +00:00
Cloudburst
c513708d74
smol tweaks to the previous commit 2022-11-27 10:37:36 +01:00
Jace
9be50eb28b [Feature] Filter extension list automatically by preferred media language. 2022-11-27 10:10:02 +01:00
Blatzar
789f3db554 Merge remote-tracking branch 'origin/master' 2022-11-23 15:58:20 +01:00
Blatzar
e21c8f8038 Fixed DdosGuardKiller, SSL on android 9 and some OpenSubtitles fixes 2022-11-23 15:57:56 +01:00
Jace
9bca7a0780
Fix duration regex, returning null on first checker (#218) 2022-11-21 07:32:32 +00:00
Jace
a8f3d18c2e
[Feature] Get duration from string in format of '00 hr 00 min 00 sec', in any combination (#215) 2022-11-19 10:53:34 +00:00
Blatzar
263f74fb9c Removed a million duplicates in values-fr to make it compile 2022-11-18 20:24:24 +01:00
Cloudburst
dbd91d788c
?? 2022-11-18 18:11:25 +01:00
MXC48
c9fe7c79dc
update the strings.xml in french (#211)
* update the strings.xml in french

* fix build error

* Update and rename app/src/main/res/values-fr/strings.xml to application/src/principal/res/valeurs-fr/strings.xml

Removal of "sort_copy" "sort_close" "sort_clear" "sort_save" in duplicate

Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
2022-11-18 14:11:47 +00:00
Blatzar
924d797e07 Fix next episode button showing up erroneously 2022-11-15 16:32:31 +01:00
Blatzar
2b29e8078f Added intent to start searching 2022-11-13 01:40:49 +01:00
Blatzar
9a93b375f3 Fixed:
Lock when switching episodes
Lock on RTL layouts
Skipping to the next season
2022-11-12 22:29:22 +01:00
Blatzar
30316107c8 Merge remote-tracking branch 'origin/master' 2022-11-12 20:07:17 +01:00
Blatzar
cf22ada266 Fix VoeExtractor 2022-11-12 20:07:08 +01:00
Kylianalex
456cd2e6e2
Updated French translation (#207)
Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com>
2022-11-11 16:22:35 +01:00
TubaApollo
81adb10c1f
german translation update (#210) 2022-11-11 16:22:26 +01:00
SANCTI-afk
639de891c6
Arabic language minor update (#204) 2022-11-11 16:22:13 +01:00
Davide
2e7823034b
Updated Italian translation (#206) 2022-11-11 16:21:56 +01:00
Sdarfeesh
e95d117ebc
Update Simplified Chinese Translation (#199) 2022-11-07 10:49:53 +00:00
Blatzar
1226426389 More crash fixes 2022-11-06 20:16:48 +01:00
darkdemon
aef6f93efe - Open all extractor classes
- Add StreamTape clones[Streamtape.net, ShaveTape.cash]
- optimize imports for AstreamHub, GMplayer
2022-11-06 12:31:33 +01:00
reduplicated
3e2c53a5b7 Merge remote-tracking branch 'origin/master' 2022-11-05 22:15:50 +01:00
reduplicated
8fa00f4ca9 added setting for skip op and changed key for player seek amount 2022-11-05 22:15:34 +01:00
Sdarfeesh
4fb65e7242
Update Simplified Chinese Translation (#198) 2022-11-05 20:55:41 +00:00
Blatzar
60bcbf0060 A lil cache fixing & search fixes 2022-11-05 19:54:04 +01:00
reduplicated
60a2f7c1c5 clear history 2022-11-05 01:27:35 +01:00
reduplicated
9e67e856a0 small fix for multi request 2022-11-05 00:40:31 +01:00
Osten
c10ec34ab8
Aniskip (#195)
* aniskip groundwork

* working

* removed prints

* bump nicehttp

* small fix

* small fixes

* bump

Co-authored-by: reduplicated <110570621+reduplicated@users.noreply.github.com>
2022-11-04 23:36:27 +00:00
Blatzar
f84259f898 Merge remote-tracking branch 'origin/master' 2022-11-03 11:56:59 +01:00
Blatzar
8810d5abd6 Trying to the dumbest crash ever 2022-11-03 11:56:45 +01:00
reduplicated
bc03f6ebb5 version bump 2022-11-02 23:59:14 +01:00
Blatzar
344f974af2 FIX CRASHING! 2022-11-02 23:55:41 +01:00
reduplicated
c09b6881e5 bump 2022-11-02 22:56:03 +01:00
reduplicated
4a193d5d27 small fix 2022-11-02 21:41:39 +01:00
Blatzar
b57a7c3772 Merge remote-tracking branch 'origin/master' 2022-11-01 23:29:41 +01:00
Blatzar
e5be703a47 Small code fix 2022-11-01 23:29:32 +01:00
Blatzar
6308fd0fec Fixed subtitles on new exoplayer version 2022-11-01 23:27:42 +01:00
Blatzar
3d3c85a1ad Revert "Revert "Bumped the exoplayer version and fixed the audio & video track selection""
This reverts commit 6b586388b9.
2022-11-01 23:16:56 +01:00
reduplicated
28b4456dfd resultpage UI update and readded trailers 2022-11-01 18:01:29 +01:00
reduplicated
f268418190 fixed homepage 2022-11-01 00:29:10 +01:00
Blatzar
1c494f0ce2 Add back provider languages option (settings > providers)
If any of your favorite sites disappear check that setting
2022-10-31 22:09:56 +01:00
Blatzar
ddae2ddf3c Revert "Remove provider language (#141)"
This reverts commit a43e950a48.
2022-10-31 20:55:33 +01:00
Hexated
64303eab8d
[extractor] added Jeniusplay (#183) 2022-10-31 19:47:15 +00:00
Blatzar
a201f5e4f8 Add a few more animations to homepage 2022-10-31 16:46:02 +01:00
Cloudburst
0e8aacf989
[skip ci] add builds archive 2022-10-31 14:23:11 +01:00
Cloudburst
e72f3ff8b9
oops forgor \ this time 2022-10-31 12:49:00 +01:00
Cloudburst
8406f6de65 i forgor the ? 2022-10-31 12:37:06 +01:00
Cloudburst
7272dc67b7 fix chip tint and make adding repos by url easier 2022-10-31 12:33:09 +01:00
reduplicated
d349190238 scroll 2022-10-31 01:16:15 +01:00
reduplicated
47b79550f1 color fixes 2022-10-30 23:15:43 +01:00
Blatzar
65b5efb848 Make searching always respect your preferred media setting 2022-10-28 22:29:32 +02:00
Blatzar
fd7cf51f57 Fix status bar padding again 2022-10-28 21:49:14 +02:00
Blatzar
617fc4a295 Fix status bar padding on None 2022-10-28 21:43:56 +02:00
Blatzar
9ee0653ecf Fixed critical TV crash and made the bookmarks single select 2022-10-28 21:14:57 +02:00
reduplicated
c18856c8c3 minor UI and icon changes + tmp readded search 2022-10-28 18:43:14 +02:00
reduplicated
47da6efb59 Merge remote-tracking branch 'origin/master' 2022-10-28 03:51:41 +02:00
reduplicated
997420a942 phone UI changes + cache 2022-10-28 03:51:27 +02:00
Blatzar
6b586388b9 Revert "Bumped the exoplayer version and fixed the audio & video track selection"
This reverts commit 93cbd29f3d.
2022-10-27 18:31:38 +02:00
Blatzar
93cbd29f3d Bumped the exoplayer version and fixed the audio & video track selection 2022-10-27 15:38:53 +02:00
Cloudburst
7e750a40e0
fix da build (again) 2022-10-26 23:09:28 +02:00
Cloudburst
fa6a620bf9 fix builds 2022-10-26 22:39:36 +02:00
Cloudburst
c9c339795a
fix the bulgarian string once again 2022-10-26 22:12:17 +02:00
Cloudburst
49ebd27f80
fix broken strings.xml and migrate to $GITHUB_OUTPUT 2022-10-26 22:06:19 +02:00
J. Fronny
0f625142da
Switch to kotlin build scripts (#158) 2022-10-26 21:56:31 +02:00
Cloudburst
044822040f Translation in Bulgarian language.
Co-authored-by: ardoslav <fifata@gmail.com>
2022-10-26 21:55:59 +02:00
Hexated
ecd363992c
added sub to streamsb & xtreamCdn (#163) 2022-10-26 15:38:46 +00:00
Blatzar
7cbcee4d48 Merge remote-tracking branch 'origin/master' 2022-10-26 14:59:40 +02:00
Blatzar
7f71eef755 Allow playback from buffer with no internet 2022-10-26 14:59:29 +02:00
Cloudburst
4c309bbb2a
[skip ci] increase issue analysis threshold 2022-10-25 10:41:12 +02:00
Blatzar
544f277d0c Fixed player caching being invalidated by OOM 2022-10-24 15:31:35 +02:00
Blatzar
034bad289f Crashfix + version bump 2022-10-23 18:59:01 +02:00
Blatzar
570fdb5af4 Merge remote-tracking branch 'origin/master' 2022-10-19 00:14:26 +02:00
Blatzar
6a5286e363 Several crashfixes 2022-10-19 00:14:15 +02:00
Cloudburst
4c0f6df1a2
update newpipe (#160) 2022-10-18 12:33:25 +02:00
Sdarfeesh
4848e43c97
Simplified Chinese Correction and Update (#150) 2022-10-18 12:33:17 +02:00
Cloudburst
a58ca547d7
Apply fixes from CodeFactor (#157)
Co-authored-by: codefactor-io <support@codefactor.io>
2022-10-16 19:51:00 +02:00
Blatzar
f49d9de09b Added CineGrabber Extractor 2022-10-16 19:42:32 +02:00
Cloudburst
cf08c958eb
[skip ci] remove link to site 2022-10-15 22:01:16 +02:00
Blatzar
e67d248f7f Fix searching in repos & search provider selection bottom sheet 2022-10-14 23:56:21 +02:00
Blatzar
63c713fc68 Changed home selection sheet to fully open and allow upwards scrolling :) 2022-10-14 20:18:32 +02:00
Blatzar
1228701f0e Fix focusing add repo on firestick 2022-10-13 22:58:18 +02:00
reduplicated
661f8c3c4e mini api fix 2022-10-11 15:24:16 +02:00
Blatzar
af4d57e842 Merge remote-tracking branch 'origin/master' 2022-10-10 23:57:59 +02:00
Blatzar
a565319ecb Remove links to website in app & fix plugin deletion bug 2022-10-10 23:57:50 +02:00
SANCTI-afk
fc7e39e3cc
arabicLanguage (#118)
* arabicLanguage100%

* Update strings.xml

* Arabic Full

* translated(preffVplayerBtn)
2022-10-10 19:58:28 +00:00
LagradOst
a43e950a48
Remove provider language (#141) 2022-10-10 19:51:03 +00:00
Hexated
98ef6a3f16
added Vidmoly & Voe (extractor) (#147) 2022-10-10 19:43:56 +00:00
Thanasis Trispiotis
b3ff3ec086
update strings.xml Greek (#145)
* update strings.xml

Add new translations to greek language (el) and fix typos.

* remove duplicate
2022-10-10 19:42:59 +00:00
Samet Mert Karataş
e2118c3271
Update Turkish translation (#143)
* Update strings.xml and array.xml

* Fix escape characters

* Little changes
2022-10-09 00:35:19 +00:00
reduplicated
ddcdb04d78 Merge remote-tracking branch 'origin/master' 2022-10-09 01:36:22 +02:00
reduplicated
61fb302a37 delay mainpage 2022-10-09 01:36:06 +02:00
326 changed files with 24180 additions and 9936 deletions

BIN
.github/downloads.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

BIN
.github/home.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

63
.github/locales.py vendored Normal file
View file

@ -0,0 +1,63 @@
import re
import glob
import requests
import lxml.etree as ET # builtin library doesn't preserve comments
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */"
XML_NAME = "app/src/main/res/values-"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
INDENT = " "*4
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
# Load settings file
src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
before_src, rest = src.split(START_MARKER)
rest, after_src = rest.split(END_MARKER)
# Load already added langs
languages = {}
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
flag, name, iso = lang.groups()
languages[iso] = (flag, name)
# Add not yet added langs
for folder in glob.glob(f"{XML_NAME}*"):
iso = folder[len(XML_NAME):]
if iso not in languages.keys():
entry = iso_map.get(iso.lower(),{'nativeName':iso})
languages[iso] = ("", entry['nativeName'].split(',')[0])
# Create triples
triples = []
for iso in sorted(languages.keys()):
flag, name = languages[iso]
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
# Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
before_src +
START_MARKER +
"\n" +
"\n".join(triples) +
"\n" +
END_MARKER +
after_src
)
# Go through each values.xml file and fix escaped \@string
for file in glob.glob(f"{XML_NAME}*/strings.xml"):
try:
tree = ET.parse(file)
for child in tree.getroot():
if child.text.startswith("\\@string/"):
print(f"[{file}] fixing {child.attrib['name']}")
child.text = child.text.replace("\\@string/", "@string/")
with open(file, 'wb') as fp:
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
except ET.ParseError as ex:
print(f"[{file}] {ex}")

BIN
.github/player.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

BIN
.github/results.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

BIN
.github/search.jpg vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

76
.github/workflows/build_to_archive.yml vendored Normal file
View file

@ -0,0 +1,76 @@
name: Archive build
on:
push:
branches: [ master ]
paths-ignore:
- '*.md'
- '*.json'
- '**/wcokey.txt'
workflow_dispatch:
concurrency:
group: "Archive-build"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- name: Generate access token (archive)
id: generate_archive_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Fetch keystore
id: fetch_keystore
run: |
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
./gradlew assemblePrerelease
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- uses: actions/checkout@v3
with:
repository: "recloudstream/cloudstream-archive"
token: ${{ steps.generate_archive_token.outputs.token }}
path: "archive"
- name: Move build
run: |
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
- name: Push archive
run: |
cd $GITHUB_WORKSPACE/archive
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
git push --force

View file

@ -15,15 +15,28 @@ 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 }}
- name: Similarity analysis - name: Similarity analysis
id: similarity
uses: actions-cool/issues-similarity-analysis@v1 uses: actions-cool/issues-similarity-analysis@v1
with: with:
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
filter-threshold: 0.5 filter-threshold: 0.60
title-excludes: '' title-excludes: ''
comment-title: | comment-title: |
### Your issue looks similar to these issues: ### Your issue looks similar to these issues:
Please close if duplicate. Please close if duplicate.
comment-body: '${index}. ${similarity} #${number}' comment-body: '${index}. ${similarity} #${number}'
- name: Label if possible duplicate
if: steps.similarity.outputs.similar-issues-found =='true'
uses: actions/github-script@v6
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@v2 - uses: actions/checkout@v2
- name: Automatically close issues that dont follow the issue template - name: Automatically close issues that dont follow the issue template
uses: lucasbento/auto-close-issues@v1.0.2 uses: lucasbento/auto-close-issues@v1.0.2
@ -41,7 +54,7 @@ jobs:
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py" wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
pip3 install httpx pip3 install httpx
RES="$(python3 ./check_issue.py)" RES="$(python3 ./check_issue.py)"
echo "::set-output name=name::${RES}" echo "name=${RES}" >> $GITHUB_OUTPUT
- name: Comment if issue mentions a provider - name: Comment if issue mentions a provider
if: steps.provider_check.outputs.name != 'none' if: steps.provider_check.outputs.name != 'none'
uses: actions-cool/issues-helper@v3 uses: actions-cool/issues-helper@v3
@ -53,6 +66,18 @@ jobs:
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). 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 }}` Found provider name: `${{ steps.provider_check.outputs.name }}`
- name: Label if mentions provider
if: steps.provider_check.outputs.name != 'none'
uses: actions/github-script@v6
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 - name: Add eyes reaction to all issues
uses: actions-cool/emoji-helper@v1.0.0 uses: actions-cool/emoji-helper@v1.0.0
with: with:

View file

@ -40,7 +40,7 @@ jobs:
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt" curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)" KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}" echo "::add-mask::${KEY_PWD}"
echo "::set-output name=key_pwd::$KEY_PWD" echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle - name: Run Gradle
run: | run: |
./gradlew assemblePrerelease makeJar androidSourcesJar ./gradlew assemblePrerelease makeJar androidSourcesJar
@ -56,6 +56,6 @@ jobs:
prerelease: true prerelease: true
title: "Pre-release Build" title: "Pre-release Build"
files: | files: |
app/build/outputs/apk/prerelease/*.apk app/build/outputs/apk/prerelease/release/*.apk
app/build/libs/app-sources.jar app/build/libs/app-sources.jar
app/build/classes.jar app/build/classes.jar

View file

@ -15,9 +15,9 @@ jobs:
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Run Gradle - name: Run Gradle
run: ./gradlew assembleDebug run: ./gradlew assemblePrereleaseDebug
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
name: pull-request-build name: pull-request-build
path: "app/build/outputs/apk/debug/*.apk" path: "app/build/outputs/apk/prerelease/debug/*.apk"

42
.github/workflows/update_locales.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Fix locale issues
on:
workflow_dispatch:
push:
paths:
- '**.xml'
branches:
- master
concurrency:
group: "locale"
cancel-in-progress: true
jobs:
create:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream"
- uses: actions/checkout@v2
with:
token: ${{ steps.generate_token.outputs.token }}
- name: Install dependencies
run: |
pip3 install lxml
- name: Edit files
run: |
python3 .github/locales.py
- name: Commit to the repo
run: |
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
git config --local user.name "recloudstream[bot]"
git add .
# "echo" returns true so the build succeeds, even if no changed files
git commit -m 'chore(locales): fix locale issues' || echo
git push

View file

@ -31,5 +31,10 @@
<option name="name" value="maven2" /> <option name="name" value="maven2" />
<option name="url" value="https://jitpack.io" /> <option name="url" value="https://jitpack.io" />
</remote-repository> </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> </component>
</project> </project>

View file

@ -1,45 +1,18 @@
# CloudStream # CloudStream
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.** **⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
You can find the list of community-maintained extension repositories [here
](https://recloudstream.github.io/repos/)
[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM) [![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
***Features:*** ### Features:
+ **AdFree**, No ads whatsoever + **AdFree**, No ads whatsoever
+ No tracking/analytics + No tracking/analytics
+ Bookmarks + Bookmarks
+ Download and stream movies, tv-shows and anime + Download and stream movies, tv-shows and anime
+ Chromecast + Chromecast
***Screenshots:*** ### Supported languages:
<a href="https://hosted.weblate.org/engage/cloudstream/">
<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/results.jpg" height="400"/> <img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
<img src="./.github/player.jpg" height="200"/> </a>
***The list of supported languages:***
* 🇱🇧 Arabic
* 🇭🇷 Croatian
* 🇨🇿 Czech
* 🇳🇱 Dutch
* 🇬🇧 English
* 🇫🇷 French
* 🇩🇪 German
* 🇬🇷 Greek
* 🇮🇳 Hindi
* 🇮🇩 Indonesian
* 🇮🇹 Italian
* 🇲🇰 Macedonian
* 🇮🇳 Malayalam
* 🇳🇴 Norsk
* 🇵🇱 Polish
* 🇧🇷 Portuguese (Brazil)
* 🇷🇴 Romanian
* 🇪🇸 Spanish
* 🇸🇪 Swedish
* 🇵🇭 Tagalog
* 🇹🇷 Turkish
* 🇻🇳 Vietnamese

View file

@ -1,233 +0,0 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-android-extensions'
id 'org.jetbrains.dokka'
}
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
def allFilesFromDir = new File(tmpFilePath).listFiles()
def prereleaseStoreFile = null
if (allFilesFromDir != null) {
prereleaseStoreFile = allFilesFromDir.first()
}
android {
testOptions {
unitTests.returnDefaultValues = true
}
signingConfigs {
prerelease {
if (prereleaseStoreFile != null) {
storeFile = file(prereleaseStoreFile)
storePassword System.getenv("SIGNING_STORE_PASSWORD")
keyAlias System.getenv("SIGNING_KEY_ALIAS")
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
}
}
}
compileSdkVersion 31
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.lagradost.cloudstream3"
minSdkVersion 21
targetSdkVersion 30
versionCode 51
versionName "3.1.5"
resValue "string", "app_version",
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
resValue "string", "commit_hash",
("git rev-parse --short HEAD".execute().text.trim() ?: "")
resValue "bool", "is_prerelease", "false"
buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
kapt {
includeCompileClasspath = true
}
}
buildTypes {
// release {
// debuggable false
// minifyEnabled false
// shrinkResources false
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// resValue "bool", "is_prerelease", "false"
// }
prerelease {
applicationIdSuffix ".prerelease"
buildConfigField("boolean", "BETA", "true")
signingConfig signingConfigs.prerelease
versionNameSuffix '-PRE'
debuggable false
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "bool", "is_prerelease", "true"
}
debug {
debuggable true
applicationIdSuffix ".debug"
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
resValue "bool", "is_prerelease", "true"
}
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
freeCompilerArgs = ['-Xjvm-default=compatibility']
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.google.android.mediahome:video:1.0.0'
implementation 'androidx.test.ext:junit-ktx:1.1.3'
testImplementation 'org.json:json:20180813'
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2' // need target 32 for 1.5.0
// dont change this to 1.6.0 it looks ugly af
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//implementation "io.karn:khttp-android:0.1.2" //okhttp instead
// implementation 'org.jsoup:jsoup:1.13.1'
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation 'com.github.bumptech.glide:glide:4.13.1'
kapt 'com.github.bumptech.glide:compiler:4.13.1'
implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
implementation 'jp.wasabeef:glide-transformations:4.3.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
// implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
// Exoplayer
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
// Bug reports
implementation "ch.acra:acra-core:5.8.4"
implementation "ch.acra:acra-toast:5.8.4"
compileOnly "com.google.auto.service:auto-service-annotations:1.0"
//either for java sources:
annotationProcessor "com.google.auto.service:auto-service:1.0"
//or for kotlin sources (requires kapt gradle plugin):
kapt "com.google.auto.service:auto-service:1.0"
// subtitle color picker
implementation 'com.jaredrummler:colorpicker:1.1.0'
//run JS
implementation 'org.mozilla:rhino:1.7.14'
// TorrentStream
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
// Downloading
implementation "androidx.work:work-runtime:2.7.1"
implementation "androidx.work:work-runtime-ktx:2.7.1"
// Networking
// implementation "com.squareup.okhttp3:okhttp:4.9.2"
// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
implementation 'com.github.Blatzar:NiceHttp:0.3.3'
// Util to skip the URI file fuckery 🙏
implementation "com.github.tachiyomiorg:unifile:17bec43"
// API because cba maintaining it myself
implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
implementation 'com.github.discord:OverlappingPanels:0.1.3'
// debugImplementation because LeakCanary should only run in debug builds.
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
// for shimmer when loading
implementation 'com.facebook.shimmer:shimmer:0.5.0'
implementation "androidx.tvprovider:tvprovider:1.0.0"
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
// slow af yt
//implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
// newpipe yt
implementation 'com.github.recloudstream:NewPipeExtractor:master-SNAPSHOT'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// Library/extensions searching with Levenshtein distance
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
}
task androidSourcesJar(type: Jar) {
getArchiveClassifier().set('sources')
from android.sourceSets.main.java.srcDirs//full sources
}
// this is used by the gradlew plugin
task makeJar(type: Copy) {
from('build/intermediates/compile_app_classes_jar/debug')
into('build')
include('classes.jar')
dependsOn('build')
}
dokkaHtml {
moduleName.set("Cloudstream")
dokkaSourceSets {
main {
sourceLink {
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
localDirectory.set(file("src/main/java"))
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(new URL(
"https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
}
}
}

258
app/build.gradle.kts Normal file
View file

@ -0,0 +1,258 @@
import com.android.build.gradle.api.BaseVariantOutput
import org.jetbrains.dokka.gradle.DokkaTask
import java.io.ByteArrayOutputStream
import java.net.URL
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-android-extensions")
id("org.jetbrains.dokka")
}
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
fun String.execute() = ByteArrayOutputStream().use { baot ->
if (project.exec {
workingDir = projectDir
commandLine = this@execute.split(Regex("\\s"))
standardOutput = baot
}.exitValue == 0)
String(baot.toByteArray()).trim()
else null
}
android {
testOptions {
unitTests.isReturnDefaultValues = true
}
signingConfigs {
create("prerelease") {
if (prereleaseStoreFile != null) {
storeFile = file(prereleaseStoreFile)
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
}
}
}
compileSdk = 33
buildToolsVersion = "30.0.3"
defaultConfig {
applicationId = "com.lagradost.cloudstream3"
minSdk = 21
targetSdk = 33
versionCode = 57
versionName = "4.0.0"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
resValue("bool", "is_prerelease", "false")
buildConfigField(
"String",
"BUILDDATE",
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
kapt {
includeCompileClasspath = true
}
}
buildTypes {
release {
isDebuggable = false
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
isDebuggable = true
applicationIdSuffix = ".debug"
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
flavorDimensions.add("state")
productFlavors {
create("stable") {
dimension = "state"
resValue("bool", "is_prerelease", "false")
}
create("prerelease") {
dimension = "state"
resValue("bool", "is_prerelease", "true")
buildConfigField("boolean", "BETA", "true")
applicationIdSuffix = ".prerelease"
signingConfig = signingConfigs.getByName("prerelease")
versionNameSuffix = "-PRE"
versionCode = (System.currentTimeMillis() / 60000).toInt()
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
}
lint {
abortOnError = false
checkReleaseBuilds = false
}
namespace = "com.lagradost.cloudstream3"
}
repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.google.android.mediahome:video:1.0.0")
implementation("androidx.test.ext:junit-ktx:1.1.3")
testImplementation("org.json:json:20180813")
implementation("androidx.core:core-ktx:1.8.0")
implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
// dont change this to 1.6.0 it looks ugly af
implementation("com.google.android.material:material:1.5.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.navigation:navigation-fragment-ktx:2.5.1")
implementation("androidx.navigation:navigation-ui-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.5.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("org.jsoup:jsoup:1.13.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("androidx.preference:preference-ktx:1.2.0")
implementation("com.github.bumptech.glide:glide:4.13.1")
kapt("com.github.bumptech.glide:compiler:4.13.1")
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
// Exoplayer
implementation("com.google.android.exoplayer:exoplayer:2.18.2")
implementation("com.google.android.exoplayer:extension-cast:2.18.2")
implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
// Use the Jellyfin ffmpeg extension for easy ffmpeg audio decoding in exoplayer. Thank you Jellyfin <3
// implementation("org.jellyfin.exoplayer:exoplayer-ffmpeg-extension:2.18.2+1")
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
// Bug reports
implementation("ch.acra:acra-core:5.8.4")
implementation("ch.acra:acra-toast:5.8.4")
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
//either for java sources:
annotationProcessor("com.google.auto.service:auto-service:1.0")
//or for kotlin sources (requires kapt gradle plugin):
kapt("com.google.auto.service:auto-service:1.0")
// subtitle color picker
implementation("com.jaredrummler:colorpicker:1.1.0")
//run JS
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation("org.mozilla:rhino:1.7.13")
// TorrentStream
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
// Downloading
implementation("androidx.work:work-runtime:2.8.0")
implementation("androidx.work:work-runtime-ktx:2.8.0")
// Networking
// implementation("com.squareup.okhttp3:okhttp:4.9.2")
// implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1")
implementation("com.github.Blatzar:NiceHttp:0.4.2")
// To fix SSL fuckery on android 9
implementation("org.conscrypt:conscrypt-android:2.2.1")
// Util to skip the URI file fuckery 🙏
implementation("com.github.tachiyomiorg:unifile:17bec43")
// API because cba maintaining it myself
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
implementation("com.github.discord:OverlappingPanels:0.1.3")
// debugImplementation because LeakCanary should only run in debug builds.
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
// for shimmer when loading
implementation("com.facebook.shimmer:shimmer:0.5.0")
implementation("androidx.tvprovider:tvprovider:1.0.0")
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
implementation("com.github.albfernandez:juniversalchardet:2.4.0")
// slow af yt
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0")
// color pallette for images -> colors
implementation("androidx.palette:palette-ktx:1.0.0")
}
tasks.register("androidSourcesJar", Jar::class) {
archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.srcDirs) //full sources
}
// this is used by the gradlew plugin
tasks.register("makeJar", Copy::class) {
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
into("build")
include("classes.jar")
dependsOn("build")
}
tasks.withType<DokkaTask>().configureEach {
moduleName.set("Cloudstream")
dokkaSourceSets {
named("main") {
sourceLink {
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
localDirectory.set(file("src/main/java"))
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
}
}
}
}

View file

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here. # Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the # You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle. # proguardFiles setting in build.gradle.kts.
# #
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html

View file

@ -1,9 +1,8 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
@ -16,142 +15,11 @@ import org.junit.runner.RunWith
*/ */
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
//@Test
//fun useAppContext() {
// // Context of the app under test.
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
//}
private fun getAllProviders(): List<MainAPI> { private fun getAllProviders(): List<MainAPI> {
println("Providers: ${APIHolder.allProviders.size}")
return APIHolder.allProviders //.filter { !it.usesWebView } return APIHolder.allProviders //.filter { !it.usesWebView }
} }
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
if (url == null) return true
var linksLoaded = 0
try {
val success = api.loadLinks(url, false, {}) { link ->
Assert.assertTrue(
"Api ${api.name} returns link with invalid Quality",
Qualities.values().map { it.value }.contains(link.quality)
)
Assert.assertTrue(
"Api ${api.name} returns link with invalid url ${link.url}",
link.url.length > 4
)
linksLoaded++
}
if (success) {
return linksLoaded > 0
}
Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .loadLinks")
}
logError(e)
}
return true
}
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
val searchQueries = listOf("over", "iron", "guy")
var correctResponses = 0
var searchResult: List<SearchResponse>? = null
for (query in searchQueries) {
val response = try {
api.search(query)
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .search")
}
logError(e)
null
}
if (!response.isNullOrEmpty()) {
correctResponses++
if (searchResult == null) {
searchResult = response
}
}
}
if (correctResponses == 0 || searchResult == null) {
System.err.println("Api ${api.name} did not return any valid search responses")
return false
}
try {
var validResults = false
for (result in searchResult) {
Assert.assertEquals(
"Invalid apiName on response on ${api.name}",
result.apiName,
api.name
)
val load = api.load(result.url) ?: continue
Assert.assertEquals(
"Invalid apiName on load on ${api.name}",
load.apiName,
result.apiName
)
Assert.assertTrue(
"Api ${api.name} on load does not contain any of the supportedTypes",
api.supportedTypes.contains(load.type)
)
when (load) {
is AnimeLoadResponse -> {
val gotNoEpisodes =
load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
if (gotNoEpisodes) {
println("Api ${api.name} got no episodes on ${load.url}")
continue
}
val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
validResults = loadLinks(api, url)
if (!validResults) continue
}
is MovieLoadResponse -> {
val gotNoEpisodes = load.dataUrl.isBlank()
if (gotNoEpisodes) {
println("Api ${api.name} got no movie on ${load.url}")
continue
}
validResults = loadLinks(api, load.dataUrl)
if (!validResults) continue
}
is TvSeriesLoadResponse -> {
val gotNoEpisodes = load.episodes.isEmpty()
if (gotNoEpisodes) {
println("Api ${api.name} got no episodes on ${load.url}")
continue
}
validResults = loadLinks(api, load.episodes.first().data)
if (!validResults) continue
}
}
break
}
if(!validResults) {
System.err.println("Api ${api.name} did not load on any")
}
return validResults
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider has not implemented .load")
}
logError(e)
return false
}
}
@Test @Test
fun providersExist() { fun providersExist() {
Assert.assertTrue(getAllProviders().isNotEmpty()) Assert.assertTrue(getAllProviders().isNotEmpty())
@ -159,6 +27,7 @@ class ExampleInstrumentedTest {
} }
@Test @Test
@Throws(AssertionError::class)
fun providerCorrectData() { fun providerCorrectData() {
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 } val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty()) Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
@ -180,66 +49,21 @@ class ExampleInstrumentedTest {
@Test @Test
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().apmap { api -> getAllProviders().amap { api ->
if (api.hasMainPage) { TestingUtils.testHomepage(api, ::println)
try {
val homepage = api.getMainPage()
when {
homepage == null -> {
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
}
homepage.items.isEmpty() -> {
System.err.println("Homepage provider ${api.name} does not contain any items!")
}
homepage.items.any { it.list.isEmpty() } -> {
System.err.println ("Homepage provider ${api.name} does not have any items on result!")
}
}
} catch (e: Exception) {
if (e.cause is NotImplementedError) {
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
}
logError(e)
}
}
} }
} }
println("Done providerCorrectHomepage") println("Done providerCorrectHomepage")
} }
// @Test
// fun testSingleProvider() {
// testSingleProviderApi(ThenosProvider())
// }
@Test @Test
fun providerCorrect() { fun testAllProvidersCorrect() {
runBlocking { runBlocking {
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>() TestingUtils.getDeferredProviderTests(
val providers = getAllProviders() this,
providers.apmap { api -> getAllProviders(),
try { ::println
println("Trying $api") ) { _, _ -> }
if (testSingleProviderApi(api)) {
println("Success $api")
} else {
System.err.println("Error $api")
invalidProvider.add(Pair(api, null))
}
} catch (e: Exception) {
logError(e)
invalidProvider.add(Pair(api, e))
}
}
if(invalidProvider.isEmpty()) {
println("No Invalid providers! :D")
} else {
println("Invalid providers are: ")
for (provider in invalidProvider) {
println("${provider.first}")
}
}
} }
println("Done providerCorrect")
} }
} }

View file

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
package="com.lagradost.cloudstream3">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads -->
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this --> <uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
@ -11,7 +10,11 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide --> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> <!-- Used for player vertical slide -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update --> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <!-- Used for app update -->
<!-- <uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ -->
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; --> <!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; -->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
@ -27,6 +30,7 @@
<package android:name="is.xyz.mpv" /> <package android:name="is.xyz.mpv" />
</queries> </queries>
<!-- 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=".AcraApplication"
@ -36,6 +40,7 @@
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true"
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"
@ -93,6 +98,16 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
<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="cloudstreamplayer" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -109,6 +124,30 @@
<data android:scheme="cloudstreamrepo" /> <data android:scheme="cloudstreamrepo" />
</intent-filter> </intent-filter>
<!-- Allow searching with intents: cloudstreamsearch://Your%20Name -->
<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="cloudstreamsearch" />
</intent-filter>
<!--
Allow opening from continue watching with intents: cloudstreamsearch://1234
Used on Android TV Watch Next
-->
<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="cloudstreamcontinuewatching" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -144,6 +183,10 @@
android:name=".ui.ControllerActivity" android:name=".ui.ControllerActivity"
android:exported="false" /> android:exported="false" />
<service
android:name=".utils.PackageInstallerService"
android:exported="false" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View file

@ -43,9 +43,9 @@ class CustomReportSender : ReportSender {
override fun send(context: Context, errorContent: CrashReportData) { override fun send(context: Context, errorContent: CrashReportData) {
println("Sending report") println("Sending report")
val url = val url =
"https://docs.google.com/forms/u/0/d/e/1FAIpQLSe9Vff8oHGMRXcjgCXZwkjvx3eBdNpn4DzjO0FkcWEU1gEQpA/formResponse" "https://docs.google.com/forms/d/e/1FAIpQLSdOlbgCx7NeaxjvEGyEQlqdh2nCvwjm2vwpP1VwW7REj9Ri3Q/formResponse"
val data = mapOf( val data = mapOf(
"entry.1586460852" to errorContent.toJSON() "entry.753293084" to errorContent.toJSON()
) )
thread { // to not run it on main thread thread { // to not run it on main thread

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.Manifest
import android.app.Activity import android.app.Activity
import android.app.PictureInPictureParams import android.app.PictureInPictureParams
import android.content.Context import android.content.Context
@ -16,6 +17,7 @@ import androidx.annotation.MainThread
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
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.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
@ -61,7 +63,9 @@ object CommonActivity {
} }
} }
fun showToast(act: Activity?, @StringRes message: Int, duration: Int) { /** duration is Toast.LENGTH_SHORT if null*/
@MainThread
fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
if (act == null) return if (act == null) return
showToast(act, act.getString(message), duration) showToast(act, act.getString(message), duration)
} }
@ -69,6 +73,7 @@ object CommonActivity {
const val TAG = "COMPACT" const val TAG = "COMPACT"
/** duration is Toast.LENGTH_SHORT if null*/ /** duration is Toast.LENGTH_SHORT if null*/
@MainThread
fun showToast(act: Activity?, message: String?, duration: Int? = null) { fun showToast(act: Activity?, message: String?, duration: Int? = null) {
if (act == null || message == null) { if (act == null || message == null) {
Log.w(TAG, "invalid showToast act = $act message = $message") Log.w(TAG, "invalid showToast act = $act message = $message")
@ -105,9 +110,18 @@ object CommonActivity {
} }
} }
/**
* Not all languages can be fetched from locale with a code.
* This map allows sidestepping the default Locale(languageCode)
* when setting the app language.
**/
val appLanguageExceptions = hashMapOf(
"zh-rTW" to Locale.TRADITIONAL_CHINESE
)
fun setLocale(context: Context?, languageCode: String?) { fun setLocale(context: Context?, languageCode: String?) {
if (context == null || languageCode == null) return if (context == null || languageCode == null) return
val locale = Locale(languageCode) val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
val resources: Resources = context.resources val resources: Resources = context.resources
val config = resources.configuration val config = resources.configuration
Locale.setDefault(locale) Locale.setDefault(locale)
@ -143,8 +157,8 @@ object CommonActivity {
val resultCode = result.resultCode val resultCode = result.resultCode
val data = result.data val data = result.data
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
val pos = data.getLongExtra(resumeApp.position, -1L) val pos = resumeApp.getPosition(data)
val dur = data.getLongExtra(resumeApp.duration, -1L) val dur = resumeApp.getDuration(data)
if (dur > 0L && pos > 0L) if (dur > 0L && pos > 0L)
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur) DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
removeKey(resumeApp.lastId) removeKey(resumeApp.lastId)
@ -152,6 +166,23 @@ object CommonActivity {
} }
} }
} }
// Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(
act,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
val requestPermissionLauncher = act.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted")
}
requestPermissionLauncher.launch(
Manifest.permission.POST_NOTIFICATIONS
)
}
} }
private fun Activity.enterPIPMode() { private fun Activity.enterPIPMode() {
@ -337,6 +368,9 @@ object CommonActivity {
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> { KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp 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 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 PlayerEventType.PlayPauseToggle
} }
@ -415,4 +449,4 @@ object CommonActivity {
} }
return null return null
} }
} }

View file

@ -0,0 +1,11 @@
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))
}

View file

@ -13,17 +13,17 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.loadExtractor
import okhttp3.Interceptor import okhttp3.Interceptor
import org.mozilla.javascript.Scriptable
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
import kotlin.collections.MutableList
const val USER_AGENT = const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
@ -32,6 +32,12 @@ const val USER_AGENT =
val mapper = JsonMapper.builder().addModule(KotlinModule()) val mapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!! .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
/**
* Defines the constant for the all languages preference, if this is set then it is
* the equivalent of all languages being set
**/
const val AllLanguagesName = "universal"
object APIHolder { object APIHolder {
val unixTime: Long val unixTime: Long
get() = System.currentTimeMillis() / 1000L get() = System.currentTimeMillis() / 1000L
@ -41,7 +47,7 @@ object APIHolder {
private const val defProvider = 0 private const val defProvider = 0
// ConcurrentModificationException is possible!!! // ConcurrentModificationException is possible!!!
val allProviders: MutableList<MainAPI> = arrayListOf() val allProviders = threadSafeListOf<MainAPI>()
fun initAll() { fun initAll() {
for (api in allProviders) { for (api in allProviders) {
@ -54,7 +60,7 @@ object APIHolder {
return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } return this.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
} }
var apis: List<MainAPI> = arrayListOf() var apis: List<MainAPI> = threadSafeListOf()
var apiMap: Map<String, Int>? = null var apiMap: Map<String, Int>? = null
fun addPluginMapping(plugin: MainAPI) { fun addPluginMapping(plugin: MainAPI) {
@ -74,16 +80,20 @@ object APIHolder {
fun getApiFromNameNull(apiName: String?): MainAPI? { fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null if (apiName == null) return null
initMap() synchronized(allProviders) {
return apiMap?.get(apiName)?.let { apis.getOrNull(it) } initMap()
?: allProviders.firstOrNull { it.name == apiName } return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName }
}
} }
fun getApiFromUrlNull(url: String?): MainAPI? { fun getApiFromUrlNull(url: String?): MainAPI? {
if (url == null) return null if (url == null) return null
for (api in allProviders) { synchronized(allProviders) {
if (url.startsWith(api.mainUrl)) allProviders.forEach { api ->
return api if (url.startsWith(api.mainUrl)) return api
}
} }
return null return null
} }
@ -152,12 +162,61 @@ object APIHolder {
return null return null
} }
private var trackerCache: HashMap<String, AniSearch> = hashMapOf()
/**
* Get anime tracker information based on title, year and type.
* Both titles are attempted to be matched with both Romaji and English title.
* Uses the consumet api.
*
* @param titles uses first index to search, but if you have multiple titles and want extra guarantee to match you can also have that
* @param types Optional parameter to narrow down the scope to Movies, TV, etc. See TrackerType.getTypes()
* @param year Optional parameter to only get anime with a specific year
**/
suspend fun getTracker(
titles: List<String>,
types: Set<TrackerType>?,
year: Int?
): Tracker? {
return try {
require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" }
val mainTitle = titles[0]
val search =
trackerCache[mainTitle]
?: app.get("https://api.consumet.org/meta/anilist/$mainTitle")
.parsedSafe<AniSearch>()?.also {
trackerCache[mainTitle] = it
} ?: return null
val res = search.results?.find { media ->
val matchingYears = year == null || media.releaseDate == year
val matchingTitles = media.title?.let { title ->
titles.any { userTitle ->
title.isMatchingTitles(userTitle)
}
} ?: false
val matchingTypes = types?.any { it.name.equals(media.type, true) } == true
matchingTitles && matchingTypes && matchingYears
} ?: return null
Tracker(res.malId, res.aniId, res.image, res.cover)
} catch (t: Throwable) {
logError(t)
null
}
}
fun Context.getApiSettings(): HashSet<String> { fun Context.getApiSettings(): HashSet<String> {
//val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) //val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>() val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings() val activeLangs = getApiProviderLangSettings()
hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name }) val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
.map { it.name })
/*val set = settingsManager.getStringSet( /*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key), this.getString(R.string.search_providers_list_key),
@ -193,11 +252,11 @@ object APIHolder {
fun Context.getApiProviderLangSettings(): HashSet<String> { fun Context.getApiProviderLangSettings(): HashSet<String> {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = HashSet<String>() val hashSet = hashSetOf(AllLanguagesName) // def is all languages
hashSet.add("en") // def is only en // hashSet.add("en") // def is only en
val list = settingsManager.getStringSet( val list = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key), this.getString(R.string.provider_lang_key),
hashSet.toMutableSet() hashSet
) )
if (list.isNullOrEmpty()) return hashSet if (list.isNullOrEmpty()) return hashSet
@ -227,13 +286,24 @@ object APIHolder {
} }
private fun Context.getHasTrailers(): Boolean { private fun Context.getHasTrailers(): Boolean {
if (isTvSettings()) return false
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true) return settingsManager.getBoolean(this.getString(R.string.show_trailers_key), true)
} }
fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> { fun Context.filterProviderByPreferredMedia(hasHomePageIsRequired: Boolean = true): List<MainAPI> {
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal } // We are getting the weirdest crash ever done:
// java.lang.ClassCastException: com.lagradost.cloudstream3.TvType cannot be cast to com.lagradost.cloudstream3.TvType
// Trying fixing using classloader fuckery
val oldLoader = Thread.currentThread().contextClassLoader
Thread.currentThread().contextClassLoader = TvType::class.java.classLoader
val default = TvType.values()
.sorted()
.filter { it != TvType.NSFW }
.map { it.ordinal }
Thread.currentThread().contextClassLoader = oldLoader
val defaultSet = default.map { it.toString() }.toSet() val defaultSet = default.map { it.toString() }.toSet()
val currentPrefMedia = try { val currentPrefMedia = try {
PreferenceManager.getDefaultSharedPreferences(this) PreferenceManager.getDefaultSharedPreferences(this)
@ -243,7 +313,8 @@ object APIHolder {
null null
} ?: default } ?: default
val langs = this.getApiProviderLangSettings() val langs = this.getApiProviderLangSettings()
val allApis = apis.filter { langs.contains(it.lang) } val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
.filter { api -> api.hasMainPage || !hasHomePageIsRequired } .filter { api -> api.hasMainPage || !hasHomePageIsRequired }
return if (currentPrefMedia.isEmpty()) { return if (currentPrefMedia.isEmpty()) {
allApis allApis
@ -296,6 +367,57 @@ object APIHolder {
} }
} }
/*
// THIS IS WORK IN PROGRESS API
interface ITag {
val name: UiText
}
data class SimpleTag(override val name: UiText, val data: String) : ITag
enum class SelectType {
SingleSelect,
MultiSelect,
MultiSelectAndExclude,
}
enum class SelectValue {
Selected,
Excluded,
}
interface GenreSelector {
val title: UiText
val id : Int
}
data class TagSelector(
override val title: UiText,
override val id : Int,
val tags: Set<ITag>,
val defaultTags : Set<ITag> = setOf(),
val selectType: SelectType = SelectType.SingleSelect,
) : GenreSelector
data class BoolSelector(
override val title: UiText,
override val id : Int,
val defaultValue : Boolean = false,
) : GenreSelector
data class InputField(
override val title: UiText,
override val id : Int,
val hint : UiText? = null,
) : GenreSelector
// This response describes how a user might filter the homepage or search results
data class GenreResponse(
val searchSelectors : List<GenreSelector>,
val filterSelectors: List<GenreSelector> = searchSelectors
) */
/* /*
0 = Site not good 0 = Site not good
@ -324,13 +446,24 @@ data class SettingsJson(
data class MainPageData( data class MainPageData(
val name: String, val name: String,
val data: String, val data: String,
val horizontalImages: Boolean = false
) )
data class MainPageRequest( data class MainPageRequest(
val name: String, val name: String,
val data: String, val data: String,
val horizontalImages: Boolean,
//TODO genre selection or smth
) )
fun mainPage(url: String, name: String, horizontalImages: Boolean = false): MainPageData {
return MainPageData(name = name, data = url, horizontalImages = horizontalImages)
}
fun mainPageOf(vararg elements: MainPageData): List<MainPageData> {
return elements.toList()
}
/** return list of MainPageData with url to name, make for more readable code */ /** return list of MainPageData with url to name, make for more readable code */
fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> { fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> {
return elements.map { (url, name) -> MainPageData(name = name, data = url) } return elements.map { (url, name) -> MainPageData(name = name, data = url) }
@ -339,7 +472,7 @@ fun mainPageOf(vararg elements: Pair<String, String>): List<MainPageData> {
fun newHomePageResponse( fun newHomePageResponse(
name: String, name: String,
list: List<SearchResponse>, list: List<SearchResponse>,
hasNext: Boolean? = null hasNext: Boolean? = null,
): HomePageResponse { ): HomePageResponse {
return HomePageResponse( return HomePageResponse(
listOf(HomePageList(name, list)), listOf(HomePageList(name, list)),
@ -347,6 +480,17 @@ fun newHomePageResponse(
) )
} }
fun newHomePageResponse(
data: MainPageRequest,
list: List<SearchResponse>,
hasNext: Boolean? = null,
): HomePageResponse {
return HomePageResponse(
listOf(HomePageList(data.name, list, data.horizontalImages)),
hasNext = hasNext ?: list.isNotEmpty()
)
}
fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse { fun newHomePageResponse(list: HomePageList, hasNext: Boolean? = null): HomePageResponse {
return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty()) return HomePageResponse(listOf(list), hasNext = hasNext ?: list.list.isNotEmpty())
} }
@ -381,7 +525,19 @@ abstract class MainAPI {
open var storedCredentials: String? = null open var storedCredentials: String? = null
open var canBeOverridden: Boolean = true open var canBeOverridden: Boolean = true
//open val uniqueId : Int by lazy { this.name.hashCode() } // in case of duplicate providers you can have a shared id /** if this is turned on then it will request the homepage one after the other,
used to delay if they block many request at the same time*/
open var sequentialMainPage: Boolean = false
/** in milliseconds, this can be used to add more delay between homepage requests
* on first load if sequentialMainPage is turned on */
open var sequentialMainPageDelay: Long = 0L
/** in milliseconds, this can be used to add more delay between homepage requests when scrolling */
open var sequentialMainPageScrollDelay: Long = 0L
/** used to keep track when last homepage request was in unixtime ms */
var lastHomepageRequest: Long = 0L
open var lang = "en" // ISO_639_1 check SubtitleHelper open var lang = "en" // ISO_639_1 check SubtitleHelper
@ -403,6 +559,20 @@ abstract class MainAPI {
open val hasMainPage = false open val hasMainPage = false
open val hasQuickSearch = false open val hasQuickSearch = false
/**
* A set of which ids the provider can open with getLoadUrl()
* If the set contains SyncIdName.Imdb then getLoadUrl() can be started with
* an Imdb class which inherits from SyncId.
*
* getLoadUrl() is then used to get page url based on that ID.
*
* Example:
* "tt6723592" -> getLoadUrl(ImdbSyncId("tt6723592")) -> "mainUrl/imdb/tt6723592" -> load("mainUrl/imdb/tt6723592")
*
* This is used to launch pages from personal lists or recommendations using IDs.
**/
open val supportedSyncNames = setOf<SyncIdName>()
open val supportedTypes = setOf( open val supportedTypes = setOf(
TvType.Movie, TvType.Movie,
TvType.TvSeries, TvType.TvSeries,
@ -414,7 +584,8 @@ abstract class MainAPI {
open val vpnStatus = VPNStatus.None open val vpnStatus = VPNStatus.None
open val providerType = ProviderType.DirectProvider open val providerType = ProviderType.DirectProvider
open val mainPage = listOf(MainPageData("", "")) //emptyList<MainPageData>() //
open val mainPage = listOf(MainPageData("", "", false))
@WorkerThread @WorkerThread
open suspend fun getMainPage( open suspend fun getMainPage(
@ -472,6 +643,14 @@ abstract class MainAPI {
open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? { open fun getVideoInterceptor(extractorLink: ExtractorLink): Interceptor? {
return null return null
} }
/**
* Get the load() url based on a sync ID like IMDb or MAL.
* Only contains SyncIds based on supportedSyncUrls.
**/
open suspend fun getLoadUrl(name: SyncIdName, id: String): String? {
return null
}
} }
/** Might need a different implementation for desktop*/ /** Might need a different implementation for desktop*/
@ -557,6 +736,19 @@ fun fixTitle(str: String): String {
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } .replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
} }
} }
/**
* Get rhino context in a safe way as it needs to be initialized on the main thread.
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
* Use like the following: rhino.evaluateString(scope, js, "JavaScript", 1, null)
**/
suspend fun getRhinoContext(): org.mozilla.javascript.Context {
return Coroutines.mainWork {
val rhino = org.mozilla.javascript.Context.enter()
rhino.initSafeStandardObjects()
rhino.optimizationLevel = -1
rhino
}
}
/** https://www.imdb.com/title/tt2861424/ -> tt2861424 */ /** https://www.imdb.com/title/tt2861424/ -> tt2861424 */
fun imdbUrlToId(url: String): String? { fun imdbUrlToId(url: String): String? {
@ -1020,7 +1212,7 @@ interface LoadResponse {
) { ) {
if (!isTrailersEnabled || trailerUrls == null) return if (!isTrailersEnabled || trailerUrls == null) return
trailers.addAll(trailerUrls.map { TrailerData(it, referer, addRaw) }) trailers.addAll(trailerUrls.map { TrailerData(it, referer, addRaw) })
/*val trailers = trailerUrls.filter { it.isNotBlank() }.apmap { trailerUrl -> /*val trailers = trailerUrls.filter { it.isNotBlank() }.amap { trailerUrl ->
val links = arrayListOf<ExtractorLink>() val links = arrayListOf<ExtractorLink>()
val subs = arrayListOf<SubtitleFile>() val subs = arrayListOf<SubtitleFile>()
if (!loadExtractor( if (!loadExtractor(
@ -1081,18 +1273,43 @@ interface LoadResponse {
fun getDurationFromString(input: String?): Int? { fun getDurationFromString(input: String?): Int? {
val cleanInput = input?.trim()?.replace(" ", "") ?: return null val cleanInput = input?.trim()?.replace(" ", "") ?: return null
//Use first as sometimes the text passes on the 2 other Regex, but failed to provide accurate return value
Regex("(\\d+\\shr)|(\\d+\\shour)|(\\d+\\smin)|(\\d+\\ssec)").findAll(input).let { values ->
var seconds = 0
values.forEach {
val time_text = it.value
if (time_text.isNotBlank()) {
val time = time_text.filter { s -> s.isDigit() }.trim().toInt()
val scale = time_text.filter { s -> !s.isDigit() }.trim()
//println("Scale: $scale")
val timeval = when (scale) {
"hr", "hour" -> time * 60 * 60
"min" -> time * 60
"sec" -> time
else -> 0
}
seconds += timeval
}
}
if (seconds > 0) {
return seconds / 60
}
}
Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values -> Regex("([0-9]*)h.*?([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 3) { if (values.size == 3) {
val hours = values[1].toIntOrNull() val hours = values[1].toIntOrNull()
val minutes = values[2].toIntOrNull() val minutes = values[2].toIntOrNull()
return if (minutes != null && hours != null) { if (minutes != null && hours != null) {
hours * 60 + minutes return hours * 60 + minutes
} else null }
} }
} }
Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values -> Regex("([0-9]*)m").find(cleanInput)?.groupValues?.let { values ->
if (values.size == 2) { if (values.size == 2) {
return values[1].toIntOrNull() val return_value = values[1].toIntOrNull()
if (return_value != null) {
return return_value
}
} }
} }
return null return null
@ -1110,7 +1327,7 @@ fun LoadResponse?.isAnimeBased(): Boolean {
fun TvType?.isEpisodeBased(): Boolean { fun TvType?.isEpisodeBased(): Boolean {
if (this == null) return false if (this == null) return false
return (this == TvType.TvSeries || this == TvType.Anime) return (this == TvType.TvSeries || this == TvType.Anime || this == TvType.AsianDrama)
} }
@ -1134,6 +1351,7 @@ interface EpisodeResponse {
var showStatus: ShowStatus? var showStatus: ShowStatus?
var nextAiring: NextAiring? var nextAiring: NextAiring?
var seasonNames: List<SeasonData>? var seasonNames: List<SeasonData>?
fun getLatestEpisodes(): Map<DubStatus, Int?>
} }
@JvmName("addSeasonNamesString") @JvmName("addSeasonNamesString")
@ -1202,7 +1420,18 @@ data class AnimeLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse ) : LoadResponse, EpisodeResponse {
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
return episodes.map { (status, episodes) ->
val maxSeason = episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
status to episodes
.filter { it.season == maxSeason }
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
}.toMap()
}
}
/** /**
* If episodes already exist appends the list. * If episodes already exist appends the list.
@ -1400,7 +1629,17 @@ data class TvSeriesLoadResponse(
override var nextAiring: NextAiring? = null, override var nextAiring: NextAiring? = null,
override var seasonNames: List<SeasonData>? = null, override var seasonNames: List<SeasonData>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
) : LoadResponse, EpisodeResponse ) : LoadResponse, EpisodeResponse {
override fun getLatestEpisodes(): Map<DubStatus, Int?> {
val maxSeason =
episodes.maxOfOrNull { it.season ?: Int.MIN_VALUE }.takeUnless { it == Int.MIN_VALUE }
val max = episodes
.filter { it.season == maxSeason }
.maxOfOrNull { it.episode ?: Int.MIN_VALUE }
.takeUnless { it == Int.MIN_VALUE }
return mapOf(DubStatus.None to max)
}
}
suspend fun MainAPI.newTvSeriesLoadResponse( suspend fun MainAPI.newTvSeriesLoadResponse(
name: String, name: String,
@ -1432,3 +1671,61 @@ fun fetchUrls(text: String?): List<String> {
fun String?.toRatingInt(): Int? = fun String?.toRatingInt(): Int? =
this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt() this?.replace(" ", "")?.trim()?.toDoubleOrNull()?.absoluteValue?.times(1000f)?.toInt()
data class Tracker(
val malId: Int? = null,
val aniId: String? = null,
val image: String? = null,
val cover: String? = null,
)
data class Title(
@JsonProperty("romaji") val romaji: String? = null,
@JsonProperty("english") val english: String? = null,
) {
fun isMatchingTitles(title: String?): Boolean {
if (title == null) return false
return english.equals(title, true) || romaji.equals(title, true)
}
}
data class Results(
@JsonProperty("id") val aniId: String? = null,
@JsonProperty("malId") val malId: Int? = null,
@JsonProperty("title") val title: Title? = null,
@JsonProperty("releaseDate") val releaseDate: Int? = null,
@JsonProperty("type") val type: String? = null,
@JsonProperty("image") val image: String? = null,
@JsonProperty("cover") val cover: String? = null,
)
data class AniSearch(
@JsonProperty("results") val results: ArrayList<Results>? = arrayListOf()
)
/**
* used for the getTracker() method
**/
enum class TrackerType {
MOVIE,
TV,
TV_SHORT,
ONA,
OVA,
SPECIAL,
MUSIC;
companion object {
fun getTypes(type: TvType): Set<TrackerType> {
return when (type) {
TvType.Movie -> setOf(MOVIE)
TvType.AnimeMovie -> setOf(MOVIE)
TvType.TvSeries -> setOf(TV, TV_SHORT)
TvType.Anime -> setOf(TV, TV_SHORT, ONA, OVA)
TvType.OVA -> setOf(OVA, SPECIAL, ONA)
TvType.Others -> setOf(MUSIC)
else -> emptySet()
}
}
}
}

View file

@ -1,21 +1,25 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.*
import android.view.Menu import android.widget.Toast
import android.view.MenuItem
import android.view.WindowManager
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavDestination.Companion.hierarchy
@ -28,7 +32,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.android.gms.cast.framework.* import com.google.android.gms.cast.framework.*
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.APIHolder.allProviders
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
@ -43,55 +49,78 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths
import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO
import com.lagradost.cloudstream3.ui.home.HomeViewModel
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.IOnBackPressed
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.bottom_resultview_preview.*
import kotlinx.android.synthetic.main.fragment_result_swipe.* import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.net.URLDecoder
import java.nio.charset.Charset import java.nio.charset.Charset
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess
//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 //https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
@ -112,13 +141,15 @@ val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlay
val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity")
//TODO REFACTOR AF //TODO REFACTOR AF
data class ResultResume( open class ResultResume(
val packageString: String, val packageString: String,
val action: String = Intent.ACTION_VIEW, val action: String = Intent.ACTION_VIEW,
val position: String? = null, val position: String? = null,
val duration: String? = null, val duration: String? = null,
var launcher: ActivityResultLauncher<Intent>? = null, var launcher: ActivityResultLauncher<Intent>? = null,
) { ) {
val defaultTime = -1L
val lastId get() = "${packageString}_last_open_id" val lastId get() = "${packageString}_last_open_id"
suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) {
val intent = Intent(action) val intent = Intent(action)
@ -132,21 +163,50 @@ data class ResultResume(
callback.invoke(intent) callback.invoke(intent)
launcher?.launch(intent) launcher?.launch(intent)
} }
open fun getPosition(intent: Intent?): Long {
return defaultTime
}
open fun getDuration(intent: Intent?): Long {
return defaultTime
}
} }
val VLC = ResultResume( val VLC = object : ResultResume(
VLC_PACKAGE, VLC_PACKAGE,
"org.videolan.vlc.player.result", // Android 13 intent restrictions fucks up specifically launching the VLC player
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.player.result"
} else {
Intent.ACTION_VIEW
},
"extra_position", "extra_position",
"extra_duration", "extra_duration",
) ) {
override fun getPosition(intent: Intent?): Long {
return intent?.getLongExtra(this.position, defaultTime) ?: defaultTime
}
val MPV = ResultResume( override fun getDuration(intent: Intent?): Long {
return intent?.getLongExtra(this.duration, defaultTime) ?: defaultTime
}
}
val MPV = object : ResultResume(
MPV_PACKAGE, MPV_PACKAGE,
//"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive:
position = "position", position = "position",
duration = "duration", duration = "duration",
) ) {
override fun getPosition(intent: Intent?): Long {
return intent?.getIntExtra(this.position, defaultTime.toInt())?.toLong() ?: defaultTime
}
override fun getDuration(intent: Intent?): Long {
return intent?.getIntExtra(this.duration, defaultTime.toInt())?.toLong() ?: defaultTime
}
}
val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE)
@ -185,14 +245,30 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
companion object { companion object {
const val TAG = "MAINACT" const val TAG = "MAINACT"
/**
* Setting this will automatically enter the query in the search
* next time the search fragment is opened.
* This variable will clear itself after one use. Null does nothing.
*
* This is a very bad solution but I was unable to find a better one.
**/
private var nextSearchQuery: String? = null
/** /**
* Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread * Fires every time a new batch of plugins have been loaded, no guarantee about how often this is run and on which thread
* Boolean signifies if stuff should be force reloaded (true if force reload, false if reload when necessary).
*
* The force reloading are used for plugin development to instantly reload the page on deployWithAdb
* */ * */
val afterPluginsLoadedEvent = Event<Boolean>() val afterPluginsLoadedEvent = Event<Boolean>()
val mainPluginsLoadedEvent = val mainPluginsLoadedEvent =
Event<Boolean>() // homepage api, used to speed up time to load for homepage Event<Boolean>() // homepage api, used to speed up time to load for homepage
val afterRepositoryLoadedEvent = Event<Boolean>() val afterRepositoryLoadedEvent = Event<Boolean>()
// kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event<Boolean>()
/** /**
* @return true if the str has launched an app task (be it successful or not) * @return true if the str has launched an app task (be it successful or not)
* @param isWebview does not handle providers and opening download page if true. Can still add repos and login. * @param isWebview does not handle providers and opening download page if true. Can still add repos and login.
@ -203,6 +279,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
isWebview: Boolean isWebview: Boolean
): Boolean = ): Boolean =
with(activity) { with(activity) {
// TODO MUCH BETTER HANDLING
// Invalid URIs can crash
fun safeURI(uri: String) = normalSafeApiCall { URI(uri) }
if (str != null && this != null) { if (str != null && this != null) {
if (str.startsWith("https://cs.repo")) { if (str.startsWith("https://cs.repo")) {
val realUrl = "https://" + str.substringAfter("?") val realUrl = "https://" + str.substringAfter("?")
@ -238,10 +319,50 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
return true return true
} }
} }
} else if (URI(str).scheme == appStringRepo) { // This specific intent is used for the gradle deployWithAdb
// https://github.com/recloudstream/gradle/blob/master/src/main/kotlin/com/lagradost/cloudstream3/gradle/tasks/DeployWithAdbTask.kt#L46
if (str == "$appString:") {
PluginManager.hotReloadAllLocalPlugins(activity)
}
} else if (safeURI(str)?.scheme == appStringRepo) {
val url = str.replaceFirst(appStringRepo, "https") val url = str.replaceFirst(appStringRepo, "https")
loadRepository(url) loadRepository(url)
return true return true
} else if (safeURI(str)?.scheme == appStringSearch) {
nextSearchQuery =
URLDecoder.decode(str.substringAfter("$appStringSearch://"), "UTF-8")
// Use both navigation views to support both layouts.
// It might be better to use the QuickSearch.
nav_view?.selectedItemId = R.id.navigation_search
nav_rail_view?.selectedItemId = R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
val url = URLDecoder.decode(uri.authority, "UTF-8")
navigate(
R.id.global_to_navigation_player,
GeneratorPlayer.newInstance(
LinkGenerator(
listOf(BasicLink(url, name)),
extract = true,
)
)
)
} else if (safeURI(str)?.scheme == appStringResumeWatching) {
val id =
str.substringAfter("$appStringResumeWatching://").toIntOrNull()
?: return false
ioSafe {
val resumeWatchingCard =
HomeViewModel.getResumeWatching()?.firstOrNull { it.id == id }
?: return@ioSafe
activity.loadSearchResult(
resumeWatchingCard,
START_ACTION_RESUME_LATEST
)
}
} else if (!isWebview) { } else if (!isWebview) {
if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) { if (str.startsWith(DOWNLOAD_NAVIGATE_TO)) {
this.navigate(R.id.navigation_downloads) this.navigate(R.id.navigation_downloads)
@ -260,6 +381,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
var lastPopup: SearchResponse? = null
fun loadPopup(result: SearchResponse) {
lastPopup = result
viewModel.load(
this, result.url, result.apiName, false, if (getApiDubstatusSettings()
.contains(DubStatus.Dubbed)
) DubStatus.Dubbed else DubStatus.Subbed, null
)
}
override fun onColorSelected(dialogId: Int, color: Int) { override fun onColorSelected(dialogId: Int, color: Int) {
onColorSelectedEvent.invoke(Pair(dialogId, color)) onColorSelectedEvent.invoke(Pair(dialogId, color))
} }
@ -291,6 +422,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val isNavVisible = listOf( val isNavVisible = listOf(
R.id.navigation_home, R.id.navigation_home,
R.id.navigation_search, R.id.navigation_search,
R.id.navigation_library,
R.id.navigation_downloads, R.id.navigation_downloads,
R.id.navigation_settings, R.id.navigation_settings,
R.id.navigation_download_child, R.id.navigation_download_child,
@ -304,8 +436,30 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_settings_general, R.id.navigation_settings_general,
R.id.navigation_settings_extensions, R.id.navigation_settings_extensions,
R.id.navigation_settings_plugins, R.id.navigation_settings_plugins,
R.id.navigation_test_providers,
).contains(destination.id) ).contains(destination.id)
val dontPush = listOf(
R.id.navigation_home,
R.id.navigation_search,
R.id.navigation_results_phone,
R.id.navigation_results_tv,
R.id.navigation_player,
).contains(destination.id)
nav_host_fragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
params.setMargins(
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
params.topMargin,
params.rightMargin,
params.bottomMargin
)
layoutParams = params
}
val landscape = when (resources.configuration.orientation) { val landscape = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> { Configuration.ORIENTATION_LANDSCAPE -> {
true true
@ -320,6 +474,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.isVisible = isNavVisible && !landscape nav_view?.isVisible = isNavVisible && !landscape
nav_rail_view?.isVisible = isNavVisible && landscape nav_rail_view?.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
} }
//private var mCastSession: CastSession? = null //private var mCastSession: CastSession? = null
@ -372,6 +531,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
// Start any delayed updates
if (ApkInstaller.delayedInstaller?.startInstallation() == true) {
Toast.makeText(this, R.string.update_started, Toast.LENGTH_LONG).show()
}
try { try {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
mSessionManager.removeSessionManagerListener(mSessionManagerListener) mSessionManager.removeSessionManagerListener(mSessionManagerListener)
@ -402,12 +566,34 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
onUserLeaveHint(this) onUserLeaveHint(this)
} }
private fun showConfirmExitDialog() {
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder.setTitle(R.string.confirm_exit_dialog)
builder.apply {
// Forceful exit since back button can actually go back to setup
setPositiveButton(R.string.yes) { _, _ -> exitProcess(0) }
setNegativeButton(R.string.no) { _, _ -> }
}
builder.show().setDefaultFocus()
}
private fun backPressed() { private fun backPressed() {
this.window?.navigationBarColor = this.window?.navigationBarColor =
this.colorFromAttribute(R.attr.primaryGrayBackground) this.colorFromAttribute(R.attr.primaryGrayBackground)
this.updateLocale() this.updateLocale()
super.onBackPressed()
this.updateLocale() this.updateLocale()
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
val navController = navHostFragment?.navController
val isAtHome =
navController?.currentDestination?.matchDestination(R.id.navigation_home) == true
if (isAtHome && isTrueTvSettings()) {
showConfirmExitDialog()
} else {
super.onBackPressed()
}
} }
override fun onBackPressed() { override fun onBackPressed() {
@ -495,6 +681,37 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
lateinit var viewModel: ResultViewModel2
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
viewModel =
ViewModelProvider(this)[ResultViewModel2::class.java]
return super.onCreateView(name, context, attrs)
}
private fun hidePreviewPopupDialog() {
viewModel.clear()
bottomPreviewPopup.dismissSafe(this)
}
var bottomPreviewPopup: BottomSheetDialog? = null
private fun showPreviewPopupDialog(): BottomSheetDialog {
val ret = (bottomPreviewPopup ?: run {
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_resultview_preview)
builder.setOnDismissListener {
bottomPreviewPopup = null
viewModel.clear()
}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
})
bottomPreviewPopup = ret
return ret
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this) app.initClient(this)
@ -525,7 +742,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
updateTv()
if (isTvSettings()) { if (isTvSettings()) {
setContentView(R.layout.activity_main_tv) setContentView(R.layout.activity_main_tv)
} else { } else {
@ -534,7 +751,35 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
changeStatusBarState(isEmulatorSettings()) changeStatusBarState(isEmulatorSettings())
if (lastError == null) { // Automatically enable jsdelivr if cant connect to raw.githubusercontent.com
if (this.getKey<Boolean>(getString(R.string.jsdelivr_proxy_key)) == null && isNetworkAvailable()) {
main {
if (checkGithubConnectivity()) {
this.setKey(getString(R.string.jsdelivr_proxy_key), false)
} else {
this.setKey(getString(R.string.jsdelivr_proxy_key), true)
val parentView: View = findViewById(android.R.id.content)
Snackbar.make(parentView, R.string.jsdelivr_enabled, Snackbar.LENGTH_LONG)
.let { snackbar ->
snackbar.setAction(R.string.revert) {
setKey(getString(R.string.jsdelivr_proxy_key), false)
}
snackbar.setBackgroundTint(colorFromAttribute(R.attr.primaryGrayBackground))
snackbar.setTextColor(colorFromAttribute(R.attr.textColor))
snackbar.setActionTextColor(colorFromAttribute(R.attr.colorPrimary))
snackbar.show()
}
}
}
}
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
ioSafe { ioSafe {
getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> getKey<String>(USER_SELECTED_HOMEPAGE_API)?.let { homeApi ->
mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi))
@ -550,12 +795,21 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
) { ) {
PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity) PluginManager.updateAllOnlinePluginsAndLoadThem(this@MainActivity)
} else { } else {
PluginManager.loadAllOnlinePlugins(this@MainActivity) loadAllOnlinePlugins(this@MainActivity)
}
//Automatically download not existing plugins
if (settingsManager.getBoolean(
getString(R.string.auto_download_plugins_key),
false
)
) {
PluginManager.downloadNotExistingPluginsAndLoad(this@MainActivity)
} }
} }
ioSafe { ioSafe {
PluginManager.loadAllLocalPlugins(this@MainActivity) PluginManager.loadAllLocalPlugins(this@MainActivity, false)
} }
} }
} else { } else {
@ -572,9 +826,81 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
setNegativeButton("Ok") { _, _ -> } setNegativeButton("Ok") { _, _ -> }
} }
builder.show() builder.show().setDefaultFocus()
} }
observeNullable(viewModel.page) { resource ->
if (resource == null) {
bottomPreviewPopup.dismissSafe(this)
return@observeNullable
}
when (resource) {
is Resource.Failure -> {
showToast(this, R.string.error)
hidePreviewPopupDialog()
}
is Resource.Loading -> {
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = true
resultview_preview_result?.isVisible = false
resultview_preview_loading_shimmer?.startShimmer()
}
}
is Resource.Success -> {
val d = resource.value
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = false
resultview_preview_result?.isVisible = true
resultview_preview_loading_shimmer?.stopShimmer()
resultview_preview_title?.text = d.title
resultview_preview_meta_type.setText(d.typeText)
resultview_preview_meta_year.setText(d.yearText)
resultview_preview_meta_duration.setText(d.durationText)
resultview_preview_meta_rating.setText(d.ratingText)
resultview_preview_description?.setText(d.plotText)
resultview_preview_poster?.setImage(
d.posterImage ?: d.posterBackgroundImage
)
resultview_preview_poster?.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
val value = viewModel.watchStatus.value ?: WatchType.NONE
this@MainActivity.showBottomDialog(
WatchType.values().map { getString(it.stringRes) }.toList(),
value.ordinal,
this@MainActivity.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
bookmarksUpdatedEvent(true)
}
}
if (!isTvSettings()) // dont want this clickable on tv layout
resultview_preview_description?.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(d.plotText.asString(ctx).html())
.setTitle(d.plotHeaderText.asString(ctx))
.show()
}
}
resultview_preview_more_info?.setOnClickListener {
hidePreviewPopupDialog()
lastPopup?.let {
loadSearchResult(it)
}
}
}
}
}
}
// ioSafe { // ioSafe {
// val plugins = // val plugins =
@ -592,7 +918,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
api.init() api.init()
} }
inAppAuths.apmap { api -> inAppAuths.amap { api ->
try { try {
api.initialize() api.initialize()
} catch (e: Exception) { } catch (e: Exception) {
@ -616,6 +942,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
val navHostFragment = val navHostFragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController val navController = navHostFragment.navController
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
// Intercept search and add a query
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
bundle?.apply {
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
nextSearchQuery = null
}
}
}
//val navController = findNavController(R.id.nav_host_fragment) //val navController = findNavController(R.id.nav_host_fragment)
/*navOptions = NavOptions.Builder() /*navOptions = NavOptions.Builder()
@ -629,7 +966,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
nav_view?.setupWithNavController(navController) nav_view?.setupWithNavController(navController)
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view) val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
nav_rail?.setupWithNavController(navController) nav_rail?.setupWithNavController(navController)
if (isTvSettings()) {
nav_rail?.background?.alpha = 200
} else {
nav_rail?.background?.alpha = 255
}
nav_rail?.setOnItemSelectedListener { item -> nav_rail?.setOnItemSelectedListener { item ->
onNavDestinationSelected( onNavDestinationSelected(
item, item,
@ -798,10 +1140,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Used to check current focus for TV // Used to check current focus for TV
// main { // main {
// while (true) { // while (true) {
// delay(1000) // delay(5000)
// println("Current focus: $currentFocus") // println("Current focus: $currentFocus")
// showToast(this, currentFocus.toString(), Toast.LENGTH_LONG)
// } // }
// } // }
} }
suspend fun checkGithubConnectivity(): Boolean {
return try {
app.get(
"https://raw.githubusercontent.com/recloudstream/.github/master/connectivitycheck",
timeout = 5
).text.trim() == "ok"
} catch (t: Throwable) {
false
}
}
} }

View file

@ -1,8 +1,7 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.async import kotlinx.coroutines.*
import kotlinx.coroutines.runBlocking
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections //https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
/* /*
@ -26,10 +25,25 @@ fun <T, R> Iterable<T>.pmap(
return ArrayList<R>(destination) return ArrayList<R>(destination)
}*/ }*/
@OptIn(DelicateCoroutinesApi::class)
suspend fun <K, V, R> Map<out K, V>.amap(f: suspend (Map.Entry<K, V>) -> R): List<R> =
with(CoroutineScope(GlobalScope.coroutineContext)) {
map { async { f(it) } }.map { it.await() }
}
fun <K, V, R> Map<out K, V>.apmap(f: suspend (Map.Entry<K, V>) -> R): List<R> = runBlocking { fun <K, V, R> Map<out K, V>.apmap(f: suspend (Map.Entry<K, V>) -> R): List<R> = runBlocking {
map { async { f(it) } }.map { it.await() } map { async { f(it) } }.map { it.await() }
} }
@OptIn(DelicateCoroutinesApi::class)
suspend fun <A, B> List<A>.amap(f: suspend (A) -> B): List<B> =
with(CoroutineScope(GlobalScope.coroutineContext)) {
map { async { f(it) } }.map { it.await() }
}
fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking { fun <A, B> List<A>.apmap(f: suspend (A) -> B): List<B> = runBlocking {
map { async { f(it) } }.map { it.await() } map { async { f(it) } }.map { it.await() }
} }
@ -38,6 +52,12 @@ fun <A, B> List<A>.apmapIndexed(f: suspend (index: Int, A) -> B): List<B> = runB
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() } mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
} }
@OptIn(DelicateCoroutinesApi::class)
suspend fun <A, B> List<A>.amapIndexed(f: suspend (index: Int, A) -> B): List<B> =
with(CoroutineScope(GlobalScope.coroutineContext)) {
mapIndexed { index, a -> async { f(index, a) } }.map { it.await() }
}
// run code in parallel // run code in parallel
/*fun <R> argpmap( /*fun <R> argpmap(
vararg transforms: () -> R, vararg transforms: () -> R,

View file

@ -2,10 +2,11 @@ package com.lagradost.cloudstream3.extractors
import android.util.Log import android.util.Log
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class AStreamHub : ExtractorApi() { open class AStreamHub : ExtractorApi() {
override val name = "AStreamHub" override val name = "AStreamHub"
override val mainUrl = "https://astreamhub.com" override val mainUrl = "https://astreamhub.com"
override val requiresReferer = true override val requiresReferer = true

View file

@ -4,7 +4,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
class Acefile : ExtractorApi() { open class Acefile : ExtractorApi() {
override val name = "Acefile" override val name = "Acefile"
override val mainUrl = "https://acefile.co" override val mainUrl = "https://acefile.co"
override val requiresReferer = false override val requiresReferer = false
@ -27,7 +27,6 @@ class Acefile : ExtractorApi() {
res.substringAfter("\"file\":\"").substringBefore("\","), res.substringAfter("\"file\":\"").substringBefore("\","),
"$mainUrl/", "$mainUrl/",
Qualities.Unknown.value, Qualities.Unknown.value,
headers = mapOf("range" to "bytes=0-")
) )
) )
} }

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI import java.net.URI
class AsianLoad : ExtractorApi() { open class AsianLoad : ExtractorApi() {
override var name = "AsianLoad" override var name = "AsianLoad"
override var mainUrl = "https://asianembed.io" override var mainUrl = "https://asianembed.io"
override val requiresReferer = true override val requiresReferer = true

View file

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Blogger : ExtractorApi() { open class Blogger : ExtractorApi() {
override val name = "Blogger" override val name = "Blogger"
override val mainUrl = "https://www.blogger.com" override val mainUrl = "https://www.blogger.com"
override val requiresReferer = false override val requiresReferer = false

View file

@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class BullStream : ExtractorApi() { open class BullStream : ExtractorApi() {
override val name = "BullStream" override val name = "BullStream"
override val mainUrl = "https://bullstream.xyz" override val mainUrl = "https://bullstream.xyz"
override val requiresReferer = false override val requiresReferer = false
@ -18,7 +18,7 @@ class BullStream : ExtractorApi() {
?: return null ?: return null
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}" val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
println("shiv : $m3u8") //println("shiv : $m3u8")
return M3u8Helper.generateM3u8( return M3u8Helper.generateM3u8(
name, name,
m3u8, m3u8,

View file

@ -0,0 +1,23 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.utils.*
open class ByteShare : ExtractorApi() {
override val name = "ByteShare"
override val mainUrl = "https://byteshare.net"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
sources.add(
ExtractorLink(
name,
name,
url.replace("/embed/", "/download/"),
"",
Qualities.Unknown.value,
)
)
return sources
}
}

View file

@ -0,0 +1,97 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import android.util.Log
import java.net.URLDecoder
open class Cda: ExtractorApi() {
override var mainUrl = "https://ebd.cda.pl"
override var name = "Cda"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val mediaId = url
.split("/").last()
.split("?").first()
val doc = app.get("https://ebd.cda.pl/647x500/$mediaId", headers=mapOf(
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
"User-Agent" to USER_AGENT,
"Cookie" to "cda.player=html5"
)).document
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null
return listOf(ExtractorLink(
name,
name,
getFile(playerData.video.file),
referer = "https://ebd.cda.pl/647x500/$mediaId",
quality = Qualities.Unknown.value
))
}
private fun rot13(a: String): String {
return a.map {
when {
it in 'A'..'M' || it in 'a'..'m' -> it + 13
it in 'N'..'Z' || it in 'n'..'z' -> it - 13
else -> it
}
}.joinToString("")
}
private fun cdaUggc(a: String): String {
val decoded = rot13(a)
return if (decoded.endsWith("adc.mp4")) decoded.replace("adc.mp4",".mp4")
else decoded
}
private fun cdaDecrypt(b: String): String {
var a = b
.replace("_XDDD", "")
.replace("_CDA", "")
.replace("_ADC", "")
.replace("_CXD", "")
.replace("_QWE", "")
.replace("_Q5", "")
.replace("_IKSDE", "")
a = URLDecoder.decode(a, "UTF-8")
a = a.map { char ->
if (32 < char.toInt() && char.toInt() < 127) {
return@map String.format("%c", 33 + (char.toInt() + 14) % 94)
} else {
return@map char
}
}.joinToString("")
a = a
.replace(".cda.mp4", "")
.replace(".2cda.pl", ".cda.pl")
.replace(".3cda.pl", ".cda.pl")
return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream")
else "https://${a}.mp4"
}
private fun getFile(a: String) = when {
a.startsWith("uggc") -> cdaUggc(a)
!a.startsWith("http") -> cdaDecrypt(a)
else -> a
}
data class VideoPlayerData(
val file: String,
val qualities: Map<String, String> = mapOf(),
val quality: String?,
val ts: Int?,
val hash2: String?
)
data class PlayerData(
val video: VideoPlayerData
)
}

View file

@ -0,0 +1,105 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.lagradost.cloudstream3.utils.Qualities
import java.net.URL
open class Dailymotion : ExtractorApi() {
override val mainUrl = "https://www.dailymotion.com"
override val name = "Dailymotion"
override val requiresReferer = false
@Suppress("RegExpSimplifiable")
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
// https://www.dailymotion.com/video/k3JAHfletwk94ayCVIu
// https://www.dailymotion.com/embed/video/k3JAHfletwk94ayCVIu
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val embedUrl = getEmbedUrl(url) ?: return
val doc = app.get(embedUrl).document
val prefix = "window.__PLAYER_CONFIG__ = "
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
val id = getVideoId(embedUrl) ?: return
val dmV1st = config.dmInternalData.v1st
val dmTs = config.dmInternalData.ts
val metaDataUrl =
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
val cookies = mapOf(
"v1st" to dmV1st,
"dmvk" to config.context.dmvk,
"ts" to dmTs.toString()
)
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
.parsedSafe<MetaData>() ?: return
metaData.qualities.forEach { (_, video) ->
video.forEach {
getStream(it.url, this.name, callback)
}
}
}
private fun getEmbedUrl(url: String): String? {
if (url.contains("/embed/")) {
return url
}
val vid = getVideoId(url) ?: return null
return "$mainUrl/embed/video/$vid"
}
private fun getVideoId(url: String): String? {
val path = URL(url).path
val id = path.substringAfter("video/")
if (id.matches(videoIdRegex)) {
return id
}
return null
}
private suspend fun getStream(
streamLink: String,
name: String,
callback: (ExtractorLink) -> Unit
) {
return generateM3u8(
name,
streamLink,
"",
).forEach(callback)
}
data class Config(
val context: Context,
val dmInternalData: InternalData
)
data class InternalData(
val ts: Int,
val v1st: String
)
data class Context(
@JsonProperty("access_token") val accessToken: String?,
val dmvk: String,
)
data class MetaData(
val qualities: Map<String, List<VideoLink>>
)
data class VideoLink(
val type: String,
val url: String
)
}

View file

@ -38,6 +38,9 @@ class DoodWsExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.ws" override var mainUrl = "https://dood.ws"
} }
class DoodYtExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.yt"
}
open class DoodLaExtractor : ExtractorApi() { open class DoodLaExtractor : ExtractorApi() {
override var name = "DoodStream" override var name = "DoodStream"

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.httpsify import com.lagradost.cloudstream3.utils.httpsify
class Embedgram : ExtractorApi() { open class Embedgram : ExtractorApi() {
override val name = "Embedgram" override val name = "Embedgram"
override val mainUrl = "https://embedgram.com" override val mainUrl = "https://embedgram.com"
override val requiresReferer = true override val requiresReferer = true

View file

@ -16,26 +16,7 @@ open class Evoload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val lang = url.substring(0, 2) val id = url.replace("https://evoload.io/e/", "") // wanted media id
val flag =
if (lang == "vo") {
" \uD83C\uDDEC\uD83C\uDDE7"
}
else if (lang == "vf"){
" \uD83C\uDDE8\uD83C\uDDF5"
} else {
""
}
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
url.substring(2, url.length)
}
//println(lang)
//println(cleaned_url)
val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars) val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass) val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
@ -44,9 +25,9 @@ open class Evoload : ExtractorApi() {
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,
name + flag, name,
link, link,
cleaned_url, url,
Qualities.Unknown.value, Qualities.Unknown.value,
) )
) )

View file

@ -1,39 +1,54 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8 import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.lagradost.cloudstream3.utils.getAndUnpack
import org.jsoup.nodes.Document
class Fastream: ExtractorApi() { open class Fastream: ExtractorApi() {
override var mainUrl = "https://fastream.to" override var mainUrl = "https://fastream.to"
override var name = "Fastream" override var name = "Fastream"
override val requiresReferer = false override val requiresReferer = false
suspend fun getstream(
response: Document,
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { sources: ArrayList<ExtractorLink>): Boolean{
val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList() response.select("script").amap { script ->
val sources = mutableListOf<ExtractorLink>() if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
val response = app.post("$mainUrl/dl", val unpacked = getAndUnpack(script.data())
data = mapOf( //val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
Pair("op","embed"), val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"")
Pair("file_code",id), //val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach
Pair("auto","1")
)).document
response.select("script").apmap { script ->
if (script.data().contains("sources")) {
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
generateM3u8( generateM3u8(
name, name,
m3u8, newm3u8link,
mainUrl mainUrl
).forEach { link -> ).forEach { link ->
sources.add(link) sources.add(link)
} }
} }
} }
return true
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = ArrayList<ExtractorLink>()
val idregex = Regex("emb.html\\?(.*)=")
if (url.contains(Regex("(emb.html.*fastream)"))) {
val id = idregex.find(url)?.destructured?.component1() ?: ""
val response = app.post("https://fastream.to/dl", allowRedirects = false,
data = mapOf(
"op" to "embed",
"file_code" to id,
"auto" to "1"
)
).document
getstream(response, sources)
}
val response = app.get(url, referer = url).document
getstream(response, sources)
return sources return sources
} }
} }

View file

@ -1,38 +1,57 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
class Filesim : ExtractorApi() {
class Ztreamhub : Filesim() {
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
override val name = "Zstreamhub"
}
class FileMoon : Filesim() {
override val mainUrl = "https://filemoon.to"
override val name = "FileMoon"
}
class FileMoonSx : Filesim() {
override val mainUrl = "https://filemoon.sx"
override val name = "FileMoonSx"
}
open class Filesim : ExtractorApi() {
override val name = "Filesim" override val name = "Filesim"
override val mainUrl = "https://files.im" override val mainUrl = "https://files.im"
override val requiresReferer = false override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(
val sources = mutableListOf<ExtractorLink>() url: String,
with(app.get(url).document) { referer: String?,
this.select("script").map { script -> subtitleCallback: (SubtitleFile) -> Unit,
if (script.data().contains("eval(function(p,a,c,k,e,d)")) { callback: (ExtractorLink) -> Unit
val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]") ) {
tryParseJson<List<ResponseSource>>("[$data]")?.map { val response = app.get(url, referer = mainUrl).document
M3u8Helper.generateM3u8( response.select("script[type=text/javascript]").map { script ->
name, if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
it.file, val unpackedscript = getAndUnpack(script.data())
"$mainUrl/", val m3u8Regex = Regex("file.\\\"(.*?m3u8.*?)\\\"")
).forEach { m3uData -> sources.add(m3uData) } val m3u8 = m3u8Regex.find(unpackedscript)?.destructured?.component1() ?: ""
} if (m3u8.isNotEmpty()) {
generateM3u8(
name,
m3u8,
mainUrl
).forEach(callback)
} }
} }
} }
return sources
} }
private data class ResponseSource( /* private data class ResponseSource(
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
@JsonProperty("type") val type: String?, @JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String? @JsonProperty("label") val label: String?
) ) */
} }

View file

@ -3,10 +3,9 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
class GMPlayer : ExtractorApi() { open class GMPlayer : ExtractorApi() {
override val name = "GM Player" override val name = "GM Player"
override val mainUrl = "https://gmplayer.xyz" override val mainUrl = "https://gmplayer.xyz"
override val requiresReferer = true override val requiresReferer = true

View file

@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.security.DigestException import java.security.DigestException
import java.security.MessageDigest import java.security.MessageDigest
@ -10,43 +11,47 @@ import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec import javax.crypto.spec.SecretKeySpec
class DatabaseGdrive2 : Gdriveplayer() {
override var mainUrl = "https://databasegdriveplayer.co"
}
class DatabaseGdrive : Gdriveplayer() { class DatabaseGdrive : Gdriveplayer() {
override var mainUrl = "https://series.databasegdriveplayer.co" override var mainUrl = "https://series.databasegdriveplayer.co"
} }
class Gdriveplayerapi: Gdriveplayer() { class Gdriveplayerapi : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayerapi.com" override val mainUrl: String = "https://gdriveplayerapi.com"
} }
class Gdriveplayerapp: Gdriveplayer() { class Gdriveplayerapp : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.app" override val mainUrl: String = "https://gdriveplayer.app"
} }
class Gdriveplayerfun: Gdriveplayer() { class Gdriveplayerfun : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.fun" override val mainUrl: String = "https://gdriveplayer.fun"
} }
class Gdriveplayerio: Gdriveplayer() { class Gdriveplayerio : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.io" override val mainUrl: String = "https://gdriveplayer.io"
} }
class Gdriveplayerme: Gdriveplayer() { class Gdriveplayerme : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.me" override val mainUrl: String = "https://gdriveplayer.me"
} }
class Gdriveplayerbiz: Gdriveplayer() { class Gdriveplayerbiz : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.biz" override val mainUrl: String = "https://gdriveplayer.biz"
} }
class Gdriveplayerorg: Gdriveplayer() { class Gdriveplayerorg : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.org" override val mainUrl: String = "https://gdriveplayer.org"
} }
class Gdriveplayerus: Gdriveplayer() { class Gdriveplayerus : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.us" override val mainUrl: String = "https://gdriveplayer.us"
} }
class Gdriveplayerco: Gdriveplayer() { class Gdriveplayerco : Gdriveplayer() {
override val mainUrl: String = "https://gdriveplayer.co" override val mainUrl: String = "https://gdriveplayer.co"
} }
@ -136,6 +141,10 @@ open class Gdriveplayer : ExtractorApi() {
return find(str)?.groupValues?.getOrNull(1) return find(str)?.groupValues?.getOrNull(1)
} }
private fun String.addMarks(str: String): String {
return this.replace(Regex("\"?$str\"?"), "\"$str\"")
}
override suspend fun getUrl( override suspend fun getUrl(
url: String, url: String,
referer: String?, referer: String?,
@ -145,18 +154,19 @@ open class Gdriveplayer : ExtractorApi() {
val document = app.get(url).document val document = app.get(url).document
val eval = unpackJs(document)?.replace("\\", "") ?: return val eval = unpackJs(document)?.replace("\\", "") ?: return
val data = AppUtils.tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+")) ?.split(Regex("\\D+"))
?.joinToString("") { ?.joinToString("") {
Char(it.toInt()).toString() Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() } }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password") ?: throw ErrorLoadingException("can't find password")
val decryptedData = val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
?.substringAfter("sources:[")?.substringBefore("],")
Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(decryptedData ?: return).map { val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
Regex("\"file\":\"(\\S+?)\".*?res=(\\d+)").findAll(sourceData ?: return).map {
it.groupValues[1] to it.groupValues[2] it.groupValues[1] to it.groupValues[2]
}.toList().distinctBy { it.second }.map { (link, quality) -> }.toList().distinctBy { it.second }.map { (link, quality) ->
callback.invoke( callback.invoke(
@ -171,6 +181,17 @@ open class Gdriveplayer : ExtractorApi() {
) )
} }
subData?.addMarks("file")?.addMarks("kind")?.addMarks("label").let { dataSub ->
tryParseJson<List<Tracks>>("[$dataSub]")?.map { sub ->
subtitleCallback.invoke(
SubtitleFile(
sub.label,
httpsify(sub.file)
)
)
}
}
} }
data class AesData( data class AesData(
@ -179,4 +200,10 @@ open class Gdriveplayer : ExtractorApi() {
@JsonProperty("s") val s: String @JsonProperty("s") val s: String
) )
data class Tracks(
@JsonProperty("file") val file: String,
@JsonProperty("kind") val kind: String,
@JsonProperty("label") val label: String
)
} }

View file

@ -1,36 +1,88 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
class Vanfem : GuardareStream() {
override var name = "Vanfem"
override var mainUrl = "https://vanfem.com/"
}
class CineGrabber : GuardareStream() {
override var name = "CineGrabber"
override var mainUrl = "https://cinegrabber.com"
}
open class GuardareStream : ExtractorApi() { open class GuardareStream : ExtractorApi() {
override var name = "Guardare" override var name = "Guardare"
override var mainUrl = "https://guardare.stream" override var mainUrl = "https://guardare.stream"
override val requiresReferer = false override val requiresReferer = false
data class GuardareJsonData ( data class GuardareJsonData(
@JsonProperty("data") val data : List<GuardareData>, @JsonProperty("data") val data: List<GuardareData>,
@JsonProperty("captions") val captions: List<GuardareCaptions?>?,
) )
data class GuardareData ( data class GuardareData(
@JsonProperty("file") val file : String, @JsonProperty("file") val file: String,
@JsonProperty("label") val label : String, @JsonProperty("label") val label: String,
@JsonProperty("type") val type : String @JsonProperty("type") val type: String
) )
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text // https://cinegrabber.com/asset/userdata/224879/caption/gqdmzh-71ez76z8/876438.srt
val jsonvideodata = AppUtils.parseJson<GuardareJsonData>(response) data class GuardareCaptions(
return jsonvideodata.data.map { @JsonProperty("id") val id: String,
ExtractorLink( @JsonProperty("hash") val hash: String,
it.file+".${it.type}", @JsonProperty("language") val language: String?,
this.name, @JsonProperty("extension") val extension: String
it.file+".${it.type}", ) {
mainUrl, fun getUrl(mainUrl: String, userId: String): String {
it.label.filter{ it.isDigit() }.toInt(), return "$mainUrl/asset/userdata/$userId/caption/$hash/$id.$extension"
false }
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val response =
app.post(url.replace("/v/", "/api/source/"), data = mapOf("d" to mainUrl)).text
val jsonVideoData = AppUtils.parseJson<GuardareJsonData>(response)
jsonVideoData.data.forEach {
callback.invoke(
ExtractorLink(
it.file + ".${it.type}",
this.name,
it.file + ".${it.type}",
mainUrl,
it.label.filter { it.isDigit() }.toInt(),
false
)
) )
} }
if (!jsonVideoData.captions.isNullOrEmpty()){
val iframe = app.get(url)
// var USER_ID = '224879';
val userIdRegex = Regex("""USER_ID.*?(\d+)""")
val userId = userIdRegex.find(iframe.text)?.groupValues?.getOrNull(1) ?: return
jsonVideoData.captions.forEach {
if (it == null) return@forEach
val subUrl = it.getUrl(mainUrl, userId)
subtitleCallback.invoke(
SubtitleFile(
it.language ?: "",
subUrl
)
)
}
}
} }
} }

View file

@ -0,0 +1,72 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
open class Jeniusplay : ExtractorApi() {
override val name = "Jeniusplay"
override val mainUrl = "https://jeniusplay.com"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url, referer = "$mainUrl/").document
val hash = url.split("/").last().substringAfter("data=")
val m3uLink = app.post(
url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
data = mapOf("hash" to hash, "r" to "$referer"),
referer = url,
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
).parsed<ResponseSource>().videoSource
M3u8Helper.generateM3u8(
this.name,
m3uLink,
url,
).forEach(callback)
document.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val subData =
getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],")
tryParseJson<List<Tracks>>("[$subData]")?.map { subtitle ->
subtitleCallback.invoke(
SubtitleFile(
getLanguage(subtitle.label ?: ""),
subtitle.file
)
)
}
}
}
}
private fun getLanguage(str: String): String {
return when {
str.contains("indonesia", true) || str
.contains("bahasa", true) -> "Indonesian"
else -> str
}
}
data class ResponseSource(
@JsonProperty("hls") val hls: Boolean,
@JsonProperty("videoSource") val videoSource: String,
@JsonProperty("securedLink") val securedLink: String?,
)
data class Tracks(
@JsonProperty("kind") val kind: String?,
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?,
)
}

View file

@ -1,46 +1,53 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
class Linkbox : ExtractorApi() { open class Linkbox : ExtractorApi() {
override val name = "Linkbox" override val name = "Linkbox"
override val mainUrl = "https://www.linkbox.to" override val mainUrl = "https://www.linkbox.to"
override val requiresReferer = true override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(
val id = url.substringAfter("id=") url: String,
val sources = mutableListOf<ExtractorLink>() referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link -> callback: (ExtractorLink) -> Unit
sources.add( ) {
ExtractorLink( val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
name, app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
name, .parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
link.url, callback.invoke(
url, ExtractorLink(
getQualityFromName(link.resolution) name,
name,
link.url ?: return@map null,
url,
getQualityFromName(link.resolution)
)
) )
) }
}
return sources
} }
data class RList( data class Resolutions(
@JsonProperty("url") val url: String, @JsonProperty("url") val url: String? = null,
@JsonProperty("resolution") val resolution: String?, @JsonProperty("resolution") val resolution: String? = null,
)
data class ItemInfo(
@JsonProperty("resolutionList") val resolutionList: ArrayList<Resolutions>? = arrayListOf(),
) )
data class Data( data class Data(
@JsonProperty("rList") val rList: List<RList>?, @JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
) )
data class Responses( data class Responses(
@JsonProperty("data") val data: Data?, @JsonProperty("data") val data: Data? = null,
) )
} }

View file

@ -1,7 +0,0 @@
package com.lagradost.cloudstream3.extractors
open class Mcloud : WcoStream() {
override var name = "Mcloud"
override var mainUrl = "https://mcloud.to"
override val requiresReferer = true
}

View file

@ -0,0 +1,44 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class MoviehabNet : Moviehab() {
override var mainUrl = "https://play.moviehab.net"
}
open class Moviehab : ExtractorApi() {
override var name = "Moviehab"
override var mainUrl = "https://play.moviehab.com"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val res = app.get(url)
res.document.select("video#player").let {
//should redirect first for making it works
val link = app.get("$mainUrl/${it.select("source").attr("src")}", referer = url).url
M3u8Helper.generateM3u8(
this.name,
link,
url
).forEach(callback)
Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub ->
subtitleCallback.invoke(
SubtitleFile(
it.select("track").attr("label"),
"$mainUrl/$sub"
)
)
}
}
}
}

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack import com.lagradost.cloudstream3.utils.getAndUnpack
class Mp4Upload : ExtractorApi() { open class Mp4Upload : ExtractorApi() {
override var name = "Mp4Upload" override var name = "Mp4Upload"
override var mainUrl = "https://www.mp4upload.com" override var mainUrl = "https://www.mp4upload.com"
private val srcRegex = Regex("""player\.src\("(.*?)"""") private val srcRegex = Regex("""player\.src\("(.*?)"""")

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI import java.net.URI
class MultiQuality : ExtractorApi() { open class MultiQuality : ExtractorApi() {
override var name = "MultiQuality" override var name = "MultiQuality"
override var mainUrl = "https://gogo-play.net" override var mainUrl = "https://gogo-play.net"
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""") private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
class Mvidoo : ExtractorApi() { open class Mvidoo : ExtractorApi() {
override val name = "Mvidoo" override val name = "Mvidoo"
override val mainUrl = "https://mvidoo.com" override val mainUrl = "https://mvidoo.com"
override val requiresReferer = true override val requiresReferer = true

View file

@ -0,0 +1,39 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
data class Okrulinkdata (
@JsonProperty("status" ) var status : String? = null,
@JsonProperty("url" ) var url : String? = null
)
open class Okrulink: ExtractorApi() {
override var mainUrl = "https://okru.link"
override var name = "Okrulink"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
val key = url.substringAfter("html?t=")
val request = app.post("https://apizz.okru.link/decoding", allowRedirects = false,
data = mapOf("video" to key)
).parsedSafe<Okrulinkdata>()
if (request?.url != null) {
sources.add(
ExtractorLink(
name,
name,
request.url!!,
"",
Qualities.Unknown.value,
isM3u8 = false
)
)
}
return sources
}
}

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
@ -14,7 +14,7 @@ import org.jsoup.Jsoup
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc * overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
* If they diverge it'd be better to make them separate. * If they diverge it'd be better to make them separate.
* */ * */
class Pelisplus(val mainUrl: String) { open class Pelisplus(val mainUrl: String) {
val name: String = "Vidstream" val name: String = "Vidstream"
private fun getExtractorUrl(id: String): String { private fun getExtractorUrl(id: String): String {
@ -35,7 +35,7 @@ class Pelisplus(val mainUrl: String) {
callback: (ExtractorLink) -> Unit callback: (ExtractorLink) -> Unit
): Boolean { ): Boolean {
try { try {
normalApis.apmap { api -> normalApis.amap { api ->
val url = api.getExtractorUrl(id) val url = api.getExtractorUrl(id)
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback) api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
} }
@ -51,8 +51,8 @@ class Pelisplus(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P") val qualityRegex = Regex("(\\d+)P")
//a[download] //a[download]
pageDoc.select(".dowload > a")?.apmap { element -> pageDoc.select(".dowload > a")?.amap { element ->
val href = element.attr("href") ?: return@apmap val href = element.attr("href") ?: return@amap
val qual = if (element.text() val qual = if (element.text()
.contains("HDP") .contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1() ) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
@ -84,7 +84,7 @@ class Pelisplus(val mainUrl: String) {
//val name = element.text() //val name = element.text()
// Matches vidstream links with extractors // Matches vidstream links with extractors
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api -> extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
if (link.startsWith(api.mainUrl)) { if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
} }

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class PlayLtXyz: ExtractorApi() { open class PlayLtXyz: ExtractorApi() {
override val name: String = "PlayLt" override val name: String = "PlayLt"
override val mainUrl: String = "https://play.playlt.xyz" override val mainUrl: String = "https://play.playlt.xyz"
override val requiresReferer = true override val requiresReferer = true

View file

@ -0,0 +1,28 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
open class Sendvid : ExtractorApi() {
override var name = "Sendvid"
override val mainUrl = "https://sendvid.com"
override val requiresReferer = false
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val doc = app.get(url).document
val urlString = doc.select("head meta[property=og:video:secure_url]").attr("content")
if (urlString.contains("m3u8")) {
generateM3u8(
name,
urlString,
mainUrl,
).forEach(callback)
}
}
}

View file

@ -8,7 +8,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
class Solidfiles : ExtractorApi() { open class Solidfiles : ExtractorApi() {
override val name = "Solidfiles" override val name = "Solidfiles"
override val mainUrl = "https://www.solidfiles.com" override val mainUrl = "https://www.solidfiles.com"
override val requiresReferer = false override val requiresReferer = false

View file

@ -7,7 +7,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class SpeedoStream : ExtractorApi() { class SpeedoStream1 : SpeedoStream() {
override val mainUrl = "https://speedostream.nl"
}
open class SpeedoStream : ExtractorApi() {
override val name = "SpeedoStream" override val name = "SpeedoStream"
override val mainUrl = "https://speedostream.com" override val mainUrl = "https://speedostream.com"
override val requiresReferer = true override val requiresReferer = true

View file

@ -7,6 +7,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class Sbspeed : StreamSB() {
override var name = "Sbspeed"
override var mainUrl = "https://sbspeed.com"
}
class Streamsss : StreamSB() { class Streamsss : StreamSB() {
override var mainUrl = "https://streamsss.net" override var mainUrl = "https://streamsss.net"
} }
@ -72,6 +77,10 @@ class StreamSB10 : StreamSB() {
override var mainUrl = "https://sbplay2.xyz" override var mainUrl = "https://sbplay2.xyz"
} }
class StreamSB11 : StreamSB() {
override var mainUrl = "https://sbbrisk.com"
}
// This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt // This is a modified version of https://github.com/jmir1/aniyomi-extensions/blob/master/src/en/genoanime/src/eu/kanade/tachiyomi/animeextension/en/genoanime/extractors/StreamSBExtractor.kt
// The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE // The following code is under the Apache License 2.0 https://github.com/jmir1/aniyomi-extensions/blob/master/LICENSE
open class StreamSB : ExtractorApi() { open class StreamSB : ExtractorApi() {
@ -93,15 +102,15 @@ open class StreamSB : ExtractorApi() {
} }
data class Subs ( data class Subs (
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String, @JsonProperty("label") val label: String? = null,
) )
data class StreamData ( data class StreamData (
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
@JsonProperty("cdn_img") val cdnImg: String, @JsonProperty("cdn_img") val cdnImg: String,
@JsonProperty("hash") val hash: String, @JsonProperty("hash") val hash: String,
@JsonProperty("subs") val subs: List<Subs>?, @JsonProperty("subs") val subs: ArrayList<Subs>? = arrayListOf(),
@JsonProperty("length") val length: String, @JsonProperty("length") val length: String,
@JsonProperty("id") val id: String, @JsonProperty("id") val id: String,
@JsonProperty("title") val title: String, @JsonProperty("title") val title: String,
@ -125,7 +134,7 @@ open class StreamSB : ExtractorApi() {
it.value.replace(Regex("(embed-|/e/)"), "") it.value.replace(Regex("(embed-|/e/)"), "")
}.first() }.first()
// val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362" // val master = "$mainUrl/sources48/6d6144797752744a454267617c7c${bytesToHex.lowercase()}7c7c4e61755a56456f34385243727c7c73747265616d7362/6b4a33767968506e4e71374f7c7c343837323439333133333462353935333633373836643638376337633462333634663539343137373761333635313533333835333763376333393636363133393635366136323733343435323332376137633763373337343732363536313664373336327c7c504d754478413835306633797c7c73747265616d7362"
val master = "$mainUrl/sources48/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/" val master = "$mainUrl/sources15/" + bytesToHex("||$id||||streamsb".toByteArray()) + "/"
val headers = mapOf( val headers = mapOf(
"watchsb" to "sbstream", "watchsb" to "sbstream",
) )
@ -141,5 +150,14 @@ open class StreamSB : ExtractorApi() {
url, url,
headers = headers headers = headers
).forEach(callback) ).forEach(callback)
mapped.streamData.subs?.map {sub ->
subtitleCallback.invoke(
SubtitleFile(
sub.label.toString(),
sub.file ?: return@map null,
)
)
}
} }
} }

View file

@ -5,7 +5,15 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
class StreamTape : ExtractorApi() { class StreamTapeNet : StreamTape() {
override var mainUrl = "https://streamtape.net"
}
class ShaveTape : StreamTape(){
override var mainUrl = "https://shavetape.cash"
}
open class StreamTape : ExtractorApi() {
override var name = "StreamTape" override var name = "StreamTape"
override var mainUrl = "https://streamtape.com" override var mainUrl = "https://streamtape.com"
override val requiresReferer = false override val requiresReferer = false
@ -16,7 +24,8 @@ class StreamTape : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
with(app.get(url)) { with(app.get(url)) {
linkRegex.find(this.text)?.let { linkRegex.find(this.text)?.let {
val extractedUrl = "https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3,)}" val extractedUrl =
"https:${it.groups[1]!!.value + it.groups[2]!!.value.substring(3)}"
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,

View file

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

View file

@ -9,7 +9,7 @@ import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import java.net.URI import java.net.URI
class Streamplay : ExtractorApi() { open class Streamplay : ExtractorApi() {
override val name = "Streamplay" override val name = "Streamplay"
override val mainUrl = "https://streamplay.to" override val mainUrl = "https://streamplay.to"
override val requiresReferer = true override val requiresReferer = true

View file

@ -11,7 +11,7 @@ data class Files(
@JsonProperty("label") val label: String? = null, @JsonProperty("label") val label: String? = null,
) )
open class Supervideo : ExtractorApi() { open class Supervideo : ExtractorApi() {
override var name = "Supervideo" override var name = "Supervideo"
override var mainUrl = "https://supervideo.tv" override var mainUrl = "https://supervideo.tv"
override val requiresReferer = false override val requiresReferer = false
@ -20,10 +20,13 @@ data class Files(
val response = app.get(url).text val response = app.get(url).text
val jstounpack = Regex("eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value val jstounpack = Regex("eval((.|\\n)*?)</script>").find(response)?.groups?.get(1)?.value
val unpacjed = JsUnpacker(jstounpack).unpack() val unpacjed = JsUnpacker(jstounpack).unpack()
val extractedUrl = unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString().replace("file",""""file"""").replace("label",""""label"""").substringBeforeLast(",") val extractedUrl =
unpacjed?.let { Regex("""sources:((.|\n)*?)image""").find(it) }?.groups?.get(1)?.value.toString()
.replace("file", """"file"""").replace("label", """"label"""")
.substringBeforeLast(",")
val parsedlinks = parseJson<List<Files>>(extractedUrl) val parsedlinks = parseJson<List<Files>>(extractedUrl)
parsedlinks.forEach { data -> parsedlinks.forEach { data ->
if (data.label.isNullOrBlank()){ // mp4 links (with labels) are slow. Use only m3u8 link. if (data.label.isNullOrBlank()) { // mp4 links (with labels) are slow. Use only m3u8 link.
M3u8Helper.generateM3u8( M3u8Helper.generateM3u8(
name, name,
data.id, data.id,
@ -34,8 +37,6 @@ data class Files(
} }
} }
} }
return extractedLinksList return extractedLinksList
} }
} }

View file

@ -1,41 +1,64 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.kotlin.readValue
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.mapper
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
class Cinestart: Tomatomatela() { class Cinestart: Tomatomatela() {
override var name = "Cinestart" override var name: String = "Cinestart"
override var mainUrl = "https://cinestart.net" override val mainUrl: String = "https://cinestart.net"
override val details = "vr.php?v=" override val details = "vr.php?v="
} }
class TomatomatelalClub: Tomatomatela() {
override var name: String = "Tomatomatela"
override val mainUrl: String = "https://tomatomatela.club"
}
open class Tomatomatela : ExtractorApi() { open class Tomatomatela : ExtractorApi() {
override var name = "Tomatomatela" override var name = "Tomatomatela"
override var mainUrl = "https://tomatomatela.com" override val mainUrl = "https://tomatomatela.com"
override val requiresReferer = false override val requiresReferer = false
private data class Tomato ( private data class Tomato (
@JsonProperty("status") val status: Int, @JsonProperty("status") val status: Int,
@JsonProperty("file") val file: String @JsonProperty("file") val file: String?
) )
open val details = "details.php?v=" open val details = "details.php?v="
open val embeddetails = "/embed.html#"
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val link = url.replace("$mainUrl/embed.html#","$mainUrl/$details") val link = url.replace("$mainUrl$embeddetails","$mainUrl/$details")
val server = app.get(link, allowRedirects = false).text val sources = ArrayList<ExtractorLink>()
val json = parseJson<Tomato>(server) val server = app.get(link, allowRedirects = false,
if (json.status == 200) return listOf( headers = mapOf(
ExtractorLink( "User-Agent" to USER_AGENT,
name, "Accept" to "application/json, text/javascript, */*; q=0.01",
name, "Accept-Language" to "en-US,en;q=0.5",
json.file, "X-Requested-With" to "XMLHttpRequest",
"", "DNT" to "1",
Qualities.Unknown.value, "Connection" to "keep-alive",
isM3u8 = false "Sec-Fetch-Dest" to "empty",
"Sec-Fetch-Mode" to "cors",
"Sec-Fetch-Site" to "same-origin"
) )
) ).parsedSafe<Tomato>()
return null if (server?.file != null) {
sources.add(
ExtractorLink(
name,
name,
server.file,
"",
Qualities.Unknown.value,
isM3u8 = false
)
)
}
return sources
} }
} }

View file

@ -6,7 +6,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class UpstreamExtractor : ExtractorApi() { open class UpstreamExtractor : ExtractorApi() {
override val name: String = "Upstream" override val name: String = "Upstream"
override val mainUrl: String = "https://upstream.to" override val mainUrl: String = "https://upstream.to"
override val requiresReferer = true override val requiresReferer = true

View file

@ -7,6 +7,10 @@ class Uqload1 : Uqload() {
override var mainUrl = "https://uqload.com" override var mainUrl = "https://uqload.com"
} }
class Uqload2 : Uqload() {
override var mainUrl = "https://uqload.co"
}
open class Uqload : ExtractorApi() { open class Uqload : ExtractorApi() {
override val name: String = "Uqload" override val name: String = "Uqload"
override val mainUrl: String = "https://www.uqload.com" override val mainUrl: String = "https://www.uqload.com"
@ -15,30 +19,14 @@ open class Uqload : ExtractorApi() {
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val lang = url.substring(0, 2) with(app.get(url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
val flag =
if (lang == "vo") {
" \uD83C\uDDEC\uD83C\uDDE7"
}
else if (lang == "vf"){
" \uD83C\uDDE8\uD83C\uDDF5"
} else {
""
}
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
url
} else {
url.substring(2, url.length)
}
with(app.get(cleaned_url)) { // raised error ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED (3003) is due to the response: "error_nofile"
srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link -> srcRegex.find(this.text)?.groupValues?.get(1)?.replace("\"", "")?.let { link ->
return listOf( return listOf(
ExtractorLink( ExtractorLink(
name, name,
name + flag, name,
link, link,
cleaned_url, url,
Qualities.Unknown.value, Qualities.Unknown.value,
) )
) )

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -59,8 +59,8 @@ open class VidSrcExtractor : ExtractorApi() {
if (datahash.isNotBlank()) { if (datahash.isNotBlank()) {
val links = try { val links = try {
app.get( app.get(
"$absoluteUrl/src/$datahash", "$absoluteUrl/srcrcp/$datahash",
referer = "https://source.vidsrc.me/" referer = "https://rcp.vidsrc.me/"
).url ).url
} catch (e: Exception) { } catch (e: Exception) {
"" ""
@ -69,12 +69,12 @@ open class VidSrcExtractor : ExtractorApi() {
} else "" } else ""
} }
serverslist.apmap { server -> serverslist.amap { server ->
val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/") val linkfixed = server.replace("https://vidsrc.xyz/", "https://embedsito.com/")
if (linkfixed.contains("/pro")) { if (linkfixed.contains("/prorcp")) {
val srcresponse = app.get(server, referer = absoluteUrl).text val srcresponse = app.get(server, referer = absoluteUrl).text
val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)") val m3u8Regex = Regex("((https:|http:)//.*\\.m3u8)")
val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@apmap val srcm3u8 = m3u8Regex.find(srcresponse)?.value ?: return@amap
val passRegex = Regex("""['"](.*set_pass[^"']*)""") val passRegex = Regex("""['"](.*set_pass[^"']*)""")
val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace( val pass = passRegex.find(srcresponse)?.groupValues?.get(1)?.replace(
Regex("""^//"""), "https://" Regex("""^//"""), "https://"
@ -85,18 +85,12 @@ open class VidSrcExtractor : ExtractorApi() {
this.name, this.name,
this.name, this.name,
srcm3u8, srcm3u8,
this.mainUrl, "https://vidsrc.stream/",
Qualities.Unknown.value, Qualities.Unknown.value,
extractorData = pass, extractorData = pass,
isM3u8 = true isM3u8 = true
) )
) )
// M3u8Helper.generateM3u8(
// name,
// srcm3u8,
// absoluteUrl
// ).forEach(callback)
} else { } else {
loadExtractor(linkfixed, url, subtitleCallback, callback) loadExtractor(linkfixed, url, subtitleCallback, callback)
} }

View file

@ -11,7 +11,7 @@ class VideovardSX : WcoStream() {
override var mainUrl = "https://videovard.sx" override var mainUrl = "https://videovard.sx"
} }
class VideoVard : ExtractorApi() { open class VideoVard : ExtractorApi() {
override var name = "Videovard" // Cause works for animekisa and wco override var name = "Videovard" // Cause works for animekisa and wco
override var mainUrl = "https://videovard.to" override var mainUrl = "https://videovard.to"
override val requiresReferer = false override val requiresReferer = false

View file

@ -0,0 +1,69 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Vidmolyme : Vidmoly() {
override val mainUrl = "https://vidmoly.me"
}
open class Vidmoly : ExtractorApi() {
override val name = "Vidmoly"
override val mainUrl = "https://vidmoly.to"
override val requiresReferer = true
private fun String.addMarks(str: String): String {
return this.replace(Regex("\"?$str\"?"), "\"$str\"")
}
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val script = app.get(
url,
referer = referer,
).document.select("script")
.find { it.data().contains("sources:") }?.data()
val videoData = script?.substringAfter("sources: [")
?.substringBefore("],")?.addMarks("file")
val subData = script?.substringAfter("tracks: [")?.substringBefore("]")?.addMarks("file")
?.addMarks("label")?.addMarks("kind")
tryParseJson<Source>(videoData)?.file?.let { m3uLink ->
M3u8Helper.generateM3u8(
name,
m3uLink,
"$mainUrl/"
).forEach(callback)
}
tryParseJson<List<SubSource>>("[${subData}]")
?.filter { it.kind == "captions" }?.map {
subtitleCallback.invoke(
SubtitleFile(
it.label.toString(),
fixUrl(it.file.toString())
)
)
}
}
private data class Source(
@JsonProperty("file") val file: String? = null,
)
private data class SubSource(
@JsonProperty("file") val file: String? = null,
@JsonProperty("label") val label: String? = null,
@JsonProperty("kind") val kind: String? = null,
)
}

View file

@ -0,0 +1,34 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack
class Vido : ExtractorApi() {
override var name = "Vido"
override var mainUrl = "https://vido.lol"
private val srcRegex = Regex("""sources:\s*\["(.*?)"\]""")
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val methode = app.get(url.replace("/e/", "/embed-")) // fix wiflix and mesfilms
with(methode) {
if (!methode.isSuccessful) return null
//val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
srcRegex.find(this.text)?.groupValues?.get(1)?.let { link ->
return listOf(
ExtractorLink(
name,
name,
link,
url,
Qualities.Unknown.value,
true,
)
)
}
}
return null
}
}

View file

@ -1,7 +1,7 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.argamap import com.lagradost.cloudstream3.argamap
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
@ -37,7 +37,7 @@ class Vidstream(val mainUrl: String) {
val extractorUrl = getExtractorUrl(id) val extractorUrl = getExtractorUrl(id)
argamap( argamap(
{ {
normalApis.apmap { api -> normalApis.amap { api ->
val url = api.getExtractorUrl(id) val url = api.getExtractorUrl(id)
api.getSafeUrl( api.getSafeUrl(
url, url,
@ -55,8 +55,8 @@ class Vidstream(val mainUrl: String) {
val qualityRegex = Regex("(\\d+)P") val qualityRegex = Regex("(\\d+)P")
//a[download] //a[download]
pageDoc.select(".dowload > a")?.apmap { element -> pageDoc.select(".dowload > a")?.amap { element ->
val href = element.attr("href") ?: return@apmap val href = element.attr("href") ?: return@amap
val qual = if (element.text() val qual = if (element.text()
.contains("HDP") .contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1() ) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
@ -87,7 +87,7 @@ class Vidstream(val mainUrl: String) {
//val name = element.text() //val name = element.text()
// Matches vidstream links with extractors // Matches vidstream links with extractors
extractorApis.filter { !it.requiresReferer || !isCasting }.apmap { api -> extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
if (link.startsWith(api.mainUrl)) { if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback) api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
} }

View file

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

View file

@ -13,39 +13,42 @@ open class VoeExtractor : ExtractorApi() {
override val requiresReferer = false override val requiresReferer = false
private data class ResponseLinks( private data class ResponseLinks(
@JsonProperty("hls") val url: String?, @JsonProperty("hls") val hls: String?,
@JsonProperty("mp4") val mp4: String?,
@JsonProperty("video_height") val label: Int? @JsonProperty("video_height") val label: Int?
//val type: String // Mp4 //val type: String // Mp4
) )
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf() val html = app.get(url).text
val doc = app.get(url).text if (html.isNotBlank()) {
if (doc.isNotBlank()) { val src = html.substringAfter("const sources =").substringBefore(";")
val start = "const sources =" // Remove last comma, it is not proper json otherwise
var src = doc.substring(doc.indexOf(start))
src = src.substring(start.length, src.indexOf(";"))
.replace("0,", "0") .replace("0,", "0")
.trim() // Make json use the proper quotes
.replace("'", "\"")
//Log.i(this.name, "Result => (src) ${src}") //Log.i(this.name, "Result => (src) ${src}")
parseJson<ResponseLinks?>(src)?.let { voelink -> parseJson<ResponseLinks?>(src)?.let { voeLink ->
//Log.i(this.name, "Result => (voelink) ${voelink}") //Log.i(this.name, "Result => (voeLink) ${voeLink}")
val linkUrl = voelink.url
val linkLabel = voelink.label?.toString() ?: "" // Always defaults to the hls link, but returns the mp4 if null
val linkUrl = voeLink.hls ?: voeLink.mp4
val linkLabel = voeLink.label?.toString() ?: ""
if (!linkUrl.isNullOrEmpty()) { if (!linkUrl.isNullOrEmpty()) {
extractedLinksList.add( return listOf(
ExtractorLink( ExtractorLink(
name = this.name, name = this.name,
source = this.name, source = this.name,
url = linkUrl, url = linkUrl,
quality = getQualityFromName(linkLabel), quality = getQualityFromName(linkLabel),
referer = url, referer = url,
isM3u8 = true isM3u8 = voeLink.hls != null
) )
) )
} }
} }
} }
return extractedLinksList return emptyList()
} }
} }

View file

@ -53,6 +53,12 @@ class VizcloudSite : WcoStream() {
override var mainUrl = "https://vizcloud.site" override var mainUrl = "https://vizcloud.site"
} }
class Mcloud : WcoStream() {
override var name = "Mcloud"
override var mainUrl = "https://mcloud.to"
override val requiresReferer = true
}
open class WcoStream : ExtractorApi() { open class WcoStream : ExtractorApi() {
override var name = "VidStream" // Cause works for animekisa and wco override var name = "VidStream" // Cause works for animekisa and wco
override var mainUrl = "https://vidstream.pro" override var mainUrl = "https://vidstream.pro"

View file

@ -1,12 +1,23 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
class Cdnplayer: XStreamCdn() {
override val name: String = "Cdnplayer"
override val mainUrl: String = "https://cdnplayer.online"
}
class Kotakajair: XStreamCdn() {
override val name: String = "Kotakajair"
override val mainUrl: String = "https://kotakajair.xyz"
}
class FEnet: XStreamCdn() { class FEnet: XStreamCdn() {
override val name: String = "FEnet" override val name: String = "FEnet"
override val mainUrl: String = "https://fembed.net" override val mainUrl: String = "https://fembed.net"
@ -59,44 +70,67 @@ open class XStreamCdn : ExtractorApi() {
//val type: String // Mp4 //val type: String // Mp4
) )
private data class Player(
@JsonProperty("poster_file") val poster_file: String? = null,
)
private data class ResponseJson( private data class ResponseJson(
@JsonProperty("success") val success: Boolean, @JsonProperty("success") val success: Boolean,
@JsonProperty("data") val data: List<ResponseData>? @JsonProperty("player") val player: Player? = null,
@JsonProperty("data") val data: List<ResponseData>?,
@JsonProperty("captions") val captions: List<Captions?>?,
)
private data class Captions(
@JsonProperty("id") val id: String,
@JsonProperty("hash") val hash: String,
@JsonProperty("language") val language: String,
@JsonProperty("extension") val extension: String
) )
override fun getExtractorUrl(id: String): String { override fun getExtractorUrl(id: String): String {
return "$domainUrl/api/source/$id" return "$domainUrl/api/source/$id"
} }
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> { override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val headers = mapOf( val headers = mapOf(
"Referer" to url, "Referer" to url,
"User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0", "User-Agent" to "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
) )
val id = url.trimEnd('/').split("/").last() val id = url.trimEnd('/').split("/").last()
val newUrl = "https://${domainUrl}/api/source/${id}" val newUrl = "https://${domainUrl}/api/source/${id}"
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf() app.post(newUrl, headers = headers).let { res ->
with(app.post(newUrl, headers = headers)) { val sources = tryParseJson<ResponseJson?>(res.text)
if (this.code != 200) return listOf() sources?.let {
val text = this.text
if (text.isEmpty()) return listOf()
if (text == """{"success":false,"data":"Video not found or has been removed"}""") return listOf()
AppUtils.parseJson<ResponseJson?>(text)?.let {
if (it.success && it.data != null) { if (it.success && it.data != null) {
it.data.forEach { data -> it.data.map { source ->
extractedLinksList.add( callback.invoke(
ExtractorLink( ExtractorLink(
name, name,
name = name, name = name,
data.file, source.file,
url, url,
getQualityFromName(data.label), getQualityFromName(source.label),
) )
) )
} }
} }
} }
val userData = sources?.player?.poster_file?.split("/")?.get(2)
sources?.captions?.map {
subtitleCallback.invoke(
SubtitleFile(
it?.language.toString(),
"$mainUrl/asset/userdata/$userData/caption/${it?.hash}/${it?.id}.${it?.extension}"
)
)
}
} }
return extractedLinksList
} }
} }

View file

@ -7,7 +7,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName import com.lagradost.cloudstream3.utils.getQualityFromName
class YourUpload: ExtractorApi() { open class YourUpload: ExtractorApi() {
override val name = "Yourupload" override val name = "Yourupload"
override val mainUrl = "https://www.yourupload.com" override val mainUrl = "https://www.yourupload.com"
override val requiresReferer = false override val requiresReferer = false

View file

@ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper import com.lagradost.cloudstream3.utils.M3u8Helper
class Zorofile : ExtractorApi() { open class Zorofile : ExtractorApi() {
override val name = "Zorofile" override val name = "Zorofile"
override val mainUrl = "https://zorofile.com" override val mainUrl = "https://zorofile.com"
override val requiresReferer = true override val requiresReferer = true

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
@ -36,7 +36,7 @@ open class ZplayerV2 : ExtractorApi() {
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)") val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
m3u8regex.findAll(testdata).map { m3u8regex.findAll(testdata).map {
it.value it.value
}.toList().apmap { urlm3u8 -> }.toList().amap { urlm3u8 ->
if (urlm3u8.contains("m3u8")) { if (urlm3u8.contains("m3u8")) {
val testurl = app.get(urlm3u8, headers = mapOf("Referer" to url)).text val testurl = app.get(urlm3u8, headers = mapOf("Referer" to url)).text
if (testurl.contains("EXTM3U")) { if (testurl.contains("EXTM3U")) {

View file

@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.extractors.helper
import android.util.Log import android.util.Log
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.loadExtractor import com.lagradost.cloudstream3.utils.loadExtractor
@ -18,7 +18,7 @@ class AsianEmbedHelper {
val doc = app.get(url).document val doc = app.get(url).document
val links = doc.select("div#list-server-more > ul > li.linkserver") val links = doc.select("div#list-server-more > ul > li.linkserver")
if (!links.isNullOrEmpty()) { if (!links.isNullOrEmpty()) {
links.apmap { links.amap {
val datavid = it.attr("data-video") ?: "" val datavid = it.attr("data-video") ?: ""
//Log.i("AsianEmbed", "Result => (datavid) ${datavid}") //Log.i("AsianEmbed", "Result => (datavid) ${datavid}")
if (datavid.isNotBlank()) { if (datavid.isNotBlank()) {

View file

@ -1,30 +0,0 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.malApi
import com.lagradost.cloudstream3.utils.SyncUtil
object SyncRedirector {
val syncApis = SyncApis
suspend fun redirect(url: String, preferredUrl: String): String {
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
return SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(preferredUrl)
} ?: run {
throw ErrorLoadingException("Page does not exist on $preferredUrl")
}
}
}
return url
}
}

View file

@ -39,7 +39,7 @@ class CrossTmdbProvider : TmdbProvider() {
): Boolean { ): Boolean {
tryParseJson<CrossMetaData>(data)?.let { metaData -> tryParseJson<CrossMetaData>(data)?.let { metaData ->
if (!metaData.isSuccess) return false if (!metaData.isSuccess) return false
metaData.movies?.apmap { (apiName, data) -> metaData.movies?.amap { (apiName, data) ->
getApiFromNameNull(apiName)?.let { getApiFromNameNull(apiName)?.let {
try { try {
it.loadLinks(data, isCasting, subtitleCallback, callback) it.loadLinks(data, isCasting, subtitleCallback, callback)
@ -64,10 +64,10 @@ class CrossTmdbProvider : TmdbProvider() {
val matchName = filterName(this.name) val matchName = filterName(this.name)
when (this) { when (this) {
is MovieLoadResponse -> { is MovieLoadResponse -> {
val data = validApis.apmap { api -> val data = validApis.amap { api ->
try { try {
if (api.supportedTypes.contains(TvType.Movie)) { //|| api.supportedTypes.contains(TvType.AnimeMovie) if (api.supportedTypes.contains(TvType.Movie)) { //|| api.supportedTypes.contains(TvType.AnimeMovie)
return@apmap api.search(this.name)?.first { return@amap api.search(this.name)?.first {
if (filterName(it.name).equals( if (filterName(it.name).equals(
matchName, matchName,
ignoreCase = true ignoreCase = true

View file

@ -45,7 +45,7 @@ class MultiAnimeProvider : MainAPI() {
override suspend fun load(url: String): LoadResponse? { override suspend fun load(url: String): LoadResponse? {
return syncApi.getResult(url)?.let { res -> return syncApi.getResult(url)?.let { res ->
val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).apmap { url -> val data = SyncUtil.getUrlsFromId(res.id, syncUtilType).amap { url ->
validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url) validApis.firstOrNull { api -> url.startsWith(api.mainUrl) }?.load(url)
}.filterNotNull() }.filterNotNull()

View file

@ -0,0 +1,56 @@
package com.lagradost.cloudstream3.metaproviders
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncIdName
object SyncRedirector {
val syncApis = SyncApis
private val syncIds =
listOf(
SyncIdName.MyAnimeList to Regex("""myanimelist\.net\/anime\/(\d+)"""),
SyncIdName.Anilist to Regex("""anilist\.co\/anime\/(\d+)""")
)
suspend fun redirect(
url: String,
providerApi: MainAPI
): String {
// Deprecated since providers should do this instead!
// Tries built in ID -> ProviderUrl
/*
for (api in syncApis) {
if (url.contains(api.mainUrl)) {
val otherApi = when (api.name) {
aniListApi.name -> "anilist"
malApi.name -> "myanimelist"
else -> return url
}
SyncUtil.getUrlsFromId(api.getIdFromUrl(url), otherApi).firstOrNull { realUrl ->
realUrl.contains(providerApi.mainUrl)
}?.let {
return it
}
// ?: run {
// throw ErrorLoadingException("Page does not exist on $preferredUrl")
// }
}
}
*/
// Tries provider solution
// This goes through all sync ids and finds supported id by said provider
return syncIds.firstNotNullOfOrNull { (syncName, syncRegex) ->
if (providerApi.supportedSyncNames.contains(syncName)) {
syncRegex.find(url)?.value?.let {
suspendSafeApiCall {
providerApi.getLoadUrl(syncName, it)
}
}
} else null
} ?: url
}
}

View file

@ -53,6 +53,10 @@ fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.observe(this) { it?.let { t -> action(t) } } liveData.observe(this) { it?.let { t -> action(t) } }
} }
fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.observe(this) { action(it) }
}
inline fun <reified T : Any> some(value: T?): Some<T> { inline fun <reified T : Any> some(value: T?): Some<T> {
return if (value == null) { return if (value == null) {
Some.None Some.None
@ -117,13 +121,21 @@ suspend fun <T> suspendSafeApiCall(apiCall: suspend () -> T): T? {
} }
} }
fun Throwable.getAllMessages(): String {
return (this.localizedMessage ?: "") + (this.cause?.getAllMessages()?.let { "\n$it" } ?: "")
}
fun Throwable.getStackTracePretty(showMessage: Boolean = true): String {
val prefix = if (showMessage) this.localizedMessage?.let { "\n$it" } ?: "" else ""
return prefix + this.stackTrace.joinToString(
separator = "\n"
) {
"${it.fileName} ${it.lineNumber}"
}
}
fun <T> safeFail(throwable: Throwable): Resource<T> { fun <T> safeFail(throwable: Throwable): Resource<T> {
val stackTraceMsg = val stackTraceMsg = throwable.getStackTracePretty()
(throwable.localizedMessage ?: "") + "\n\n" + throwable.stackTrace.joinToString(
separator = "\n"
) {
"${it.fileName} ${it.lineNumber}"
}
return Resource.Failure(false, null, null, stackTraceMsg) return Resource.Failure(false, null, null, stackTraceMsg)
} }

View file

@ -5,6 +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.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
@ -26,7 +27,10 @@ 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.
CookieManager.getInstance().removeAllCookies(null) normalSafeApiCall {
// This can throw an exception on unsupported devices :(
CookieManager.getInstance().removeAllCookies(null)
}
} }
val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf() val savedCookies: MutableMap<String, Map<String, String>> = mutableMapOf()
@ -35,7 +39,7 @@ class CloudflareKiller : Interceptor {
* Gets the headers with cookies, webview user agent included! * Gets the headers with cookies, webview user agent included!
* */ * */
fun getCookieHeaders(url: String): Headers { fun getCookieHeaders(url: String): Headers {
val userAgentHeaders = WebViewResolver.webViewUserAgent?.let { val userAgentHeaders = WebViewResolver.webViewUserAgent?.let {
mapOf("user-agent" to it) mapOf("user-agent" to it)
} ?: emptyMap() } ?: emptyMap()
@ -60,7 +64,9 @@ class CloudflareKiller : Interceptor {
} }
private fun getWebViewCookie(url: String): String? { private fun getWebViewCookie(url: String): String? {
return CookieManager.getInstance()?.getCookie(url) return normalSafeApiCall {
CookieManager.getInstance()?.getCookie(url)
}
} }
/** /**

View file

@ -2,7 +2,7 @@ package com.lagradost.cloudstream3.network
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.nicehttp.Requests.Companion.await import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.cookies import com.lagradost.nicehttp.cookies
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor import okhttp3.Interceptor
@ -41,7 +41,8 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
savedCookiesMap[request.url.host] savedCookiesMap[request.url.host]
// If no cookies are found fetch and save em. // If no cookies are found fetch and save em.
?: (request.url.scheme + "://" + request.url.host + (ddosBypassPath ?: "")).let { ?: (request.url.scheme + "://" + request.url.host + (ddosBypassPath ?: "")).let {
app.get(it, cacheTime = 0).cookies.also { cookies -> // Somehow app.get fails
Requests().get(it).cookies.also { cookies ->
savedCookiesMap[request.url.host] = cookies savedCookiesMap[request.url.host] = cookies
} }
} }
@ -51,6 +52,6 @@ class DdosGuardKiller(private val alwaysBypass: Boolean) : Interceptor {
request.newBuilder() request.newBuilder()
.headers(headers) .headers(headers)
.build() .build()
).await() ).execute()
} }
} }

View file

@ -4,16 +4,19 @@ import android.content.Context
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
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.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ignoreAllSSLErrors import com.lagradost.nicehttp.ignoreAllSSLErrors
import okhttp3.Cache import okhttp3.Cache
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Headers.Companion.toHeaders import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.conscrypt.Conscrypt
import java.io.File import java.io.File
import java.security.Security
fun Requests.initClient(context: Context): OkHttpClient { fun Requests.initClient(context: Context): OkHttpClient {
normalSafeApiCall { 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)
baseClient = OkHttpClient.Builder() baseClient = OkHttpClient.Builder()

View file

@ -7,9 +7,12 @@ import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.mainWork
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.nicehttp.requestCreator import com.lagradost.nicehttp.requestCreator
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -64,9 +67,15 @@ class WebViewResolver(
method: String = "GET", method: String = "GET",
requestCallBack: (Request) -> Boolean = { false }, requestCallBack: (Request) -> Boolean = { false },
): Pair<Request?, List<Request>> { ): Pair<Request?, List<Request>> {
return resolveUsingWebView( return try {
requestCreator(method, url, referer = referer), requestCallBack resolveUsingWebView(
) requestCreator(method, url, referer = referer), requestCallBack
)
} catch (e: java.lang.IllegalArgumentException) {
logError(e)
debugException { "ILLEGAL URL IN resolveUsingWebView!" }
return null to emptyList()
}
} }
/** /**
@ -96,7 +105,7 @@ class WebViewResolver(
} }
var fixedRequest: Request? = null var fixedRequest: Request? = null
val extraRequestList = mutableListOf<Request>() val extraRequestList = threadSafeListOf<Request>()
main { main {
// Useful for debugging // Useful for debugging
@ -128,7 +137,7 @@ class WebViewResolver(
println("Loading WebView URL: $webViewUrl") println("Loading WebView URL: $webViewUrl")
if (interceptUrl.containsMatchIn(webViewUrl)) { if (interceptUrl.containsMatchIn(webViewUrl)) {
fixedRequest = request.toRequest().also { fixedRequest = request.toRequest()?.also {
requestCallBack(it) requestCallBack(it)
} }
println("Web-view request finished: $webViewUrl") println("Web-view request finished: $webViewUrl")
@ -137,9 +146,9 @@ class WebViewResolver(
} }
if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) { if (additionalUrls.any { it.containsMatchIn(webViewUrl) }) {
extraRequestList.add(request.toRequest().also { request.toRequest()?.also {
if (requestCallBack(it)) destroyWebView() if (requestCallBack(it)) destroyWebView()
}) }?.let(extraRequestList::add)
} }
// Suppress image requests as we don't display them anywhere // Suppress image requests as we don't display them anywhere
@ -250,14 +259,19 @@ class WebViewResolver(
} }
fun WebResourceRequest.toRequest(): Request { fun WebResourceRequest.toRequest(): Request? {
val webViewUrl = this.url.toString() val webViewUrl = this.url.toString()
return requestCreator( // If invalid url then it can crash with
this.method, // java.lang.IllegalArgumentException: Expected URL scheme 'http' or 'https' but was 'data'
webViewUrl, // At Request.Builder().url(addParamsToUrl(url, params))
this.requestHeaders, return normalSafeApiCall {
) requestCreator(
this.method,
webViewUrl,
this.requestHeaders,
)
}
} }
fun Response.toWebResourceResponse(): WebResourceResponse { fun Response.toWebResourceResponse(): WebResourceResponse {

View file

@ -1,40 +1,45 @@
package com.lagradost.cloudstream3.plugins package com.lagradost.cloudstream3.plugins
import android.app.* import android.app.*
import dalvik.system.PathClassLoader import android.content.Context
import com.google.gson.Gson
import android.content.res.AssetManager import android.content.res.AssetManager
import android.content.res.Resources import android.content.res.Resources
import android.os.Environment
import android.widget.Toast
import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.Gson
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
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.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins import com.lagradost.cloudstream3.MainAPI.Companion.settingsForProvider
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
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.normalSafeApiCall
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.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.main 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.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.utils.extractorApis import com.lagradost.cloudstream3.utils.extractorApis
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 java.io.File import java.io.File
@ -140,8 +145,10 @@ object PluginManager {
return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray() return getKey(PLUGINS_KEY_LOCAL) ?: emptyArray()
} }
private val LOCAL_PLUGINS_PATH = private val CLOUD_STREAM_FOLDER =
Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/plugins" Environment.getExternalStorageDirectory().absolutePath + "/Cloudstream3/"
private val LOCAL_PLUGINS_PATH = CLOUD_STREAM_FOLDER + "plugins"
public var currentlyLoading: String? = null public var currentlyLoading: String? = null
@ -159,11 +166,11 @@ object PluginManager {
private var loadedLocalPlugins = false private var loadedLocalPlugins = false
private val gson = Gson() private val gson = Gson()
private suspend fun maybeLoadPlugin(activity: Activity, file: File) { private suspend fun maybeLoadPlugin(context: Context, file: File) {
val name = file.name val name = file.name
if (file.extension == "zip" || file.extension == "cs3") { if (file.extension == "zip" || file.extension == "cs3") {
loadPlugin( loadPlugin(
activity, context,
file, file,
PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET) PluginData(name, null, false, file.absolutePath, PLUGIN_VERSION_NOT_SET)
) )
@ -193,7 +200,7 @@ object PluginManager {
// var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet() // var allCurrentOutDatedPlugins: Set<OnlinePluginData> = emptySet()
suspend fun loadSinglePlugin(activity: Activity, apiName: String): Boolean { suspend fun loadSinglePlugin(context: Context, apiName: String): Boolean {
return (getPluginsOnline().firstOrNull { return (getPluginsOnline().firstOrNull {
// Most of the time the provider ends with Provider which isn't part of the api name // Most of the time the provider ends with Provider which isn't part of the api name
it.internalName.replace("provider", "", ignoreCase = true) == apiName it.internalName.replace("provider", "", ignoreCase = true) == apiName
@ -203,7 +210,7 @@ object PluginManager {
})?.let { savedData -> })?.let { savedData ->
// OnlinePluginData(savedData, onlineData) // OnlinePluginData(savedData, onlineData)
loadPlugin( loadPlugin(
activity, context,
File(savedData.filePath), File(savedData.filePath),
savedData savedData
) )
@ -220,10 +227,7 @@ object PluginManager {
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) { fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
// Load all plugins as fast as possible! // Load all plugins as fast as possible!
loadAllOnlinePlugins(activity) loadAllOnlinePlugins(activity)
afterPluginsLoadedEvent.invoke(false)
ioSafe {
afterPluginsLoadedEvent.invoke(true)
}
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY) val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES ?: emptyArray()) + PREBUILT_REPOSITORIES
@ -254,11 +258,12 @@ object PluginManager {
//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) {
downloadAndLoadPlugin( downloadPlugin(
activity, activity,
pluginData.onlineData.second.url, pluginData.onlineData.second.url,
pluginData.savedData.internalName, pluginData.savedData.internalName,
File(pluginData.savedData.filePath) File(pluginData.savedData.filePath),
true
).let { success -> ).let { success ->
if (success) if (success)
updatedPlugins.add(pluginData.onlineData.second.name) updatedPlugins.add(pluginData.onlineData.second.name)
@ -267,31 +272,134 @@ object PluginManager {
} }
main { main {
createNotification(activity, updatedPlugins) val uitext = txt(R.string.plugins_updated, updatedPlugins.size)
createNotification(activity, uitext, updatedPlugins)
} }
ioSafe { // ioSafe {
afterPluginsLoadedEvent.invoke(true) afterPluginsLoadedEvent.invoke(false)
} // }
Log.i(TAG, "Plugin update done!") Log.i(TAG, "Plugin update done!")
} }
/**
* Automatically download plugins not yet existing on local
* 1. Gets all online data from online plugins repo
* 2. Fetch all not downloaded plugins
* 3. Download them and reload plugins
**/
fun downloadNotExistingPluginsAndLoad(activity: Activity) {
val newDownloadPlugins = mutableListOf<String>()
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url }
val providerLang = activity.getApiProviderLangSettings()
//Log.i(TAG, "providerLang => ${providerLang.toJson()}")
// Iterate online repos and returns not downloaded plugins
val notDownloadedPlugins = onlinePlugins.mapNotNull { onlineData ->
val sitePlugin = onlineData.second
//Don't include empty urls
if (sitePlugin.url.isBlank()) {
return@mapNotNull null
}
if (sitePlugin.repositoryUrl.isNullOrBlank()) {
return@mapNotNull null
}
//Omit already existing plugins
if (getPluginPath(activity, sitePlugin.internalName, onlineData.first).exists()) {
Log.i(TAG, "Skip > ${sitePlugin.internalName}")
return@mapNotNull null
}
//Omit lang not selected on language setting
val lang = sitePlugin.language ?: return@mapNotNull null
//If set to 'universal', don't skip any language
if (!providerLang.contains(AllLanguagesName) && !providerLang.contains(lang)) {
return@mapNotNull null
}
//Log.i(TAG, "sitePlugin lang => $lang")
//Omit NSFW, if disabled
sitePlugin.tvTypes?.let { tvtypes ->
if (!settingsForProvider.enableAdult) {
if (tvtypes.contains(TvType.NSFW.name)) {
return@mapNotNull null
}
}
}
val savedData = PluginData(
url = sitePlugin.url,
internalName = sitePlugin.internalName,
isOnline = true,
filePath = "",
version = sitePlugin.version
)
OnlinePluginData(savedData, onlineData)
}
//Log.i(TAG, "notDownloadedPlugins => ${notDownloadedPlugins.toJson()}")
notDownloadedPlugins.apmap { pluginData ->
downloadPlugin(
activity,
pluginData.onlineData.second.url,
pluginData.savedData.internalName,
pluginData.onlineData.first,
!pluginData.isDisabled
).let { success ->
if (success)
newDownloadPlugins.add(pluginData.onlineData.second.name)
}
}
main {
val uitext = txt(R.string.plugins_downloaded, newDownloadPlugins.size)
createNotification(activity, uitext, newDownloadPlugins)
}
// ioSafe {
afterPluginsLoadedEvent.invoke(false)
// }
Log.i(TAG, "Plugin download done!")
}
/** /**
* Use updateAllOnlinePluginsAndLoadThem * Use updateAllOnlinePluginsAndLoadThem
* */ * */
fun loadAllOnlinePlugins(activity: Activity) { fun loadAllOnlinePlugins(context: Context) {
// Load all plugins as fast as possible! // Load all plugins as fast as possible!
(getPluginsOnline()).toList().apmap { pluginData -> (getPluginsOnline()).toList().apmap { pluginData ->
loadPlugin( loadPlugin(
activity, context,
File(pluginData.filePath), File(pluginData.filePath),
pluginData pluginData
) )
} }
} }
fun loadAllLocalPlugins(activity: Activity) { /**
* Reloads all local plugins and forces a page update, used for hot reloading with deployWithAdb
**/
fun hotReloadAllLocalPlugins(activity: FragmentActivity?) {
Log.d(TAG, "Reloading all local plugins!")
if (activity == null) return
getPluginsLocal().forEach {
unloadPlugin(it.filePath)
}
loadAllLocalPlugins(activity, true)
}
/**
* @param forceReload see afterPluginsLoadedEvent, basically a way to load all local plugins
* and reload all pages even if they are previously valid
**/
fun loadAllLocalPlugins(context: Context, forceReload: Boolean) {
val dir = File(LOCAL_PLUGINS_PATH) val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL) removeKey(PLUGINS_KEY_LOCAL)
@ -309,24 +417,39 @@ object PluginManager {
Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins") Log.d(TAG, "Files in '${LOCAL_PLUGINS_PATH}' folder: $sortedPlugins")
sortedPlugins?.sortedBy { it.name }?.apmap { file -> sortedPlugins?.sortedBy { it.name }?.apmap { file ->
maybeLoadPlugin(activity, file) maybeLoadPlugin(context, file)
} }
loadedLocalPlugins = true loadedLocalPlugins = true
afterPluginsLoadedEvent.invoke(true) afterPluginsLoadedEvent.invoke(forceReload)
}
/**
* This can be used to override any extension loading to fix crashes!
* @return true if safe mode file is present
**/
fun checkSafeModeFile(): Boolean {
return normalSafeApiCall {
val folder = File(CLOUD_STREAM_FOLDER)
if (!folder.exists()) return@normalSafeApiCall false
val files = folder.listFiles { _, name ->
name.equals("safe", ignoreCase = true)
}
files?.any()
} ?: false
} }
/** /**
* @return True if successful, false if not * @return True if successful, false if not
* */ * */
private suspend fun loadPlugin(activity: Activity, file: File, data: PluginData): Boolean { private suspend fun loadPlugin(context: Context, file: File, data: PluginData): Boolean {
val fileName = file.nameWithoutExtension val fileName = file.nameWithoutExtension
val filePath = file.absolutePath val filePath = file.absolutePath
currentlyLoading = fileName currentlyLoading = fileName
Log.i(TAG, "Loading plugin: $data") Log.i(TAG, "Loading plugin: $data")
return try { return try {
val loader = PathClassLoader(filePath, activity.classLoader) val loader = PathClassLoader(filePath, context.classLoader)
var manifest: Plugin.Manifest var manifest: Plugin.Manifest
loader.getResourceAsStream("manifest.json").use { stream -> loader.getResourceAsStream("manifest.json").use { stream ->
if (stream == null) { if (stream == null) {
@ -370,22 +493,22 @@ object PluginManager {
addAssetPath.invoke(assets, file.absolutePath) addAssetPath.invoke(assets, file.absolutePath)
pluginInstance.resources = Resources( pluginInstance.resources = Resources(
assets, assets,
activity.resources.displayMetrics, context.resources.displayMetrics,
activity.resources.configuration context.resources.configuration
) )
} }
plugins[filePath] = pluginInstance plugins[filePath] = pluginInstance
classLoaders[loader] = pluginInstance classLoaders[loader] = pluginInstance
urlPlugins[data.url ?: filePath] = pluginInstance urlPlugins[data.url ?: filePath] = pluginInstance
pluginInstance.load(activity) pluginInstance.load(context)
Log.i(TAG, "Loaded plugin ${data.internalName} successfully") Log.i(TAG, "Loaded plugin ${data.internalName} successfully")
currentlyLoading = null currentlyLoading = null
true true
} 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(
activity, context.getActivity(),
activity.getString(R.string.plugin_load_fail).format(fileName), context.getString(R.string.plugin_load_fail).format(fileName),
Toast.LENGTH_LONG Toast.LENGTH_LONG
) )
currentlyLoading = null currentlyLoading = null
@ -393,7 +516,7 @@ object PluginManager {
} }
} }
private fun unloadPlugin(absolutePath: String) { fun unloadPlugin(absolutePath: String) {
Log.i(TAG, "Unloading plugin: $absolutePath") Log.i(TAG, "Unloading plugin: $absolutePath")
val plugin = plugins[absolutePath] val plugin = plugins[absolutePath]
if (plugin == null) { if (plugin == null) {
@ -444,49 +567,48 @@ object PluginManager {
return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3") return File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}/${folderName}/$fileName.cs3")
} }
/** suspend fun downloadPlugin(
* Used for fresh installs
* */
suspend fun downloadAndLoadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
internalName: String, internalName: String,
repositoryUrl: String repositoryUrl: String,
loadPlugin: Boolean
): Boolean { ): Boolean {
val file = getPluginPath(activity, internalName, repositoryUrl) val file = getPluginPath(activity, internalName, repositoryUrl)
downloadAndLoadPlugin(activity, pluginUrl, internalName, file) return downloadPlugin(activity, pluginUrl, internalName, file, loadPlugin)
return true
} }
/** suspend fun downloadPlugin(
* Used for updates.
*
* Uses a file instead of repository url, as extensions can get moved it is better to directly
* update the files instead of getting the filepath from repo url.
* */
private suspend fun downloadAndLoadPlugin(
activity: Activity, activity: Activity,
pluginUrl: String, pluginUrl: String,
internalName: String, internalName: String,
file: File, file: File,
loadPlugin: Boolean
): Boolean { ): Boolean {
try { try {
unloadPlugin(file.absolutePath)
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) val newFile = downloadPluginToFile(pluginUrl, file) ?: return false
return loadPlugin(
activity, val data = PluginData(
newFile ?: return false, internalName,
PluginData( pluginUrl,
internalName, true,
pluginUrl, newFile.absolutePath,
true, PLUGIN_VERSION_NOT_SET
newFile.absolutePath,
PLUGIN_VERSION_NOT_SET
)
) )
return if (loadPlugin) {
unloadPlugin(file.absolutePath)
loadPlugin(
activity,
newFile,
data
)
} else {
setPluginData(data)
true
}
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
return false return false
@ -494,7 +616,8 @@ object PluginManager {
} }
suspend fun deletePlugin(file: File): Boolean { suspend fun deletePlugin(file: File): Boolean {
val list = (getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath } val list =
(getPluginsLocal() + getPluginsOnline()).filter { it.filePath == file.absolutePath }
return try { return try {
if (File(file.absolutePath).delete()) { if (File(file.absolutePath).delete()) {
@ -529,12 +652,14 @@ object PluginManager {
private fun createNotification( private fun createNotification(
context: Context, context: Context,
extensionNames: List<String> uitext: UiText,
extensions: List<String>
): Notification? { ): Notification? {
try { try {
if (extensionNames.isEmpty()) return null
val content = extensionNames.joinToString(", ") if (extensions.isEmpty()) return null
val content = extensions.joinToString(", ")
// main { // DON'T WANT TO SLOW IT DOWN // main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID) val builder = NotificationCompat.Builder(context, EXTENSIONS_CHANNEL_ID)
.setAutoCancel(false) .setAutoCancel(false)
@ -543,7 +668,8 @@ object PluginManager {
.setSilent(true) .setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setColor(context.colorFromAttribute(R.attr.colorPrimary)) .setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(context.getString(R.string.plugins_updated, extensionNames.size)) .setContentTitle(uitext.asString(context))
//.setContentTitle(context.getString(title, extensionNames.size))
.setSmallIcon(R.drawable.ic_baseline_extension_24) .setSmallIcon(R.drawable.ic_baseline_extension_24)
.setStyle( .setStyle(
NotificationCompat.BigTextStyle() NotificationCompat.BigTextStyle()

View file

@ -2,13 +2,17 @@ package com.lagradost.cloudstream3.plugins
import android.content.Context import android.content.Context
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.apmap import com.lagradost.cloudstream3.R
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.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
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.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 com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
@ -69,18 +73,54 @@ object RepositoryManager {
val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy { val PREBUILT_REPOSITORIES: Array<RepositoryData> by lazy {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray() getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
} }
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 */
fun convertRawGitUrl(url: String): String {
if (getKey<Boolean>(context!!.getString(R.string.jsdelivr_proxy_key)) != true) return url
val match = GH_REGEX.find(url) ?: return url
val (user, repo, rest) = match.destructured
return "https://cdn.jsdelivr.net/gh/$user/$repo@$rest"
}
suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim()
return if (fixedUrl.contains("^https?://".toRegex())) {
fixedUrl
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
return@let if (!it.contains("^https?://".toRegex()))
"https://${it}"
else fixedUrl
}
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
suspendSafeApiCall {
app.get("https://l.cloudstream.cf/${fixedUrl}", allowRedirects = false).let {
it.headers["Location"]?.let { url ->
return@suspendSafeApiCall if (!url.startsWith("https://cutt.ly/branded-domains")) url
else null
}
app.get("https://cutt.ly/${fixedUrl}", allowRedirects = false).let { it2 ->
it2.headers["Location"]?.let { url ->
return@suspendSafeApiCall if (url.startsWith("https://cutt.ly/404")) url else null
}
}
}
}
} else null
}
suspend fun parseRepository(url: String): Repository? { suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall { return suspendSafeApiCall {
// Take manifestVersion and such into account later // Take manifestVersion and such into account later
app.get(url).parsedSafe() app.get(convertRawGitUrl(url)).parsedSafe()
} }
} }
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(pluginUrls) val response = app.get(convertRawGitUrl(pluginUrls))
// Normal parsed function not working? // Normal parsed function not working?
// return response.parsedSafe() // return response.parsedSafe()
tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList() tryParseJson<Array<SitePlugin>>(response.text)?.toList() ?: emptyList()
@ -95,7 +135,7 @@ object RepositoryManager {
* */ * */
suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? { suspend fun getRepoPlugins(repositoryUrl: String): List<Pair<String, SitePlugin>>? {
val repo = parseRepository(repositoryUrl) ?: return null val repo = parseRepository(repositoryUrl) ?: return null
return repo.pluginLists.apmap { url -> return repo.pluginLists.amap { url ->
parsePlugins(url).map { parsePlugins(url).map {
repositoryUrl to it repositoryUrl to it
} }
@ -110,43 +150,17 @@ object RepositoryManager {
file.mkdirs() file.mkdirs()
// Overwrite if exists // Overwrite if exists
if (file.exists()) { file.delete() } if (file.exists()) {
file.delete()
}
file.createNewFile() file.createNewFile()
val body = app.get(pluginUrl).okhttpResponse.body val body = app.get(convertRawGitUrl(pluginUrl)).okhttpResponse.body
write(body.byteStream(), file.outputStream()) write(body.byteStream(), file.outputStream())
file file
} }
} }
suspend fun downloadPluginToFile(
context: Context,
pluginUrl: String,
/** Filename without .cs3 */
fileName: String,
folder: String
): File? {
return suspendSafeApiCall {
val extensionsDir = File(context.filesDir, ONLINE_PLUGINS_FOLDER)
if (!extensionsDir.exists())
extensionsDir.mkdirs()
val newDir = File(extensionsDir, folder)
newDir.mkdirs()
val newFile = File(newDir, "${fileName}.cs3")
// Overwrite if exists
if (newFile.exists()) {
newFile.delete()
}
newFile.createNewFile()
val body = app.get(pluginUrl).okhttpResponse.body
write(body.byteStream(), newFile.outputStream())
newFile
}
}
fun getRepositories(): Array<RepositoryData> { fun getRepositories(): Array<RepositoryData> {
return getKey(REPOSITORIES_KEY) ?: emptyArray() return getKey(REPOSITORIES_KEY) ?: emptyArray()
} }
@ -178,9 +192,17 @@ object RepositoryManager {
extensionsDir, extensionsDir,
getPluginSanitizedFileName(repository.url) getPluginSanitizedFileName(repository.url)
) )
PluginManager.deleteRepositoryData(file.absolutePath)
file.delete() // Unload all plugins, not using deletePlugin since we
// delete all data and files in deleteRepositoryData
normalSafeApiCall {
file.listFiles { plugin: File ->
unloadPlugin(plugin.absolutePath)
false
}
}
PluginManager.deleteRepositoryData(file.absolutePath)
} }
private fun write(stream: InputStream, output: OutputStream) { private fun write(stream: InputStream, output: OutputStream) {
@ -191,4 +213,4 @@ object RepositoryManager {
output.write(dataBuffer, 0, readBytes) output.write(dataBuffer, 0, readBytes)
} }
} }
} }

View file

@ -0,0 +1,224 @@
package com.lagradost.cloudstream3.services
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import androidx.work.*
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.TimeUnit
const val SUBSCRIPTION_CHANNEL_ID = "cloudstream3.subscriptions"
const val SUBSCRIPTION_WORK_NAME = "work_subscription"
const val SUBSCRIPTION_CHANNEL_NAME = "Subscriptions"
const val SUBSCRIPTION_CHANNEL_DESCRIPTION = "Notifications for new episodes on subscribed shows"
const val SUBSCRIPTION_NOTIFICATION_ID = 938712897 // Random unique
class SubscriptionWorkManager(val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
companion object {
fun enqueuePeriodicWork(context: Context?) {
if (context == null) return
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val periodicSyncDataWork =
PeriodicWorkRequest.Builder(SubscriptionWorkManager::class.java, 6, TimeUnit.HOURS)
.addTag(SUBSCRIPTION_WORK_NAME)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
SUBSCRIPTION_WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
periodicSyncDataWork
)
// Uncomment below for testing
// val oneTimeSyncDataWork =
// OneTimeWorkRequest.Builder(SubscriptionWorkManager::class.java)
// .addTag(SUBSCRIPTION_WORK_NAME)
// .setConstraints(constraints)
// .build()
//
// WorkManager.getInstance(context).enqueue(oneTimeSyncDataWork)
}
}
private val progressNotificationBuilder =
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
.setAutoCancel(false)
.setColorized(true)
.setOnlyAlertOnce(true)
.setSilent(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setContentTitle(context.getString(R.string.subscription_in_progress_notification))
.setSmallIcon(R.drawable.quantum_ic_refresh_white_24)
.setProgress(0, 0, true)
private val updateNotificationBuilder =
NotificationCompat.Builder(context, SUBSCRIPTION_CHANNEL_ID)
.setColorized(true)
.setOnlyAlertOnce(true)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
.setSmallIcon(R.drawable.ic_cloudstream_monochrome_big)
private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private fun updateProgress(max: Int, progress: Int, indeterminate: Boolean) {
notificationManager.notify(
SUBSCRIPTION_NOTIFICATION_ID, progressNotificationBuilder
.setProgress(max, progress, indeterminate)
.build()
)
}
override suspend fun doWork(): Result {
// println("Update subscriptions!")
context.createNotificationChannel(
SUBSCRIPTION_CHANNEL_ID,
SUBSCRIPTION_CHANNEL_NAME,
SUBSCRIPTION_CHANNEL_DESCRIPTION
)
setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
progressNotificationBuilder.build()
)
)
val subscriptions = getAllSubscriptions()
if (subscriptions.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this.id)
return Result.success()
}
val max = subscriptions.size
var progress = 0
updateProgress(max, progress, true)
// We need all plugins loaded.
PluginManager.loadAllOnlinePlugins(context)
PluginManager.loadAllLocalPlugins(context, false)
subscriptions.apmap { savedData ->
try {
val id = savedData.id ?: return@apmap null
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
// Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse
} ?: return@apmap null
val dubPreference =
getDub(id) ?: if (
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
) {
DubStatus.Dubbed
} else {
DubStatus.Subbed
}
val latestEpisodes = response.getLatestEpisodes()
val latestPreferredEpisode = latestEpisodes[dubPreference]
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
shouldUpdate to latestPreferredEpisode
} else {
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
val shouldUpdate = latestEpisode > latestSeenEpisode
shouldUpdate to latestEpisode
}
DataStoreHelper.updateSubscribedData(
id,
savedData,
response
)
if (shouldUpdate) {
val updateHeader = savedData.name
val updateDescription = txt(
R.string.subscription_episode_released,
latestEpisode,
savedData.name
).asString(context)
val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getActivity(context, 0, intent, 0)
}
val poster = ioWork {
savedData.posterUrl?.let { url ->
context.getImageBitmapFromUrl(
url,
savedData.posterHeaders
)
}
}
val updateNotification =
updateNotificationBuilder.setContentTitle(updateHeader)
.setContentText(updateDescription)
.setContentIntent(pendingIntent)
.setLargeIcon(poster)
.build()
notificationManager.notify(id, updateNotification)
}
// You can probably get some issues here since this is async but it does not matter much.
updateProgress(max, ++progress, false)
} catch (_: Throwable) {
}
}
return Result.success()
}
}

View file

@ -1,11 +1,22 @@
package com.lagradost.cloudstream3.services package com.lagradost.cloudstream3.services
import android.app.Service
import android.app.IntentService
import android.content.Intent import android.content.Intent
import android.os.IBinder
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class VideoDownloadService : IntentService("VideoDownloadService") { class VideoDownloadService : Service() {
override fun onHandleIntent(intent: Intent?) {
private val downloadScope = CoroutineScope(Dispatchers.Default)
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent != null) { if (intent != null) {
val id = intent.getIntExtra("id", -1) val id = intent.getIntExtra("id", -1)
val type = intent.getStringExtra("type") val type = intent.getStringExtra("type")
@ -14,10 +25,36 @@ class VideoDownloadService : IntentService("VideoDownloadService") {
"resume" -> VideoDownloadManager.DownloadActionType.Resume "resume" -> VideoDownloadManager.DownloadActionType.Resume
"pause" -> VideoDownloadManager.DownloadActionType.Pause "pause" -> VideoDownloadManager.DownloadActionType.Pause
"stop" -> VideoDownloadManager.DownloadActionType.Stop "stop" -> VideoDownloadManager.DownloadActionType.Stop
else -> return else -> return START_NOT_STICKY
}
downloadScope.launch {
VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
} }
VideoDownloadManager.downloadEvent.invoke(Pair(id, state))
} }
} }
return START_NOT_STICKY
} }
}
override fun onDestroy() {
downloadScope.coroutineContext.cancel()
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

@ -12,6 +12,8 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val aniListApi = AniListApi(0) val aniListApi = AniListApi(0)
val openSubtitlesApi = OpenSubtitlesApi(0) val openSubtitlesApi = OpenSubtitlesApi(0)
val indexSubtitlesApi = IndexSubtitleApi() val indexSubtitlesApi = IndexSubtitleApi()
val addic7ed = Addic7ed()
val localListApi = LocalList()
// used to login via app intent // used to login via app intent
val OAuth2Apis val OAuth2Apis
@ -28,7 +30,7 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
// used for active syncing // used for active syncing
val SyncApis val SyncApis
get() = listOf( get() = listOf(
SyncRepo(malApi), SyncRepo(aniListApi) SyncRepo(malApi), SyncRepo(aniListApi), SyncRepo(localListApi)
) )
val inAppAuths val inAppAuths
@ -37,11 +39,19 @@ abstract class AccountManager(private val defIndex: Int) : AuthAPI {
val subtitleProviders val subtitleProviders
get() = listOf( get() = listOf(
openSubtitlesApi, openSubtitlesApi,
indexSubtitlesApi // they got anti scraping measures in place :( indexSubtitlesApi, // they got anti scraping measures in place :(
addic7ed
) )
const val appString = "cloudstreamapp" const val appString = "cloudstreamapp"
const val appStringRepo = "cloudstreamrepo" const val appStringRepo = "cloudstreamrepo"
const val appStringPlayer = "cloudstreamplayer"
// Instantly start the search given a query
const val appStringSearch = "cloudstreamsearch"
// Instantly resume watching a show
const val appStringResumeWatching = "cloudstreamcontinuewatching"
val unixTime: Long val unixTime: Long
get() = System.currentTimeMillis() / 1000L get() = System.currentTimeMillis() / 1000L

View file

@ -1,10 +1,31 @@
package com.lagradost.cloudstream3.syncproviders package com.lagradost.cloudstream3.syncproviders
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.UiText
import me.xdrop.fuzzywuzzy.FuzzySearch
enum class SyncIdName {
Anilist,
MyAnimeList,
Trakt,
Imdb,
LocalList
}
interface SyncAPI : OAuth2API { interface SyncAPI : OAuth2API {
/**
* Set this to true if the user updates something on the list like watch status or score
**/
var requireLibraryRefresh: Boolean
val mainUrl: String val mainUrl: String
/**
* Allows certain providers to open pages from
* library links.
**/
val syncIdName: SyncIdName
/** /**
-1 -> None -1 -> None
0 -> Watching 0 -> Watching
@ -22,7 +43,9 @@ interface SyncAPI : OAuth2API {
suspend fun search(name: String): List<SyncSearchResult>? suspend fun search(name: String): List<SyncSearchResult>?
fun getIdFromUrl(url : String) : String suspend fun getPersonalLibrary(): LibraryMetadata?
fun getIdFromUrl(url: String): String
data class SyncSearchResult( data class SyncSearchResult(
override val name: String, override val name: String,
@ -42,7 +65,7 @@ interface SyncAPI : OAuth2API {
val score: Int?, val score: Int?,
val watchedEpisodes: Int?, val watchedEpisodes: Int?,
var isFavorite: Boolean? = null, var isFavorite: Boolean? = null,
var maxEpisodes : Int? = null, var maxEpisodes: Int? = null,
) )
data class SyncResult( data class SyncResult(
@ -63,9 +86,9 @@ interface SyncAPI : OAuth2API {
var genres: List<String>? = null, var genres: List<String>? = null,
var synonyms: List<String>? = null, var synonyms: List<String>? = null,
var trailers: List<String>? = null, var trailers: List<String>? = null,
var isAdult : Boolean? = null, var isAdult: Boolean? = null,
var posterUrl: String? = null, var posterUrl: String? = null,
var backgroundPosterUrl : String? = null, var backgroundPosterUrl: String? = null,
/** In unixtime */ /** In unixtime */
var startDate: Long? = null, var startDate: Long? = null,
@ -76,4 +99,61 @@ interface SyncAPI : OAuth2API {
var prevSeason: SyncSearchResult? = null, var prevSeason: SyncSearchResult? = null,
var actors: List<ActorData>? = null, var actors: List<ActorData>? = null,
) )
data class Page(
val title: UiText, var items: List<LibraryItem>
) {
fun sort(method: ListSorting?, query: String? = null) {
items = when (method) {
ListSorting.Query ->
if (query != null) {
items.sortedBy {
-FuzzySearch.partialRatio(
query.lowercase(), it.name.lowercase()
)
}
} else items
ListSorting.RatingHigh -> items.sortedBy { -(it.personalRating ?: 0) }
ListSorting.RatingLow -> items.sortedBy { (it.personalRating ?: 0) }
ListSorting.AlphabeticalA -> items.sortedBy { it.name }
ListSorting.AlphabeticalZ -> items.sortedBy { it.name }.reversed()
ListSorting.UpdatedNew -> items.sortedBy { it.lastUpdatedUnixTime?.times(-1) }
ListSorting.UpdatedOld -> items.sortedBy { it.lastUpdatedUnixTime }
else -> items
}
}
}
data class LibraryMetadata(
val allLibraryLists: List<LibraryList>,
val supportedListSorting: Set<ListSorting>
)
data class LibraryList(
val name: UiText,
val items: List<LibraryItem>
)
data class LibraryItem(
override val name: String,
override val url: String,
/**
* Unique unchanging string used for data storage.
* This should be the actual id when you change scores and status
* since score changes from library might get added in the future.
**/
val syncId: String,
val episodesCompleted: Int?,
val episodesTotal: Int?,
/** Out of 100 */
val personalRating: Int?,
val lastUpdatedUnixTime: Long?,
override val apiName: String,
override var type: TvType?,
override var posterUrl: String?,
override var posterHeaders: Map<String, String>?,
override var quality: SearchQuality?,
override var id: Int? = null,
) : SearchResponse
} }

View file

@ -11,26 +11,38 @@ class SyncRepo(private val repo: SyncAPI) {
val icon = repo.icon val icon = repo.icon
val mainUrl = repo.mainUrl val mainUrl = repo.mainUrl
val requiresLogin = repo.requiresLogin val requiresLogin = repo.requiresLogin
val syncIdName = repo.syncIdName
var requireLibraryRefresh: Boolean
get() = repo.requireLibraryRefresh
set(value) {
repo.requireLibraryRefresh = value
}
suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> { suspend fun score(id: String, status: SyncAPI.SyncStatus): Resource<Boolean> {
return safeApiCall { repo.score(id, status) } return safeApiCall { repo.score(id, status) }
} }
suspend fun getStatus(id : String) : Resource<SyncAPI.SyncStatus> { suspend fun getStatus(id: String): Resource<SyncAPI.SyncStatus> {
return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") } return safeApiCall { repo.getStatus(id) ?: throw ErrorLoadingException("No data") }
} }
suspend fun getResult(id : String) : Resource<SyncAPI.SyncResult> { suspend fun getResult(id: String): Resource<SyncAPI.SyncResult> {
return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") } return safeApiCall { repo.getResult(id) ?: throw ErrorLoadingException("No data") }
} }
suspend fun search(query : String) : Resource<List<SyncAPI.SyncSearchResult>> { suspend fun search(query: String): Resource<List<SyncAPI.SyncSearchResult>> {
return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() } return safeApiCall { repo.search(query) ?: throw ErrorLoadingException() }
} }
fun hasAccount() : Boolean { suspend fun getPersonalLibrary(): Resource<SyncAPI.LibraryMetadata> {
return safeApiCall { repo.getPersonalLibrary() ?: throw ErrorLoadingException() }
}
fun hasAccount(): Boolean {
return normalSafeApiCall { repo.loginInfo() != null } ?: false return normalSafeApiCall { repo.loginInfo() != null } ?: false
} }
fun getIdFromUrl(url : String) : String = repo.getIdFromUrl(url) fun getIdFromUrl(url: String): String? = normalSafeApiCall {
repo.getIdFromUrl(url)
}
} }

View file

@ -0,0 +1,108 @@
package com.lagradost.cloudstream3.syncproviders.providers
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.utils.SubtitleHelper
class Addic7ed : AbstractSubApi {
override val name = "Addic7ed"
override val idPrefix = "addic7ed"
override val requiresLogin = false
override val icon: Nothing? = null
override val createAccountUrl: Nothing? = null
override fun loginInfo(): Nothing? = null
override fun logOut() {}
companion object {
const val host = "https://www.addic7ed.com"
const val TAG = "ADDIC7ED"
}
private fun fixUrl(url: String): String {
return if (url.startsWith("/")) host + url
else if (!url.startsWith("http")) "$host/$url"
else url
}
override suspend fun search(query: AbstractSubtitleEntities.SubtitleSearch): List<AbstractSubtitleEntities.SubtitleEntity> {
val lang = query.lang
val queryLang = SubtitleHelper.fromTwoLettersToLanguage(lang.toString())
val queryText = query.query.trim()
val epNum = query.epNumber ?: 0
val seasonNum = query.seasonNumber ?: 0
val yearNum = query.year ?: 0
fun cleanResources(
results: MutableList<AbstractSubtitleEntities.SubtitleEntity>,
name: String,
link: String,
headers: Map<String, String>,
isHearingImpaired: Boolean
) {
results.add(
AbstractSubtitleEntities.SubtitleEntity(
idPrefix = idPrefix,
name = name,
lang = queryLang.toString(),
data = link,
source = this.name,
type = if (seasonNum > 0) TvType.TvSeries else TvType.Movie,
epNumber = epNum,
seasonNumber = seasonNum,
year = yearNum,
headers = headers,
isHearingImpaired = isHearingImpaired
)
)
}
val title = queryText.substringBefore("(").trim()
val url = "$host/search.php?search=${title}&Submit=Search"
val hostDocument = app.get(url).document
var searchResult = ""
if (!hostDocument.select("span:contains($title)").isNullOrEmpty()) searchResult = url
else if (!hostDocument.select("table.tabel")
.isNullOrEmpty()
) searchResult = hostDocument.select("a:contains($title)").attr("href").toString()
else {
val show =
hostDocument.selectFirst("#sl button")?.attr("onmouseup")?.substringAfter("(")
?.substringBefore(",")
val doc = app.get(
"$host/ajax_loadShow.php?show=$show&season=$seasonNum&langs=&hd=undefined&hi=undefined",
referer = "$host/"
).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 ->
val name = if (seasonNum > 0) "${document.select(".titulo").text().replace("Subtitle","").trim()}${
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 = fixUrl(node.select("a.buttonDownload").attr("href"))
val isHearingImpaired =
!node.parent()!!.select("tr:last-child [title=\"Hearing Impaired\"]").isNullOrEmpty()
cleanResources(results, name, link, mapOf("referer" to "$host/"), isHearingImpaired)
}
return results
}
override suspend fun load(data: AbstractSubtitleEntities.SubtitleEntity): String {
return data.data
}
}

View file

@ -1,19 +1,20 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
@ -21,6 +22,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
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.DataStore.toKotlinObject
import java.net.URL import java.net.URL
import java.net.URLEncoder
import java.util.* import java.util.*
class AniListApi(index: Int) : AccountManager(index), SyncAPI { class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@ -28,10 +30,12 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
override val key = "6871" override val key = "6871"
override val redirectUrl = "anilistlogin" override val redirectUrl = "anilistlogin"
override val idPrefix = "anilist" override val idPrefix = "anilist"
override var requireLibraryRefresh = true
override var mainUrl = "https://anilist.co" override var mainUrl = "https://anilist.co"
override val icon = R.drawable.ic_anilist_icon override val icon = R.drawable.ic_anilist_icon
override val requiresLogin = false override val requiresLogin = false
override val createAccountUrl = "$mainUrl/signup" override val createAccountUrl = "$mainUrl/signup"
override val syncIdName = SyncIdName.Anilist
override fun loginInfo(): AuthAPI.LoginInfo? { override fun loginInfo(): AuthAPI.LoginInfo? {
// context.getUser(true)?. // context.getUser(true)?.
@ -46,6 +50,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
override fun logOut() { override fun logOut() {
requireLibraryRefresh = true
removeAccountKeys() removeAccountKeys()
} }
@ -65,8 +70,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
switchToNewAccount() switchToNewAccount()
setKey(accountId, ANILIST_UNIXTIME_KEY, endTime) setKey(accountId, ANILIST_UNIXTIME_KEY, endTime)
setKey(accountId, ANILIST_TOKEN_KEY, token) setKey(accountId, ANILIST_TOKEN_KEY, token)
setKey(ANILIST_SHOULD_UPDATE_LIST, true)
val user = getUser() val user = getUser()
requireLibraryRefresh = true
return user != null return user != null
} }
@ -141,7 +146,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
this.name, this.name,
recMedia.id?.toString() ?: return@mapNotNull null, recMedia.id?.toString() ?: return@mapNotNull null,
getUrlFromId(recMedia.id), getUrlFromId(recMedia.id),
recMedia.coverImage?.large ?: recMedia.coverImage?.medium recMedia.coverImage?.extraLarge ?: recMedia.coverImage?.large
?: recMedia.coverImage?.medium
) )
}, },
trailers = when (season.trailer?.site?.lowercase()?.trim()) { trailers = when (season.trailer?.site?.lowercase()?.trim()) {
@ -171,7 +177,9 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
fromIntToAnimeStatus(status.status), fromIntToAnimeStatus(status.status),
status.score, status.score,
status.watchedEpisodes status.watchedEpisodes
) ).also {
requireLibraryRefresh = requireLibraryRefresh || it
}
} }
companion object { companion object {
@ -182,7 +190,6 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api const val ANILIST_TOKEN_KEY: String = "anilist_token" // anilist token for api
const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile const val ANILIST_USER_KEY: String = "anilist_user" // user data like profile
const val ANILIST_CACHED_LIST: String = "anilist_cached_list" const val ANILIST_CACHED_LIST: String = "anilist_cached_list"
const val ANILIST_SHOULD_UPDATE_LIST: String = "anilist_should_update_list"
private fun fixName(name: String): String { private fun fixName(name: String): String {
return name.lowercase(Locale.ROOT).replace(" ", "") return name.lowercase(Locale.ROOT).replace(" ", "")
@ -220,7 +227,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
romaji romaji
} }
idMal idMal
coverImage { medium large } coverImage { medium large extraLarge }
averageScore averageScore
} }
} }
@ -233,7 +240,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
format format
id id
idMal idMal
coverImage { medium large } coverImage { medium large extraLarge }
averageScore averageScore
title { title {
english english
@ -293,15 +300,13 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
val shows = searchShows(name.replace(blackListRegex, "")) val shows = searchShows(name.replace(blackListRegex, ""))
shows?.data?.Page?.media?.find { shows?.data?.Page?.media?.find {
malId ?: "NONE" == it.idMal.toString() (malId ?: "NONE") == it.idMal.toString()
}?.let { return it } }?.let { return it }
val filtered = val filtered =
shows?.data?.Page?.media?.filter { shows?.data?.Page?.media?.filter {
( (((it.startDate.year ?: year.toString()) == year.toString()
it.startDate.year ?: year.toString() == year.toString() || year == null))
|| year == null
)
} }
filtered?.forEach { filtered?.forEach {
it.title.romaji?.let { romaji -> it.title.romaji?.let { romaji ->
@ -313,14 +318,14 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
// Changing names of these will show up in UI // Changing names of these will show up in UI
enum class AniListStatusType(var value: Int) { enum class AniListStatusType(var value: Int, @StringRes val stringRes: Int) {
Watching(0), Watching(0, R.string.type_watching),
Completed(1), Completed(1, R.string.type_completed),
Paused(2), Paused(2, R.string.type_on_hold),
Dropped(3), Dropped(3, R.string.type_dropped),
Planning(4), Planning(4, R.string.type_plan_to_watch),
ReWatching(5), ReWatching(5, R.string.type_re_watching),
None(-1) None(-1, R.string.none)
} }
fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp } fun fromIntToAnimeStatus(inp: Int): AniListStatusType {//= AniListStatusType.values().first { it.value == inp }
@ -336,7 +341,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
} }
fun convertAnilistStringToStatus(string: String): AniListStatusType { fun convertAniListStringToStatus(string: String): AniListStatusType {
return fromIntToAnimeStatus(aniListStatusString.indexOf(string)) return fromIntToAnimeStatus(aniListStatusString.indexOf(string))
} }
@ -522,19 +527,27 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
} }
private suspend fun postApi(q: String, cache: Boolean = false): String? { private suspend fun postApi(q: String, cache: Boolean = false): String? {
return if (!checkToken()) { return suspendSafeApiCall {
app.post( if (!checkToken()) {
"https://graphql.anilist.co/", app.post(
headers = mapOf( "https://graphql.anilist.co/",
"Authorization" to "Bearer " + (getAuth() ?: return null), headers = mapOf(
if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache" "Authorization" to "Bearer " + (getAuth()
), ?: return@suspendSafeApiCall null),
cacheTime = 0, if (cache) "Cache-Control" to "max-stale=$maxStale" else "Cache-Control" to "no-cache"
data = mapOf("query" to q),//(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars)) ),
timeout = 5 // REASONABLE TIMEOUT cacheTime = 0,
).text.replace("\\/", "/") data = mapOf(
} else { "query" to URLEncoder.encode(
null q,
"UTF-8"
)
), //(if (vars == null) mapOf("query" to q) else mapOf("query" to q, "variables" to vars))
timeout = 5 // REASONABLE TIMEOUT
).text.replace("\\/", "/")
} else {
null
}
} }
} }
@ -569,7 +582,8 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
data class CoverImage( data class CoverImage(
@JsonProperty("medium") val medium: String?, @JsonProperty("medium") val medium: String?,
@JsonProperty("large") val large: String? @JsonProperty("large") val large: String?,
@JsonProperty("extraLarge") val extraLarge: String?
) )
data class Media( data class Media(
@ -596,7 +610,29 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("score") val score: Int, @JsonProperty("score") val score: Int,
@JsonProperty("private") val private: Boolean, @JsonProperty("private") val private: Boolean,
@JsonProperty("media") val media: Media @JsonProperty("media") val media: Media
) ) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
// English title first
this.media.title.english ?: this.media.title.romaji
?: this.media.synonyms.firstOrNull()
?: "",
"https://anilist.co/anime/${this.media.id}/",
this.media.id.toString(),
this.progress,
this.media.episodes,
this.score,
this.updatedAt.toLong(),
"AniList",
TvType.Anime,
this.media.coverImage.extraLarge ?: this.media.coverImage.large
?: this.media.coverImage.medium,
null,
null,
null
)
}
}
data class Lists( data class Lists(
@JsonProperty("status") val status: String?, @JsonProperty("status") val status: String?,
@ -611,40 +647,59 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
@JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection @JsonProperty("MediaListCollection") val MediaListCollection: MediaListCollection
) )
fun getAnilistListCached(): Array<Lists>? { private fun getAniListListCached(): Array<Lists>? {
return getKey(ANILIST_CACHED_LIST) as? Array<Lists> return getKey(ANILIST_CACHED_LIST) as? Array<Lists>
} }
suspend fun getAnilistAnimeListSmart(): Array<Lists>? { private suspend fun getAniListAnimeListSmart(): Array<Lists>? {
if (getAuth() == null) return null if (getAuth() == null) return null
if (checkToken()) return null if (checkToken()) return null
return if (getKey(ANILIST_SHOULD_UPDATE_LIST, true) == true) { return if (requireLibraryRefresh) {
val list = getFullAnilistList()?.data?.MediaListCollection?.lists?.toTypedArray() val list = getFullAniListList()?.data?.MediaListCollection?.lists?.toTypedArray()
if (list != null) { if (list != null) {
setKey(ANILIST_CACHED_LIST, list) setKey(ANILIST_CACHED_LIST, list)
setKey(ANILIST_SHOULD_UPDATE_LIST, false)
} }
list list
} else { } else {
getAnilistListCached() getAniListListCached()
} }
} }
private suspend fun getFullAnilistList(): FullAnilistList? { override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
var userID: Int? = null val list = getAniListAnimeListSmart()?.groupBy {
/** WARNING ASSUMES ONE USER! **/ convertAniListStringToStatus(it.status ?: "").stringRes
getKeys(ANILIST_USER_KEY)?.forEach { key -> }?.mapValues { group ->
getKey<AniListUser>(key, null)?.let { group.value.map { it.entries.map { entry -> entry.toLibraryItem() } }.flatten()
userID = it.id } ?: emptyMap()
}
}
val fixedUserID = userID ?: return null // To fill empty lists when AniList does not return them
val baseMap =
AniListStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getFullAniListList(): FullAnilistList? {
/** WARNING ASSUMES ONE USER! **/
val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return null
val mediaType = "ANIME" val mediaType = "ANIME"
val query = """ val query = """
query (${'$'}userID: Int = $fixedUserID, ${'$'}MEDIA: MediaType = $mediaType) { query (${'$'}userID: Int = $userID, ${'$'}MEDIA: MediaType = $mediaType) {
MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) { MediaListCollection (userId: ${'$'}userID, type: ${'$'}MEDIA) {
lists { lists {
status status
@ -655,7 +710,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
startedAt { year month day } startedAt { year month day }
updatedAt updatedAt
progress progress
score score (format: POINT_100)
private private
media media
{ {
@ -671,7 +726,7 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
english english
romaji romaji
} }
coverImage { medium } coverImage { extraLarge large medium }
synonyms synonyms
nextAiringEpisode { nextAiringEpisode {
timeUntilAiring timeUntilAiring
@ -704,6 +759,11 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
return data != "" return data != ""
} }
/** Used to query a saved MediaItem on the list to get the id for removal */
data class MediaListItemRoot(@JsonProperty("data") val data: MediaListItem? = null)
data class MediaListItem(@JsonProperty("MediaList") val MediaList: MediaListId? = null)
data class MediaListId(@JsonProperty("id") val id: Long? = null)
private suspend fun postDataAboutId( private suspend fun postDataAboutId(
id: Int, id: Int,
type: AniListStatusType, type: AniListStatusType,
@ -711,19 +771,43 @@ class AniListApi(index: Int) : AccountManager(index), SyncAPI {
progress: Int? progress: Int?
): Boolean { ): Boolean {
val q = val q =
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${ // Delete item if status type is None
aniListStatusString[maxOf( if (type == AniListStatusType.None) {
0, val userID = getKey<AniListUser>(accountId, ANILIST_USER_KEY)?.id ?: return false
type.value // Get list ID for deletion
)] val idQuery = """
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) { query MediaList(${'$'}userId: Int = $userID, ${'$'}mediaId: Int = $id) {
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) { MediaList(userId: ${'$'}userId, mediaId: ${'$'}mediaId) {
id id
status }
progress }
score """
} val response = postApi(idQuery)
val listId =
tryParseJson<MediaListItemRoot>(response)?.data?.MediaList?.id ?: return false
"""
mutation(${'$'}id: Int = $listId) {
DeleteMediaListEntry(id: ${'$'}id) {
deleted
}
}
"""
} else {
"""mutation (${'$'}id: Int = $id, ${'$'}status: MediaListStatus = ${
aniListStatusString[maxOf(
0,
type.value
)]
}, ${if (score != null) "${'$'}scoreRaw: Int = ${score * 10}" else ""} , ${if (progress != null) "${'$'}progress: Int = $progress" else ""}) {
SaveMediaListEntry (mediaId: ${'$'}id, status: ${'$'}status, scoreRaw: ${'$'}scoreRaw, progress: ${'$'}progress) {
id
status
progress
score
}
}""" }"""
}
val data = postApi(q) val data = postApi(q)
return data != "" return data != ""
} }

View file

@ -4,7 +4,6 @@ import android.util.Log
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.imdbUrlToIdNullable import com.lagradost.cloudstream3.imdbUrlToIdNullable
import com.lagradost.cloudstream3.network.CloudflareKiller
import com.lagradost.cloudstream3.subtitles.AbstractSubApi import com.lagradost.cloudstream3.subtitles.AbstractSubApi
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
@ -22,7 +21,7 @@ class IndexSubtitleApi : AbstractSubApi {
companion object { companion object {
const val host = "https://subscene.cyou" const val host = "https://indexsubtitle.com"
const val TAG = "INDEXSUBS" const val TAG = "INDEXSUBS"
} }
@ -242,7 +241,7 @@ class IndexSubtitleApi : AbstractSubApi {
document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href") document.selectFirst("div.my-3.p-3 div.media a")!!.attr("href")
) )
} else { } else {
document.select("div.my-3.p-3 div.media").mapNotNull { block -> document.select("div.my-3.p-3 div.media").firstNotNullOf { block ->
val name = val name =
block.selectFirst("strong.d-block")?.text()?.trim().toString() block.selectFirst("strong.d-block")?.text()?.trim().toString()
if (seasonNum!! > 0) { if (seasonNum!! > 0) {
@ -254,7 +253,7 @@ class IndexSubtitleApi : AbstractSubApi {
} else { } else {
fixUrl(block.selectFirst("a")!!.attr("href")) fixUrl(block.selectFirst("a")!!.attr("href"))
} }
}.first() }
} }
return link return link
} }

View file

@ -0,0 +1,104 @@
package com.lagradost.cloudstream3.syncproviders.providers
import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState
class LocalList : SyncAPI {
override val name = "Local"
override val icon: Int = R.drawable.ic_baseline_storage_24
override val requiresLogin = false
override val createAccountUrl: Nothing? = null
override val idPrefix = "local"
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 suspend fun score(id: String, status: SyncAPI.SyncStatus): Boolean {
return true
}
override suspend fun getStatus(id: String): SyncAPI.SyncStatus? {
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 {
getAllWatchStateIds()?.map { id ->
Pair(id, getResultWatchState(id))
}
}?.distinctBy { it.first } ?: return null
val list = ioWork {
watchStatusIds.groupBy {
it.second.stringRes
}.mapValues { group ->
group.value.mapNotNull {
getBookmarkedData(it.first)?.toLibraryItem(it.first.toString())
}
} + mapOf(R.string.subscription_list_name to getAllSubscriptions().mapNotNull {
it.toLibraryItem()
})
}
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf(R.string.subscription_list_name to emptyList())
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
// ListSorting.UpdatedNew,
// ListSorting.UpdatedOld,
// ListSorting.RatingHigh,
// ListSorting.RatingLow,
)
)
}
override fun getIdFromUrl(url: String): String {
return url
}
}

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.syncproviders.providers package com.lagradost.cloudstream3.syncproviders.providers
import android.util.Base64 import android.util.Base64
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
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.getKey
@ -8,11 +9,15 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ShowStatus 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.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.parseJson import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.splitQuery import com.lagradost.cloudstream3.utils.AppUtils.splitQuery
import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject
@ -31,13 +36,15 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
override val redirectUrl = "mallogin" override val redirectUrl = "mallogin"
override val idPrefix = "mal" override val idPrefix = "mal"
override var mainUrl = "https://myanimelist.net" override var mainUrl = "https://myanimelist.net"
val apiUrl = "https://api.myanimelist.net" private val apiUrl = "https://api.myanimelist.net"
override val icon = R.drawable.mal_logo override val icon = R.drawable.mal_logo
override val requiresLogin = false override val requiresLogin = false
override val syncIdName = SyncIdName.MyAnimeList
override var requireLibraryRefresh = true
override val createAccountUrl = "$mainUrl/register.php" override val createAccountUrl = "$mainUrl/register.php"
override fun logOut() { override fun logOut() {
requireLibraryRefresh = true
removeAccountKeys() removeAccountKeys()
} }
@ -90,7 +97,9 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
fromIntToAnimeStatus(status.status), fromIntToAnimeStatus(status.status),
status.score, status.score,
status.watchedEpisodes status.watchedEpisodes
) ).also {
requireLibraryRefresh = requireLibraryRefresh || it
}
} }
data class MalAnime( data class MalAnime(
@ -248,10 +257,45 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
const val MAL_USER_KEY: String = "mal_user" // user data like profile const val MAL_USER_KEY: String = "mal_user" // user data like profile
const val MAL_CACHED_LIST: String = "mal_cached_list" const val MAL_CACHED_LIST: String = "mal_cached_list"
const val MAL_SHOULD_UPDATE_LIST: String = "mal_should_update_list"
const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires const val MAL_UNIXTIME_KEY: String = "mal_unixtime" // When token expires
const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token const val MAL_REFRESH_TOKEN_KEY: String = "mal_refresh_token" // refresh token
const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api const val MAL_TOKEN_KEY: String = "mal_token" // anilist token for api
fun convertToStatus(string: String): MalStatusType {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
enum class MalStatusType(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 fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
1 -> MalStatusType.Completed
2 -> MalStatusType.OnHold
3 -> MalStatusType.Dropped
4 -> MalStatusType.PlanToWatch
5 -> MalStatusType.Watching
else -> MalStatusType.None
}
}
private fun parseDateLong(string: String?): Long? {
return try {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(
string ?: return null
)?.time?.div(1000)
} catch (e: Exception) {
null
}
}
} }
override suspend fun handleRedirect(url: String): Boolean { override suspend fun handleRedirect(url: String): Boolean {
@ -275,7 +319,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
switchToNewAccount() switchToNewAccount()
storeToken(res) storeToken(res)
val user = getMalUser() val user = getMalUser()
setKey(MAL_SHOULD_UPDATE_LIST, true) requireLibraryRefresh = true
return user != null return user != null
} }
} }
@ -308,9 +352,10 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime)) setKey(accountId, MAL_UNIXTIME_KEY, (token.expires_in + unixTime))
setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token) setKey(accountId, MAL_REFRESH_TOKEN_KEY, token.refresh_token)
setKey(accountId, MAL_TOKEN_KEY, token.access_token) setKey(accountId, MAL_TOKEN_KEY, token.access_token)
requireLibraryRefresh = true
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() logError(e)
} }
} }
@ -329,7 +374,7 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
).text ).text
storeToken(res) storeToken(res)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() logError(e)
} }
} }
@ -382,7 +427,24 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
data class Data( data class Data(
@JsonProperty("node") val node: Node, @JsonProperty("node") val node: Node,
@JsonProperty("list_status") val list_status: ListStatus?, @JsonProperty("list_status") val list_status: ListStatus?,
) ) {
fun toLibraryItem(): SyncAPI.LibraryItem {
return SyncAPI.LibraryItem(
this.node.title,
"https://myanimelist.net/anime/${this.node.id}/",
this.node.id.toString(),
this.list_status?.num_episodes_watched,
this.node.num_episodes,
this.list_status?.score?.times(10),
parseDateLong(this.list_status?.updated_at),
"MAL",
TvType.Anime,
this.node.main_picture?.large ?: this.node.main_picture?.medium,
null,
null,
)
}
}
data class Paging( data class Paging(
@JsonProperty("next") val next: String? @JsonProperty("next") val next: String?
@ -413,18 +475,43 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return getKey(MAL_CACHED_LIST) as? Array<Data> return getKey(MAL_CACHED_LIST) as? Array<Data>
} }
suspend fun getMalAnimeListSmart(): Array<Data>? { private suspend fun getMalAnimeListSmart(): Array<Data>? {
if (getAuth() == null) return null if (getAuth() == null) return null
return if (getKey(MAL_SHOULD_UPDATE_LIST, true) == true) { return if (requireLibraryRefresh) {
val list = getMalAnimeList() val list = getMalAnimeList()
setKey(MAL_CACHED_LIST, list) setKey(MAL_CACHED_LIST, list)
setKey(MAL_SHOULD_UPDATE_LIST, false)
list list
} else { } else {
getMalAnimeListCached() getMalAnimeListCached()
} }
} }
override suspend fun getPersonalLibrary(): SyncAPI.LibraryMetadata {
val list = getMalAnimeListSmart()?.groupBy {
convertToStatus(it.list_status?.status ?: "").stringRes
}?.mapValues { group ->
group.value.map { it.toLibraryItem() }
} ?: emptyMap()
// To fill empty lists when MAL does not return them
val baseMap =
MalStatusType.values().filter { it.value >= 0 }.associate {
it.stringRes to emptyList<SyncAPI.LibraryItem>()
}
return SyncAPI.LibraryMetadata(
(baseMap + list).map { SyncAPI.LibraryList(txt(it.key), it.value) },
setOf(
ListSorting.AlphabeticalA,
ListSorting.AlphabeticalZ,
ListSorting.UpdatedNew,
ListSorting.UpdatedOld,
ListSorting.RatingHigh,
ListSorting.RatingLow,
)
)
}
private suspend fun getMalAnimeList(): Array<Data> { private suspend fun getMalAnimeList(): Array<Data> {
checkMalToken() checkMalToken()
var offset = 0 var offset = 0
@ -440,10 +527,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return fullList.toTypedArray() return fullList.toTypedArray()
} }
fun convertToStatus(string: String): MalStatusType {
return fromIntToAnimeStatus(malStatusAsString.indexOf(string))
}
private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? { private suspend fun getMalAnimeListSlice(offset: Int = 0): MalList? {
val user = "@me" val user = "@me"
val auth = getAuth() ?: return null val auth = getAuth() ?: return null
@ -557,28 +640,6 @@ class MALApi(index: Int) : AccountManager(index), SyncAPI {
return user return user
} }
enum class MalStatusType(var value: Int) {
Watching(0),
Completed(1),
OnHold(2),
Dropped(3),
PlanToWatch(4),
None(-1)
}
private fun fromIntToAnimeStatus(inp: Int): MalStatusType {//= AniListStatusType.values().first { it.value == inp }
return when (inp) {
-1 -> MalStatusType.None
0 -> MalStatusType.Watching
1 -> MalStatusType.Completed
2 -> MalStatusType.OnHold
3 -> MalStatusType.Dropped
4 -> MalStatusType.PlanToWatch
5 -> MalStatusType.Watching
else -> MalStatusType.None
}
}
private suspend fun setScoreRequest( private suspend fun setScoreRequest(
id: Int, id: Int,
status: MalStatusType? = null, status: MalStatusType? = null,

View file

@ -15,6 +15,8 @@ import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager import com.lagradost.cloudstream3.syncproviders.InAppAuthAPIManager
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi { class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi {
override val idPrefix = "opensubtitles" override val idPrefix = "opensubtitles"
@ -164,7 +166,7 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
val fixedLang = fixLanguage(query.lang) val fixedLang = fixLanguage(query.lang)
val imdbId = query.imdb ?: 0 val imdbId = query.imdb ?: 0
val queryText = query.query.replace(" ", "+") 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
@ -175,7 +177,7 @@ 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=${fixedLang}$yearQuery$epQuery$seasonQuery"
false -> "$host/subtitles?query=$queryText&languages=${fixedLang}$yearQuery$epQuery$seasonQuery" false -> "$host/subtitles?query=${queryText}&languages=${fixedLang}$yearQuery$epQuery$seasonQuery"
} }
val req = app.get( val req = app.get(
@ -198,9 +200,13 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
it.data?.forEach { item -> it.data?.forEach { item ->
val attr = item.attributes ?: return@forEach val attr = item.attributes ?: return@forEach
val featureDetails = attr.featDetails val featureDetails = attr.featDetails
//Use filename as name, if its valid
val filename = attr.files?.firstNotNullOfOrNull { subfile ->
subfile.fileName
}
//Use any valid name/title in hierarchy //Use any valid name/title in hierarchy
val name = featureDetails?.movieName ?: featureDetails?.title val name = filename ?: featureDetails?.movieName ?: featureDetails?.title
?: featureDetails?.parentTitle ?: attr.release ?: "" ?: featureDetails?.parentTitle ?: attr.release ?: query.query
val lang = fixLanguageReverse(attr.language)?: "" val lang = fixLanguageReverse(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
@ -328,4 +334,4 @@ class OpenSubtitlesApi(index: Int) : InAppAuthAPIManager(index), AbstractSubApi
@JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null, @JsonProperty("parent_tmdb_id") var parentTmdbId: Int? = null,
@JsonProperty("parent_feature_id") var parentFeatureId: Int? = null @JsonProperty("parent_feature_id") var parentFeatureId: Int? = null
) )
} }

View file

@ -1,10 +1,19 @@
package com.lagradost.cloudstream3.ui package com.lagradost.cloudstream3.ui
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
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.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
class APIRepository(val api: MainAPI) { class APIRepository(val api: MainAPI) {
companion object { companion object {
@ -24,20 +33,67 @@ class APIRepository(val api: MainAPI) {
fun isInvalidData(data: String): Boolean { fun isInvalidData(data: String): Boolean {
return data.isEmpty() || data == "[]" || data == "about:blank" return data.isEmpty() || data == "[]" || data == "about:blank"
} }
data class SavedLoadResponse(
val unixTime: Long,
val response: LoadResponse,
val hash: Pair<String, String>
)
private val cache = threadSafeListOf<SavedLoadResponse>()
private var cacheIndex: Int = 0
const val cacheSize = 20
}
private fun afterPluginsLoaded(forceReload: Boolean) {
if (forceReload) {
synchronized(cache) {
cache.clear()
}
}
}
init {
afterPluginsLoadedEvent += ::afterPluginsLoaded
} }
val hasMainPage = api.hasMainPage val hasMainPage = api.hasMainPage
val providerType = api.providerType
val name = api.name val name = api.name
val mainUrl = api.mainUrl val mainUrl = api.mainUrl
val mainPage = api.mainPage val mainPage = api.mainPage
val hasQuickSearch = api.hasQuickSearch val hasQuickSearch = api.hasQuickSearch
val vpnStatus = api.vpnStatus val vpnStatus = api.vpnStatus
val providerType = api.providerType
suspend fun load(url: String): Resource<LoadResponse> { suspend fun load(url: String): Resource<LoadResponse> {
return safeApiCall { return safeApiCall {
if (isInvalidData(url)) throw ErrorLoadingException() if (isInvalidData(url)) throw ErrorLoadingException()
api.load(api.fixUrl(url)) ?: throw ErrorLoadingException() val fixedUrl = api.fixUrl(url)
val lookingForHash = Pair(api.name, fixedUrl)
synchronized(cache) {
for (item in cache) {
// 10 min save
if (item.hash == lookingForHash && (unixTime - item.unixTime) < 60 * 10) {
return@safeApiCall item.response
}
}
}
api.load(fixedUrl)?.also { response ->
// Remove all blank tags as early as possible
response.tags = response.tags?.filter { it.isNotBlank() }
val add = SavedLoadResponse(unixTime, response, lookingForHash)
synchronized(cache) {
if (cache.size > cacheSize) {
cache[cacheIndex] = add // rolling cache
cacheIndex = (cacheIndex + 1) % cacheSize
} else {
cache.add(add)
}
}
} ?: throw ErrorLoadingException()
} }
} }
@ -62,12 +118,48 @@ class APIRepository(val api: MainAPI) {
} }
} }
suspend fun waitForHomeDelay() {
val delta = api.sequentialMainPageScrollDelay + api.lastHomepageRequest - unixTimeMS
if (delta < 0) return
delay(delta)
}
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 {
api.lastHomepageRequest = unixTimeMS
nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data -> nameIndex?.let { api.mainPage.getOrNull(it) }?.let { data ->
listOf(api.getMainPage(page, MainPageRequest(data.name, data.data))) listOf(
} ?: api.mainPage.apmap { data -> api.getMainPage(
api.getMainPage(page, MainPageRequest(data.name, data.data)) page,
MainPageRequest(data.name, data.data, data.horizontalImages)
)
)
} ?: run {
if (api.sequentialMainPage) {
var first = true
api.mainPage.map { data ->
if (!first) // dont want to sleep on first request
delay(api.sequentialMainPageDelay)
first = false
api.getMainPage(
page,
MainPageRequest(data.name, data.data, data.horizontalImages)
)
}
} else {
with(CoroutineScope(coroutineContext)) {
api.mainPage.map { data ->
async {
api.getMainPage(
page,
MainPageRequest(data.name, data.data, data.horizontalImages)
)
}
}.map { it.await() }
}
}
} }
} }
} }

View file

@ -7,7 +7,8 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs import kotlin.math.abs
class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManager(context, _spanCount) { class GrdLayoutManager(val context: Context, _spanCount: Int) :
GridLayoutManager(context, _spanCount) {
override fun onFocusSearchFailed( override fun onFocusSearchFailed(
focused: View, focused: View,
focusDirection: Int, focusDirection: Int,
@ -34,7 +35,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : GridLayoutManage
val pos = maxOf(0, getPosition(focused!!) - 2) val pos = maxOf(0, getPosition(focused!!) - 2)
parent.scrollToPosition(pos) parent.scrollToPosition(pos)
super.onRequestChildFocus(parent, state, child, focused) super.onRequestChildFocus(parent, state, child, focused)
} catch (e: Exception){ } catch (e: Exception) {
false false
} }
} }

View file

@ -150,7 +150,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
} else { } else {
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply { ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
val font = TextTrackStyle() val font = TextTrackStyle()
font.fontFamily = fontFamily ?: "Google Sans" font.setFontFamily(fontFamily ?: "Google Sans")
fontGenericFamily?.let { fontGenericFamily?.let {
font.fontGenericFamily = it font.fontGenericFamily = it
} }
@ -183,7 +183,9 @@ 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 = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }.toTypedArray() val sortingMethods =
items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
.toTypedArray()
val sotringIndex = items.indexOfFirst { it.url == contentUrl } val sotringIndex = items.indexOfFirst { it.url == contentUrl }
val arrayAdapter = val arrayAdapter =
@ -279,7 +281,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
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.setViewPos(epData.id, currentPosition, currentDuration)
} catch (t : Throwable) { } catch (t: Throwable) {
logError(t) logError(t)
} }
@ -358,10 +360,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
} }
} }
override fun onSessionConnected(castSession: CastSession?) { override fun onSessionConnected(castSession: CastSession) {
castSession?.let { super.onSessionConnected(castSession)
super.onSessionConnected(it)
}
remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject()) remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject())
} }
} }

View file

@ -69,7 +69,7 @@ class EasterEggMonke : AppCompatActivity() {
set.duration = (Math.random() * 1500 + 2500).toLong() set.duration = (Math.random() * 1500 + 2500).toLong()
set.addListener(object : AnimatorListenerAdapter() { set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator) {
frame.removeView(newStar) frame.removeView(newStar)
} }
}) })

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