forked from recloudstream/cloudstream
Compare commits
298 commits
resource-a
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
67b0549fd2 | ||
|
52d495f425 | ||
|
0cbee70683 | ||
|
4235c826a5 | ||
|
5245eff6e1 | ||
|
9c40abc4d3 | ||
|
019399952f | ||
|
cc99899cf1 | ||
|
8fff809b79 | ||
|
67318a62a3 | ||
|
288c5ffa39 | ||
|
8ebf5185a3 | ||
|
7bfcf25df4 | ||
|
2d7126d71f | ||
|
40a4f319b6 | ||
|
19dc1a2456 | ||
|
ac1012bcb8 | ||
|
ec3950ed4f | ||
|
3e2b0f2a17 | ||
|
29174dbb30 | ||
|
7b47f93190 | ||
|
13ee8e21d0 | ||
|
3a5d872545 | ||
|
fab55d82c4 | ||
|
8b2881f5f6 | ||
|
37244ab0f7 | ||
|
e85b31c35d | ||
|
1eaa4620dc | ||
|
76545f55c3 | ||
|
f0515c4dc9 | ||
|
ab324b93e8 | ||
|
d6df24eff2 | ||
|
e5834d485b | ||
|
6524eb220b | ||
|
2926dc6c8e | ||
|
f722785a37 | ||
|
aeab423d29 | ||
|
1da6a92569 | ||
|
b2fa765a2d | ||
|
bec0a2e7b9 | ||
|
51137701f2 | ||
|
5f12d067f9 | ||
|
00a91ca5fb | ||
|
33aecfbba5 | ||
|
0185854682 | ||
|
b4065b69be | ||
|
b6ac155350 | ||
|
aacd57cb5d | ||
|
3dd0fc6c8e | ||
|
135f63afff | ||
|
789cd14ef6 | ||
|
9d0cce47a6 | ||
|
4a8ee55018 | ||
|
df6c395acb | ||
|
1117271a71 | ||
|
7b11b9b585 | ||
|
5c20b479e5 | ||
|
0d2613d183 | ||
|
dd38556102 | ||
|
84493b7f3b | ||
|
4596afee06 | ||
|
3e2c2a5c86 | ||
|
6c646d65a8 | ||
|
329966732f | ||
|
19b2cae851 | ||
|
45eb9758e3 | ||
|
f6be6081dc | ||
|
80f22cea16 | ||
|
bf78fc95c2 | ||
|
a148f347cd | ||
|
ff9942407b | ||
|
0ea624ff14 | ||
|
f939e4cff2 | ||
|
2ff90c03ca | ||
|
9988753432 | ||
|
b0921161a3 | ||
|
490381451b | ||
|
b26a41bdaf | ||
|
c7c5fa250e | ||
|
6e9b1cb855 | ||
|
fd2648df45 | ||
|
9905618a47 | ||
|
2771dcb612 | ||
|
3c82548c20 | ||
|
9d11dc76a1 | ||
|
2a1311673a | ||
|
83d2e692e0 | ||
|
b2389bf14c | ||
|
b2b16fccc5 | ||
|
01f1edab3c | ||
|
5050ff65c0 | ||
|
de720983a6 | ||
|
0b4de81811 | ||
|
60aca3ebdc | ||
|
65fda1889c | ||
|
b2b894caa9 | ||
|
c7e2a19f5d | ||
|
9f18cbbc20 | ||
|
1994edb96c | ||
|
c058409f9d | ||
|
3ecaf47c9e | ||
|
b8248d1053 | ||
|
89c5cb8a46 | ||
|
9fd2e84c7a | ||
|
8e928a8a2b | ||
|
49d672718d | ||
|
a8352d3f64 | ||
|
42f90a79c4 | ||
|
cd8c5966e6 | ||
|
307d4dd494 | ||
|
d606f84545 | ||
|
60c1eb2579 | ||
|
5c8a667e9e | ||
|
2674d370a2 | ||
|
868bb8500f | ||
|
a87bbd3cfc | ||
|
92d03fc163 | ||
|
06c2cf86ec | ||
|
0ebc12e29b | ||
|
308affb6aa | ||
|
75cc4f6dfa | ||
|
61ab957e35 | ||
|
36e780f7c9 | ||
|
e7d37aa07c | ||
|
c57fce2abc | ||
|
657971d008 | ||
|
0afb6b62aa | ||
|
e362795493 | ||
|
8712f08bb1 | ||
|
591ac137f9 | ||
|
dee269ce5e | ||
|
4926c91f6c | ||
|
2b43342854 | ||
|
6e61fe5f3e | ||
|
1e8277b087 | ||
|
710885a3b7 | ||
|
fbb7046390 | ||
|
714062c6d4 | ||
|
83132f183a | ||
|
79c8b4e523 | ||
|
c6749bf988 | ||
|
7019631146 | ||
|
4440096ea4 | ||
|
2a32f62fe3 | ||
|
7982f8c491 | ||
|
d6af1e4ab6 | ||
|
53b06612c1 | ||
|
9fc5c5352e | ||
|
5f1e790163 | ||
|
0073ad8c81 | ||
|
6db688e0bf | ||
|
c11bab4a51 | ||
|
e71b70b6a0 | ||
|
7cf9c640b8 | ||
|
9c956f68f9 | ||
|
2ba78eb37e | ||
|
9e059af0bb | ||
|
871dcf7171 | ||
|
4f4061961a | ||
|
5b26c998b4 | ||
|
5953420774 | ||
|
f3e7a5daa6 | ||
|
b6b7cceea5 | ||
|
23973042f4 | ||
|
a2dbabdb6e | ||
|
53519381d7 | ||
|
a522ef0edb | ||
|
d727099c29 | ||
|
e2fc946d91 | ||
|
a1f5786f02 | ||
|
363906cf3b | ||
|
50fc8d0ffb | ||
|
1e636c8b08 | ||
|
492c950b7a | ||
|
4d13494a93 | ||
|
956c693d1b | ||
|
6246d984a1 | ||
|
495d02d583 | ||
|
304b103e32 | ||
|
5af1a0e433 | ||
|
3fdf41869e | ||
|
7362ac9f64 | ||
|
751175b3f9 | ||
|
c11f0c101b | ||
|
7c4f177e47 | ||
|
0d7c20e3bd | ||
|
95f4a15864 | ||
|
20ac21c25f | ||
|
4f54bf3ae4 | ||
|
f7b623ffc7 | ||
|
4b0b6f6f20 | ||
|
0b17862049 | ||
|
56c79e3b6a | ||
|
6d13cf0b01 | ||
|
70dcc96026 | ||
|
3fa82cdba7 | ||
|
e7d7639776 | ||
|
514e250d68 | ||
|
5c3652d1e9 | ||
|
2222a1b07b | ||
|
42d1dd9f7d | ||
|
eb90b79bf9 | ||
|
b79e2d768f | ||
|
e215747749 | ||
|
3f658a375e | ||
|
723c554bc8 | ||
|
58593ac8da | ||
|
c513708d74 | ||
|
9be50eb28b | ||
|
789f3db554 | ||
|
e21c8f8038 | ||
|
9bca7a0780 | ||
|
a8f3d18c2e | ||
|
263f74fb9c | ||
|
dbd91d788c | ||
|
c9fe7c79dc | ||
|
924d797e07 | ||
|
2b29e8078f | ||
|
9a93b375f3 | ||
|
30316107c8 | ||
|
cf22ada266 | ||
|
456cd2e6e2 | ||
|
81adb10c1f | ||
|
639de891c6 | ||
|
2e7823034b | ||
|
e95d117ebc | ||
|
1226426389 | ||
|
aef6f93efe | ||
|
3e2c53a5b7 | ||
|
8fa00f4ca9 | ||
|
4fb65e7242 | ||
|
60bcbf0060 | ||
|
60a2f7c1c5 | ||
|
9e67e856a0 | ||
|
c10ec34ab8 | ||
|
f84259f898 | ||
|
8810d5abd6 | ||
|
bc03f6ebb5 | ||
|
344f974af2 | ||
|
c09b6881e5 | ||
|
4a193d5d27 | ||
|
b57a7c3772 | ||
|
e5be703a47 | ||
|
6308fd0fec | ||
|
3d3c85a1ad | ||
|
28b4456dfd | ||
|
f268418190 | ||
|
1c494f0ce2 | ||
|
ddae2ddf3c | ||
|
64303eab8d | ||
|
a201f5e4f8 | ||
|
0e8aacf989 | ||
|
e72f3ff8b9 | ||
|
8406f6de65 | ||
|
7272dc67b7 | ||
|
d349190238 | ||
|
47b79550f1 | ||
|
65b5efb848 | ||
|
fd7cf51f57 | ||
|
617fc4a295 | ||
|
9ee0653ecf | ||
|
c18856c8c3 | ||
|
47da6efb59 | ||
|
997420a942 | ||
|
6b586388b9 | ||
|
93cbd29f3d | ||
|
7e750a40e0 | ||
|
fa6a620bf9 | ||
|
c9c339795a | ||
|
49ebd27f80 | ||
|
0f625142da | ||
|
044822040f | ||
|
ecd363992c | ||
|
7cbcee4d48 | ||
|
7f71eef755 | ||
|
4c309bbb2a | ||
|
544f277d0c | ||
|
034bad289f | ||
|
570fdb5af4 | ||
|
6a5286e363 | ||
|
4c0f6df1a2 | ||
|
4848e43c97 | ||
|
a58ca547d7 | ||
|
f49d9de09b | ||
|
cf08c958eb | ||
|
e67d248f7f | ||
|
63c713fc68 | ||
|
1228701f0e | ||
|
661f8c3c4e | ||
|
af4d57e842 | ||
|
a565319ecb | ||
|
fc7e39e3cc | ||
|
a43e950a48 | ||
|
98ef6a3f16 | ||
|
b3ff3ec086 | ||
|
e2118c3271 | ||
|
ddcdb04d78 | ||
|
61fb302a37 |
326 changed files with 24180 additions and 9936 deletions
BIN
.github/downloads.jpg
vendored
BIN
.github/downloads.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 58 KiB |
BIN
.github/home.jpg
vendored
BIN
.github/home.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 136 KiB |
63
.github/locales.py
vendored
Normal file
63
.github/locales.py
vendored
Normal 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
BIN
.github/player.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 48 KiB |
BIN
.github/results.jpg
vendored
BIN
.github/results.jpg
vendored
Binary file not shown.
Before Width: | Height: | Size: 96 KiB |
BIN
.github/search.jpg
vendored
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
76
.github/workflows/build_to_archive.yml
vendored
Normal 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
|
29
.github/workflows/issue_action.yml
vendored
29
.github/workflows/issue_action.yml
vendored
|
@ -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:
|
||||||
|
|
4
.github/workflows/prerelease.yml
vendored
4
.github/workflows/prerelease.yml
vendored
|
@ -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
|
||||||
|
|
4
.github/workflows/pull_request.yml
vendored
4
.github/workflows/pull_request.yml
vendored
|
@ -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
42
.github/workflows/update_locales.yml
vendored
Normal 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
|
|
@ -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>
|
37
README.md
37
README.md
|
@ -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
|
|
||||||
|
|
||||||
|
|
233
app/build.gradle
233
app/build.gradle
|
@ -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
258
app/build.gradle.kts
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
@ -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
|
||||||
|
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" /> <!– Used for getting if vlc is installed –> -->
|
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
||||||
<!-- 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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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-")
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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?
|
||||||
)
|
) */
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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\("(.*?)"""")
|
||||||
|
|
|
@ -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*['"](.*?)['"]""")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")) {
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 != ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue