Compare commits

..

33 commits

Author SHA1 Message Date
reduplicated
7a1cfc2f2b mini fix to text in popup 2023-02-02 18:30:48 +01:00
Blatzar
a8bcf2dcb2 Merge remote-tracking branch 'origin/anilist' into anilist 2023-01-28 23:36:30 +01:00
Blatzar
df95931d0a future proofed SyncID 2023-01-28 23:36:24 +01:00
LagradOst
46732ea917
Merge branch 'master' into anilist 2023-01-28 22:21:44 +00:00
reduplicated
cd166cb0d3 mini fix 2023-01-28 23:08:55 +01:00
Blatzar
cbeeff5cca fix library statusbar 2023-01-28 22:49:58 +01:00
Blatzar
e7732cc15f Smoother fast scrolling in library & no labels 2023-01-28 14:57:06 +01:00
Blatzar
e875d520de Fixed loading padding 2023-01-28 01:11:13 +01:00
Blatzar
ef7e0ecf0d Fixed rotation and loading delay 2023-01-28 00:54:47 +01:00
Blatzar
7512dbcbf8 Library loading fixes 2023-01-27 22:26:24 +01:00
Blatzar
1f67644290 Fix crash and sorting methods in library 2023-01-27 14:54:24 +01:00
Blatzar
0aca996bc0 Better notice when not logged in in library 2023-01-26 18:09:22 +01:00
Blatzar
26320bb535 Reworked backend to make list titles use UiText 2023-01-25 19:20:53 +01:00
reduplicated
9bc90438ec colors go brrr 2023-01-25 16:54:57 +01:00
Blatzar
dd293e9564 Added visible loading in library and fix reloading on sync 2023-01-25 12:54:51 +01:00
Blatzar
f6c609cfdf Fix embarrassing crash when using different languages in library 2023-01-25 00:55:01 +01:00
Blatzar
4a0989c9c5 fixed buggy page transform in library 2023-01-24 23:04:03 +01:00
Blatzar
847c70a127 Add search option in library 2023-01-24 22:38:06 +01:00
Blatzar
20ada8da0d Fix library bug and remove it from TV 2023-01-24 22:31:31 +01:00
Blatzar
daaab9d35b More library fixes 2023-01-24 20:27:22 +01:00
Blatzar
c4afb5e673 More library fixes 2023-01-22 23:25:29 +01:00
Blatzar
0dab9959e1 Fixes for breakages in the latest merge 2023-01-22 22:28:51 +01:00
LagradOst
e945600940
Merge pull request #325 from recloudstream/library
Bump anilist base to latest
2023-01-22 18:18:14 +00:00
LagradOst
9edb7f8999
Merge branch 'anilist' into library 2023-01-22 18:17:22 +00:00
Blatzar
7053d77ca1 Moved stuff to R.strings and fixed reloading the page 2023-01-22 19:09:37 +01:00
Blatzar
30dbd3920e Fixed "open with" in library 2023-01-22 18:54:16 +01:00
Blatzar
019e9a0c4f More work on local list support 2023-01-21 14:21:36 +01:00
Blatzar
7cd6dd3fe6 WIP local list support 2023-01-21 13:14:17 +01:00
Blatzar
8cdc31ffcd WIP adding open with specified extension 2022-12-28 20:26:59 +01:00
Blatzar
2a2a0a26e7 Add list switcher 2022-12-18 19:22:35 +01:00
Blatzar
67a1c447ae backend fixes for opening from sync urls 2022-12-05 20:49:45 +01:00
Blatzar
05dc032df6 library fixes 2022-11-20 17:05:11 +01:00
Blatzar
67fe6730e0 library work in progress 2022-11-19 20:44:12 +01:00
1070 changed files with 42733 additions and 122222 deletions

View file

@ -80,13 +80,13 @@ body:
label: Acknowledgements label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you haven't done these steps.
options: options:
- label: I am sure my issue is related to the app and **NOT some extension**.
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true required: true
- label: I have written a short but informative title. - label: I have written a short but informative title.
required: true required: true
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**. - label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
required: true required: true
- label: If related to a provider, I have checked the site and it works, but not the app.
required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true

View file

@ -2,7 +2,7 @@ blank_issues_enabled: false
contact_links: contact_links:
- name: Request a new provider or report bug with an existing provider - name: Request a new provider or report bug with an existing provider
url: https://github.com/recloudstream url: https://github.com/recloudstream
about: EXTREMELY IMPORTANT - Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord. about: Please do not report any provider bugs here or request new providers. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the discord.
- name: Discord - name: Discord
url: https://discord.gg/5Hus6fM url: https://discord.gg/5Hus6fM
about: Join our discord for faster support on smaller issues. about: Join our discord for faster support on smaller issues.

View file

@ -27,7 +27,9 @@ body:
label: Acknowledgements label: Acknowledgements
description: Your issue will be closed if you haven't done these steps. description: Your issue will be closed if you haven't done these steps.
options: options:
- label: My suggestion is **NOT** about adding a new provider
required: true
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue. - label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true required: true
- label: I have written a short but informative title.
required: true
- label: I will fill out all of the requested information in this form.
required: true

BIN
.github/downloads.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
.github/home.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

46
.github/locales.py vendored
View file

@ -1,14 +1,13 @@
import re import re
import glob import glob
import requests 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" SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
START_MARKER = "/* begin language list */" START_MARKER = "/* begin language list */"
END_MARKER = "/* end language list */" END_MARKER = "/* end language list */"
XML_NAME = "app/src/main/res/values-b+" XML_NAME = "app/src/main/res/values-"
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json" ISO_MAP_URL = "https://gist.githubusercontent.com/Josantonius/b455e315bc7f790d14b136d61d9ae469/raw"
INDENT = " "*4 INDENT = " "*4
iso_map = requests.get(ISO_MAP_URL, timeout=300).json() iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
@ -20,46 +19,29 @@ rest, after_src = rest.split(END_MARKER)
# Load already added langs # Load already added langs
languages = {} languages = {}
for lang in re.finditer(r'Pair\("(.*)", "(.*)"\)', rest): for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
name, iso = lang.groups() flag, name, iso = lang.groups()
languages[iso] = name languages[iso] = (flag, name)
# Add not yet added langs # Add not yet added langs
for folder in glob.glob(f"{XML_NAME}*"): for folder in glob.glob(f"{XML_NAME}*"):
iso = folder[len(XML_NAME):].replace("+", "-") iso = folder[len(XML_NAME):]
if iso not in languages.keys(): if iso not in languages.keys():
entry = iso_map.get(iso.lower(), {'nativeName':iso}) # fallback to iso code if not found languages[iso] = ("", iso_map.get(iso.lower(),iso))
languages[iso] = entry['nativeName'].split(',')[0] # first name if there are multiple
# Create pairs # Create triples
pairs = [] triples = []
for iso in sorted(languages, key=lambda iso: languages[iso].lower()): # sort by language name for iso in sorted(languages.keys()):
name = languages[iso] flag, name = languages[iso]
pairs.append(f'{INDENT}Pair("{name}", "{iso}"),') triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
# Update settings file # Update settings file
open(SETTINGS_PATH, "w+",encoding='utf-8').write( open(SETTINGS_PATH, "w+",encoding='utf-8').write(
before_src + before_src +
START_MARKER + START_MARKER +
"\n" + "\n" +
"\n".join(pairs) + "\n".join(triples) +
"\n" + "\n" +
END_MARKER + END_MARKER +
after_src 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 not child.text:
continue
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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
.github/results.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
.github/search.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

View file

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

View file

@ -1,67 +1,64 @@
name: Dokka name: Dokka
# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#concurrency
concurrency:
group: "dokka"
cancel-in-progress: true
on: on:
push: push:
branches: [ master ] branches:
# choose your default branch
- master
- main
paths-ignore: paths-ignore:
- '*.md' - '*.md'
permissions:
contents: read
concurrency:
group: "dokka"
cancel-in-progress: true
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/dokka" repository: "recloudstream/dokka"
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@master
with: with:
path: "src" path: "src"
- name: Checkout dokka - name: Checkout dokka
uses: actions/checkout@v6 uses: actions/checkout@master
with: with:
repository: "recloudstream/dokka" repository: "recloudstream/dokka"
path: "dokka" path: "dokka"
token: ${{ steps.generate_token.outputs.token }} token: ${{ steps.generate_token.outputs.token }}
- name: Clean old builds - name: Clean old builds
run: | run: |
cd $GITHUB_WORKSPACE/dokka/ cd $GITHUB_WORKSPACE/dokka/
rm -rf "./app" rm -rf "./-cloudstream"
rm -rf "./library"
- name: Set up JDK 17 - name: Setup JDK 11
uses: actions/setup-java@v5 uses: actions/setup-java@v1
with: with:
distribution: temurin java-version: 11
java-version: 17
- name: Setup Gradle - name: Setup Android SDK
uses: gradle/actions/setup-gradle@v5 uses: android-actions/setup-android@v2
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Generate Dokka - name: Generate Dokka
run: | run: |
cd $GITHUB_WORKSPACE/src/ cd $GITHUB_WORKSPACE/src/
chmod +x gradlew chmod +x gradlew
./gradlew docs:dokkaGeneratePublicationHtml ./gradlew app:dokkaHtml
- name: Copy Dokka - name: Copy Dokka
run: cp -r $GITHUB_WORKSPACE/src/docs/build/dokka/html/* $GITHUB_WORKSPACE/dokka/ run: |
cp -r $GITHUB_WORKSPACE/src/app/build/dokka/html/* $GITHUB_WORKSPACE/dokka/
- name: Push builds - name: Push builds
run: | run: |

View file

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

63
.github/workflows/issue_action.yml vendored Normal file
View file

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

View file

@ -8,36 +8,29 @@ on:
- '*.json' - '*.json'
- '**/wcokey.txt' - '**/wcokey.txt'
concurrency: concurrency:
group: "pre-release" group: "pre-release"
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: write
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Generate access token - name: Generate access token
id: generate_token id: generate_token
uses: tibdex/github-app-token@v2 uses: tibdex/github-app-token@v1
with: with:
app_id: ${{ secrets.GH_APP_ID }} app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets" repository: "recloudstream/secrets"
- uses: actions/checkout@v2
- uses: actions/checkout@v6 - name: Set up JDK 11
uses: actions/setup-java@v2
- name: Set up JDK 17
uses: actions/setup-java@v5
with: with:
distribution: temurin java-version: '11'
java-version: 17 distribution: 'adopt'
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew
- name: Fetch keystore - name: Fetch keystore
id: fetch_keystore id: fetch_keystore
run: | run: |
@ -48,27 +41,15 @@ jobs:
KEY_PWD="$(cat keystore_password.txt)" KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}" echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v5
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Run Gradle - name: Run Gradle
run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar run: |
./gradlew assemblePrerelease makeJar androidSourcesJar
env: env:
SIGNING_KEY_ALIAS: "key0" SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }} SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
TRAKT_CLIENT_ID: ${{ secrets.TRAKT_CLIENT_ID }}
MDL_API_KEY: ${{ secrets.MDL_API_KEY }}
MAL_KEY: ${{ secrets.MAL_KEY }}
ANILIST_KEY: ${{ secrets.ANILIST_KEY }}
- name: Create pre-release - name: Create pre-release
uses: marvinpinto/action-automatic-releases@latest uses: "marvinpinto/action-automatic-releases@latest"
with: with:
repo_token: "${{ secrets.GITHUB_TOKEN }}" repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "pre-release" automatic_release_tag: "pre-release"

View file

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

View file

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

220
.gitignore vendored
View file

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

1
.idea/.name generated Normal file
View file

@ -0,0 +1 @@
CloudStream

123
.idea/codeStyles/Project.xml generated Normal file
View file

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

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View file

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

6
.idea/compiler.xml generated Normal file
View file

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

7
.idea/discord.xml generated Normal file
View file

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

21
.idea/gradle.xml generated Normal file
View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="11" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

40
.idea/jarRepositories.xml generated Normal file
View file

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

6
.idea/vcs.xml generated Normal file
View file

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

6
.vscode/settings.json vendored Normal file
View file

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

View file

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

View file

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

104
README.md
View file

@ -1,111 +1,23 @@
# CloudStream # CloudStream
**⚠️ Warning: By default, this app doesn't provide any video sources; you have to install extensions 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.**
[![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM) [![Discord](https://invidget.switchblade.xyz/5Hus6fM)](https://discord.gg/5Hus6fM)
### Features:
## Table of Contents:
+ [About Us:](#about_us)
+ [Installation Steps:](#install_rules)
+ [Contributing:](#contributing)
+ [Issues:](#issues)
+ [Bugs Reports:](#bug_report)
+ [Enhancement:](#enhancment)
+ [Extension Development:](#extensions)
+ [Language Support:](#languages)
+ [Further Sources](#contact_and_sources)
<a id="about_us"></a>
## About us:
**CloudStream is a media center that prioritizes and emphasizes complete freedom and flexibility for users and developers.**
CloudStream is an extension-based multimedia player with tracking support. There are extensions to view videos from:
+ [Librevox (audio-books)](https://librivox.org/)
+ [Youtube](https://www.youtube.com/)
+ [Twitch](https://www.twitch.tv/)
+ [iptv-org (A collection of publicly available IPTV (Internet Protocol television) channels from all over the world.)](https://github.com/iptv-org/iptv)
+ [nginx](https://nginx.org/)
+ And more...
**Please don't create illegal extensions or use any that host any copyrighted media.** For more details about our stance on the DMCA and EUCD, you can read about it on our organization: [reCloudStream](https://github.com/recloudstream)
#### Important Copyright Note:
Our documentation is unmaintained and open to contributions; therefore, apps and sources, extensions in recommended sources, and recommended apps are not officially moderated or endorsed by CloudStream; if you or another copyright owner identify an extension that breaches your copyright, please let us know.
#### Features:
+ **AdFree**, No ads whatsoever + **AdFree**, No ads whatsoever
+ No tracking/analytics + No tracking/analytics
+ Bookmarks + Bookmarks
+ Phone and TV support + Download and stream movies, tv-shows and anime
+ Chromecast + Chromecast
+ Extension system for personal customization
### Screenshots:
<a id="install_rules"></a> <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="./.github/player.jpg" height="200"/>
## Installation:
Our documentation provides the steps to install and configure CloudStream for your streaming needs.
[Getting Started With CloudStream:](https://recloudstream.github.io/csdocs/)
<a id="contributing"></a>
## Contributing:
We **happily** accept any contributions to our project. To find out where you can start contributing towards the project, please look [at our issues tab](/cloudstream/issues)
<a id="issues"></a>
### Issues:
While we **actively** accept issues and pull requests, we do require you fill out an [template](https://github.com/recloudstream/cloudstream/issues/new/choose) for issues. These include the following:
<a id="bug_report"></a>
- [Bug Report Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=bug&projects=&template=application-bug.yml)
- For bug reports, we want as much info as possible, including your downloaded version of CloudeStream, device and updated version (if possible, current API),
expected behavior of the program, and the actual behavior that the program did, most importantly we require clear, reproducible steps of the bug. If your bug can't be reproduced, it is unlikely we'll work on your issue.
<a id="enhancment"></a>
- [Feature Request Template: ](https://github.com/recloudstream/cloudstream/issues/new?assignees=&labels=enhancement&projects=&template=feature-request.yml)
- Before adding a feature request, please check to see if a feature request already has been requested.
### Extensions:
**Further details on creating extensions for CloudStream are found in our documentation.**
[Guide: For Extension Developers](https://recloudstream.github.io/csdocs/devs/gettingstarted/)
<a id="contact_and_sources"></a>
## Further Sources:
As well as providing clear install steps, our [website](https://dweb.link/ipns/cloudstream.on.fleek.co/) includes a wide variety of other tools, such as:
- [Troubleshooting](https://recloudstream.github.io/csdocs/troubleshooting/)
- [Further CloudStream Repositories](https://recloudstream.github.io/csdocs/repositories/)
- Set-Up for other devices, such as:
- [Android TV](https://recloudstream.github.io/csdocs/other-devices/tv/)
- [Windows](https://recloudstream.github.io/csdocs/other-devices/windows/)
- [Linux](https://recloudstream.github.io/csdocs/other-devices/linux/)
- And more...
<a id="languages"> </a>
### Supported languages: ### Supported languages:
Even if you can't contribute to the code or documentation, we always look for those who can contribute to translation and language support. Your contribution is exceptionally appreciated; you can check our translation from the figure below.
<a href="https://hosted.weblate.org/engage/cloudstream/"> <a href="https://hosted.weblate.org/engage/cloudstream/">
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" /> <img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
</a> </a>

View file

@ -1,96 +1,37 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties import com.android.build.gradle.api.BaseVariantOutput
import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier import java.io.ByteArrayOutputStream
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode import java.net.URL
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
plugins { plugins {
alias(libs.plugins.android.application) id("com.android.application")
alias(libs.plugins.dokka) id("kotlin-android")
alias(libs.plugins.kotlin.serialization) id("kotlin-kapt")
id("kotlin-android-extensions")
id("org.jetbrains.dokka")
} }
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get()) val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
abstract class GenerateGitHashTask : DefaultTask() { fun String.execute() = ByteArrayOutputStream().use { baot ->
if (project.exec {
@get:InputFile workingDir = projectDir
@get:PathSensitive(PathSensitivity.RELATIVE) commandLine = this@execute.split(Regex("\\s"))
abstract val headFile: RegularFileProperty standardOutput = baot
}.exitValue == 0)
@get:InputDirectory String(baot.toByteArray()).trim()
@get:PathSensitive(PathSensitivity.RELATIVE) else null
abstract val headsDir: DirectoryProperty
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun generate() {
val head = headFile.get().asFile
val hash = try {
if (head.exists()) {
// Read the commit hash from .git/HEAD
val headContent = head.readText().trim()
if (headContent.startsWith("ref:")) {
val refPath = headContent.substring(5) // e.g., refs/heads/main
val commitFile = File(head.parentFile, refPath)
if (commitFile.exists()) commitFile.readText().trim() else ""
} else headContent // If it's a detached HEAD (commit hash directly)
} else "" // If .git/HEAD doesn't exist
} catch (_: Throwable) {
"" // Just set to an empty string if any exception occurs
}.take(7) // Get the short commit hash
val outFile = outputDir.file("git-hash.txt").get().asFile
outFile.parentFile.mkdirs()
outFile.writeText(hash)
}
}
val generateGitHash = tasks.register<GenerateGitHashTask>("generateGitHash") {
val gitDir = layout.projectDirectory.dir("../.git")
headFile.set(gitDir.file("HEAD"))
headsDir.set(gitDir.dir("refs/heads"))
outputDir.set(layout.buildDirectory.dir("generated/git"))
} }
android { android {
@Suppress("UnstableApiUsage")
testOptions { testOptions {
unitTests.isReturnDefaultValues = true unitTests.isReturnDefaultValues = true
} }
// Looks like google likes to add metadata only they can read https://gitlab.com/IzzyOnDroid/repo/-/work_items/491
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
androidComponents {
onVariants { variant ->
variant.sources.assets?.addGeneratedSourceDirectory(
generateGitHash,
GenerateGitHashTask::outputDir
)
}
}
signingConfigs { signingConfigs {
// We just use SIGNING_KEY_ALIAS here since it won't change create("prerelease") {
// so won't kill the configuration cache. if (prereleaseStoreFile != null) {
if (System.getenv("SIGNING_KEY_ALIAS") != null) { storeFile = file(prereleaseStoreFile)
create("prerelease") {
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
storeFile = prereleaseStoreFile?.let { file(it) }
storePassword = System.getenv("SIGNING_STORE_PASSWORD") storePassword = System.getenv("SIGNING_STORE_PASSWORD")
keyAlias = System.getenv("SIGNING_KEY_ALIAS") keyAlias = System.getenv("SIGNING_KEY_ALIAS")
keyPassword = System.getenv("SIGNING_KEY_PASSWORD") keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
@ -98,46 +39,34 @@ android {
} }
} }
compileSdk = libs.versions.compileSdk.get().toInt() compileSdk = 33
buildToolsVersion = "30.0.3"
defaultConfig { defaultConfig {
applicationId = "com.lagradost.cloudstream3" applicationId = "com.lagradost.cloudstream3"
minSdk = libs.versions.minSdk.get().toInt() minSdk = 21
targetSdk = libs.versions.targetSdk.get().toInt() targetSdk = 33
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get() versionCode = 56
versionName = "3.5.0"
// Reads local.properties resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
val localProperties = gradleLocalProperties(rootDir, project.providers)
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
resValue("bool", "is_prerelease", "false")
buildConfigField(
"long",
"BUILD_DATE",
"${System.currentTimeMillis()}"
)
buildConfigField( buildConfigField(
"String", "String",
"SIMKL_CLIENT_ID", "BUILDDATE",
"\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\"" "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
)
buildConfigField(
"String",
"SIMKL_CLIENT_SECRET",
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
)
buildConfigField(
"String",
"MAL_KEY",
"\"" + (System.getenv("MAL_KEY") ?: localProperties["mal.key"]) + "\""
)
buildConfigField(
"String",
"ANILIST_KEY",
"\"" + (System.getenv("ANILIST_KEY") ?: localProperties["anilist.key"]) + "\""
) )
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
kapt {
includeCompileClasspath = true
}
} }
buildTypes { buildTypes {
@ -145,210 +74,183 @@ android {
isDebuggable = false isDebuggable = false
isMinifyEnabled = false isMinifyEnabled = false
isShrinkResources = false isShrinkResources = false
proguardFiles( proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
debug { debug {
isDebuggable = true isDebuggable = true
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
proguardFiles( proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
flavorDimensions.add("state") flavorDimensions.add("state")
productFlavors { productFlavors {
create("stable") { create("stable") {
dimension = "state" dimension = "state"
resValue("bool", "is_prerelease", "false")
} }
create("prerelease") { create("prerelease") {
dimension = "state" dimension = "state"
resValue("bool", "is_prerelease", "true")
buildConfigField("boolean", "BETA", "true")
applicationIdSuffix = ".prerelease" applicationIdSuffix = ".prerelease"
if (signingConfigs.names.contains("prerelease")) { signingConfig = signingConfigs.getByName("prerelease")
signingConfig = signingConfigs.getByName("prerelease")
} else {
logger.warn("No prerelease signing config!")
}
versionNameSuffix = "-PRE" versionNameSuffix = "-PRE"
versionCode = (System.currentTimeMillis() / 60000).toInt() versionCode = (System.currentTimeMillis() / 60000).toInt()
} }
} }
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.toVersion(javaTarget.target)
targetCompatibility = JavaVersion.toVersion(javaTarget.target)
}
java { sourceCompatibility = JavaVersion.VERSION_1_8
// Use Java 17 toolchain even if a higher JDK runs the build. targetCompatibility = JavaVersion.VERSION_1_8
// We still use Java 8 for now which higher JDKs have deprecated. }
toolchain { kotlinOptions {
languageVersion.set(JavaLanguageVersion.of(libs.versions.jdkToolchain.get())) jvmTarget = "1.8"
} freeCompilerArgs = listOf("-Xjvm-default=compatibility")
} }
lint { lint {
abortOnError = false
checkReleaseBuilds = false checkReleaseBuilds = false
} }
buildFeatures {
buildConfig = true
viewBinding = true
}
packaging {
jniLibs {
// Enables legacy JNI packaging to reduce APK size (similar to builds before minSdk 23).
// Note: This may increase app startup time slightly.
useLegacyPackaging = true
}
}
namespace = "com.lagradost.cloudstream3" namespace = "com.lagradost.cloudstream3"
} }
repositories {
maven("https://jitpack.io")
}
dependencies { dependencies {
// Testing implementation("com.google.android.mediahome:video:1.0.0")
testImplementation(libs.junit) implementation("androidx.test.ext:junit-ktx:1.1.3")
testImplementation(libs.json) testImplementation("org.json:json:20180813")
androidTestImplementation(libs.core)
androidTestImplementation(libs.espresso.core)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.instancio.core)
androidTestImplementation(libs.junit.ktx)
androidTestImplementation(libs.kotlin.test)
// Android Core & Lifecycle implementation("androidx.core:core-ktx:1.8.0")
implementation(libs.core.ktx) implementation("androidx.appcompat:appcompat:1.4.2") // need target 32 for 1.5.0
implementation(libs.activity.ktx)
implementation(libs.annotation)
implementation(libs.appcompat)
implementation(libs.fragment.ktx)
implementation(libs.bundles.lifecycle)
implementation(libs.bundles.navigation)
implementation(libs.kotlinx.collections.immutable)
implementation(libs.kotlinx.serialization.json) // JSON Parser
// Design & UI // dont change this to 1.6.0 it looks ugly af
implementation(libs.preference.ktx) implementation("com.google.android.material:material:1.5.0")
implementation(libs.material) implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation(libs.constraintlayout) 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")
// Coil Image Loading //implementation("io.karn:khttp-android:0.1.2") //okhttp instead
implementation(libs.bundles.coil) // implementation("org.jsoup:jsoup:1.13.1")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
// Media 3 (ExoPlayer) implementation("androidx.preference:preference-ktx:1.2.0")
implementation(libs.bundles.media3)
implementation(libs.video)
// FFmpeg Decoding implementation("com.github.bumptech.glide:glide:4.13.1")
implementation(libs.bundles.nextlib) kapt("com.github.bumptech.glide:compiler:4.13.1")
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
// Anime-db for filler implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation(libs.anime.db)
// PlayBack implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation(libs.colorpicker) // Subtitle Color Picker
implementation(libs.newpipeextractor) // For Trailers
implementation(libs.juniversalchardet) // Subtitle Decoding
// UI Stuff // implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
implementation(libs.shimmer) // Shimmering Effect (Loading Skeleton)
implementation(libs.palette.ktx) // Palette for Images -> Colors
implementation(libs.tvprovider)
implementation(libs.overlappingpanels) // Gestures
implementation(libs.biometric) // Fingerprint Authentication
implementation(libs.previewseekbar.media3) // SeekBar Preview
implementation(libs.qrcode.kotlin) // QR Code for PIN Auth on TV
// Extensions & Other Libs // Exoplayer
implementation(libs.jsoup) // HTML Parser implementation("com.google.android.exoplayer:exoplayer:2.18.2")
implementation(libs.ksoup) // HTML Parser implementation("com.google.android.exoplayer:extension-cast:2.18.2")
implementation(libs.rhino) // Run JavaScript implementation("com.google.android.exoplayer:extension-mediasession:2.18.2")
implementation(libs.safefile) // To Prevent the URI File Fu*kery implementation("com.google.android.exoplayer:extension-okhttp:2.18.2")
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9
implementation(libs.jackson.module.kotlin) // JSON Parser
implementation(libs.zipline)
// Deprecated; will be removed once extensions have time to migrate from using it //implementation("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.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.4.1")
// 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") implementation("me.xdrop:fuzzywuzzy:1.4.0")
// Torrent Support // color pallette for images -> colors
implementation(libs.torrentserver) implementation("androidx.palette:palette-ktx:1.0.0")
// Downloading & Networking
implementation(libs.work.runtime.ktx)
implementation(libs.nicehttp) // HTTP Lib
implementation(project(":library"))
} }
tasks.register<Jar>("androidSourcesJar") { tasks.register("androidSourcesJar", Jar::class) {
archiveClassifier.set("sources") archiveClassifier.set("sources")
from(android.sourceSets.getByName("main").java.directories) // Full Sources from(android.sourceSets.getByName("main").java.srcDirs) //full sources
} }
tasks.register<Copy>("copyJar") { // this is used by the gradlew plugin
dependsOn("build", ":library:jvmJar") tasks.register("makeJar", Copy::class) {
from( from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
"build/intermediates/compile_app_classes_jar/prereleaseDebug/bundlePrereleaseDebugClassesToCompileJar", into("build")
"../library/build/libs" include("classes.jar")
) dependsOn("build")
into("build/app-classes")
include("classes.jar", "library-jvm*.jar")
// Remove the version
rename("library-jvm.*.jar", "library-jvm.jar")
} }
// Merge the app classes and the library classes into classes.jar tasks.withType<DokkaTask>().configureEach {
tasks.register<Jar>("makeJar") { moduleName.set("Cloudstream")
// Duplicates cause hard to catch errors, better to fail at compile time.
duplicatesStrategy = DuplicatesStrategy.FAIL
dependsOn(tasks.getByName("copyJar"))
from(
zipTree("build/app-classes/classes.jar"),
zipTree("build/app-classes/library-jvm.jar")
)
destinationDirectory.set(layout.buildDirectory)
archiveBaseName = "classes"
}
tasks.withType<KotlinJvmCompile> {
compilerOptions {
jvmTarget.set(javaTarget)
jvmDefault.set(JvmDefaultMode.ENABLE)
freeCompilerArgs.add("-Xannotation-default-target=param-property")
optIn.addAll(
"com.lagradost.cloudstream3.InternalAPI",
"com.lagradost.cloudstream3.Prerelease",
"kotlin.uuid.ExperimentalUuidApi",
)
}
}
dokka {
moduleName = "App"
dokkaSourceSets { dokkaSourceSets {
configureEach { named("main") {
suppress = name != "prereleaseDebug"
analysisPlatform = KotlinPlatform.JVM
displayName = "JVM"
documentedVisibilities(
VisibilityModifier.Public,
VisibilityModifier.Protected
)
sourceLink { sourceLink {
localDirectory = file("..") // Unix based directory relative path to the root of the project (where you execute gradle respectively).
remoteUrl("https://github.com/recloudstream/cloudstream/tree/master") localDirectory.set(file("src/main/java"))
remoteLineSuffix = "#L"
// URL showing where the source code can be accessed through the web browser
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
// Suffix which is used to append the line number to the URL. Use #L for GitHub
remoteLineSuffix.set("#L")
} }
} }
} }
} }

View file

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

View file

@ -1,143 +1,173 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.app.Activity
import android.os.Bundle
import android.os.PersistableBundle
import android.view.LayoutInflater
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
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
import org.junit.runner.RunWith import org.junit.runner.RunWith
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
class TestApplication : Activity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
}
}
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
private fun getAllProviders(): Array<MainAPI> { //@Test
println("Providers: ${APIHolder.allProviders.size}") //fun useAppContext() {
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView } // // Context of the app under test.
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
//}
private fun getAllProviders(): List<MainAPI> {
return APIHolder.allProviders //.filter { !it.usesWebView }
} }
@Throws private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
private inline fun <reified T : ViewBinding> testAllLayouts( Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
activity: Activity, if (url == null) return true
vararg layouts: Int var linksLoaded = 0
) { try {
val success = api.loadLinks(url, false, {}) { link ->
val bind = T::class.java.methods.first { it.name == "bind" } Assert.assertTrue(
val inflater = LayoutInflater.from(activity) "Api ${api.name} returns link with invalid Quality",
for (layout in layouts) { Qualities.values().map { it.value }.contains(link.quality)
val root = inflater.inflate(layout, null, false) )
bind.invoke(null, root) 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
} }
@Test private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
@Throws val searchQueries = listOf("over", "iron", "guy")
fun layoutTest() { var correctResponses = 0
ActivityScenario.launch(MainActivity::class.java).use { scenario -> var searchResult: List<SearchResponse>? = null
scenario.onActivity { activity: MainActivity -> for (query in searchQueries) {
// FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same val response = try {
//testAllLayouts<FragmentHomeHeadBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv) api.search(query)
//testAllLayouts<FragmentHomeHeadTvBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv) } catch (e: Exception) {
if (e.cause is NotImplementedError) {
// main cant be tested Assert.fail("Provider has not implemented .search")
// testAllLayouts<ActivityMainTvBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv) }
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv) logError(e)
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv) null
}
testAllLayouts<BottomResultviewPreviewBinding>(activity, R.layout.bottom_resultview_preview,R.layout.bottom_resultview_preview_tv) if (!response.isNullOrEmpty()) {
correctResponses++
testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv) if (searchResult == null) {
testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv) searchResult = response
}
// testAllLayouts<FragmentResultBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
// testAllLayouts<FragmentResultTvBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
testAllLayouts<PlayerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
testAllLayouts<PlayerCustomLayoutTvBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
testAllLayouts<TrailerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<FragmentHomeBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
testAllLayouts<FragmentHomeTvBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
testAllLayouts<FragmentSearchBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
testAllLayouts<FragmentSearchTvBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
testAllLayouts<HomeResultGridBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
//testAllLayouts<HomeResultGridExpandedBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
testAllLayouts<SearchResultGridExpandedBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
testAllLayouts<SearchResultGridBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
} }
} }
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
fun providersExist() {
Assert.assertTrue(getAllProviders().isNotEmpty())
println("Done providersExist")
} }
@Test @Test
@Throws(AssertionError::class)
fun providerCorrectData() { fun providerCorrectData() {
val langTagsIETF = SubtitleHelper.languages.map { it.IETF_tag } val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
Assert.assertFalse("IETFTagNames does not contain any languages", langTagsIETF.isNullOrEmpty()) Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
for (api in getAllProviders()) { for (api in getAllProviders()) {
Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE") Assert.assertTrue("Api does not contain a mainUrl", api.mainUrl != "NONE")
Assert.assertTrue("Api does not contain a name", api.name != "NONE") Assert.assertTrue("Api does not contain a name", api.name != "NONE")
Assert.assertTrue( Assert.assertTrue(
"Api ${api.name} does not contain a valid language code", "Api ${api.name} does not contain a valid language code",
langTagsIETF.contains(api.lang) isoNames.contains(api.lang)
) )
Assert.assertTrue( Assert.assertTrue(
"Api ${api.name} does not contain any supported types", "Api ${api.name} does not contain any supported types",
@ -150,20 +180,68 @@ class ExampleInstrumentedTest {
@Test @Test
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().toList().amap { api -> getAllProviders().amap { api ->
TestingUtils.testHomepage(api, TestingUtils.Logger()) if (api.hasMainPage) {
try {
val f = api.mainPage.first()
val homepage =
api.getMainPage(1, MainPageRequest(f.name, f.data, f.horizontalImages))
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 testAllProvidersCorrect() { fun providerCorrect() {
runBlocking { runBlocking {
TestingUtils.getDeferredProviderTests( val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
this, val providers = getAllProviders()
getAllProviders(), providers.amap { api ->
) { _, _ -> } try {
println("Trying $api")
if (testSingleProviderApi(api)) {
println("Success $api")
} else {
System.err.println("Error $api")
invalidProvider.add(Pair(api, null))
}
} catch (e: Exception) {
logError(e)
invalidProvider.add(Pair(api, e))
}
}
if (invalidProvider.isEmpty()) {
println("No Invalid providers! :D")
} else {
println("Invalid providers are: ")
for (provider in invalidProvider) {
println("${provider.first}")
}
}
} }
println("Done providerCorrect")
} }
} }

View file

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

View file

@ -1,41 +0,0 @@
package com.lagradost.cloudstream3.utils.serializers
import android.net.Uri
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@Serializable
data class UriData(
@Serializable(with = UriSerializer::class)
@SerialName("uri") val uri: Uri = Uri.EMPTY,
)
class UriSerializerTest {
@Test
fun uriSerializerSerializesUriToString() {
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
val result = data.toJson()
assertTrue(result.contains("https://example.com/path?query=1"))
}
@Test
fun uriSerializerDeserializesStringToUri() {
val input = """{"uri":"https://example.com/path?query=1"}"""
val result = parseJson<UriData>(input)
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
}
@Test
fun uriSerializerRoundtripsCorrectly() {
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
val encoded = data.toJson()
val decoded = parseJson<UriData>(encoded)
assertEquals(data.uri, decoded.uri)
}
}

View file

@ -25,8 +25,9 @@
android:endY="245.72" android:endY="245.72"
android:endX="292.58" android:endX="292.58"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#3FAA11"/> <item android:offset="0" android:color="#FF5D49EA"/>
<item android:offset="1" android:color="#39A11D"/> <item android:offset="0.45" android:color="#FF452FE4"/>
<item android:offset="1" android:color="#FF2309DB"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
@ -39,8 +40,9 @@
android:endY="245.72" android:endY="245.72"
android:endX="248.76" android:endX="248.76"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#37DB25"/> <item android:offset="0" android:color="#FF4F6DFB"/>
<item android:offset="1" android:color="#11DD6D"/> <item android:offset="0.6" android:color="#FF3559E7"/>
<item android:offset="1" android:color="#FF2149D8"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
@ -53,45 +55,46 @@
android:endY="245.69" android:endY="245.69"
android:endX="210.03" android:endX="210.03"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#40F15D"/> <item android:offset="0" android:color="#FF56B6FE"/>
<item android:offset="1" android:color="#42C54F"/> <item android:offset="0.61" android:color="#FF599CFA"/>
<item android:offset="1" android:color="#FF5C89F7"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
<path <path
android:pathData="M358.81,285q-13.53,0 -22.64,-9.1t-9,-22.72q0,-13.62 9,-22.64 9,-9.18 22.64,-9.19 13.79,0 22.38,10l-5.62,5.44a20.82,20.82 0,0 0,-16.76 -7.91,23 23,0 0,0 -16.94,6.81q-6.72,6.72 -6.72,17.53t6.72,17.53a23,23 0,0 0,16.94 6.81q10.63,0 18.46,-8.94l5.7,5.53a29.57,29.57 0,0 1,-10.63 8A32.44,32.44 0,0 1,358.81 285Z" android:pathData="M358.81,285q-13.53,0 -22.64,-9.1t-9,-22.72q0,-13.62 9,-22.64 9,-9.18 22.64,-9.19 13.79,0 22.38,10l-5.62,5.44a20.82,20.82 0,0 0,-16.76 -7.91,23 23,0 0,0 -16.94,6.81q-6.72,6.72 -6.72,17.53t6.72,17.53a23,23 0,0 0,16.94 6.81q10.63,0 18.46,-8.94l5.7,5.53a29.57,29.57 0,0 1,-10.63 8A32.44,32.44 0,0 1,358.81 285Z"
android:fillColor="#39A11D"/> android:fillColor="#2e24ff"/>
<path <path
android:pathData="M397.78,222.69v60.93H390V222.69Z" android:pathData="M397.78,222.69v60.93H390V222.69Z"
android:fillColor="#39A11D"/> android:fillColor="#2e24ff"/>
<path <path
android:pathData="M404.5,262.77q0,-9.61 6,-15.91a20.6,20.6 0,0 1,15.41 -6.3,20.31 20.31,0 0,1 15.31,6.3 21.87,21.87 0,0 1,6.13 15.91q0,9.71 -6.13,15.92A20.3,20.3 0,0 1,426 285a20.6,20.6 0,0 1,-15.41 -6.29Q404.5,272.39 404.5,262.77ZM412.33,262.77a15.31,15.31 0,0 0,3.91 10.9,13.38 13.38,0 0,0 19.41,0 17,17 0,0 0,0 -21.7,13.18 13.18,0 0,0 -19.41,0A15.18,15.18 0,0 0,412.33 262.77Z" android:pathData="M404.5,262.77q0,-9.61 6,-15.91a20.6,20.6 0,0 1,15.41 -6.3,20.31 20.31,0 0,1 15.31,6.3 21.87,21.87 0,0 1,6.13 15.91q0,9.71 -6.13,15.92A20.3,20.3 0,0 1,426 285a20.6,20.6 0,0 1,-15.41 -6.29Q404.5,272.39 404.5,262.77ZM412.33,262.77a15.31,15.31 0,0 0,3.91 10.9,13.38 13.38,0 0,0 19.41,0 17,17 0,0 0,0 -21.7,13.18 13.18,0 0,0 -19.41,0A15.18,15.18 0,0 0,412.33 262.77Z"
android:fillColor="#39A11D"/> android:fillColor="#2e24ff"/>
<path <path
android:pathData="M490.7,283.62h-7.48v-5.78h-0.35a13.86,13.86 0,0 1,-5.48 5.1,15.77 15.77,0 0,1 -7.7,2q-7.67,0 -11.79,-4.38t-4.13,-12.47v-26.2h7.83v25.69q0.25,10.22 10.3,10.22a9.81,9.81 0,0 0,7.83 -3.79,13.7 13.7,0 0,0 3.14,-9.06V241.93h7.83Z" android:pathData="M490.7,283.62h-7.48v-5.78h-0.35a13.86,13.86 0,0 1,-5.48 5.1,15.77 15.77,0 0,1 -7.7,2q-7.67,0 -11.79,-4.38t-4.13,-12.47v-26.2h7.83v25.69q0.25,10.22 10.3,10.22a9.81,9.81 0,0 0,7.83 -3.79,13.7 13.7,0 0,0 3.14,-9.06V241.93h7.83Z"
android:fillColor="#39A11D"/> android:fillColor="#2e24ff"/>
<path <path
android:pathData="M517.25,285a18.34,18.34 0,0 1,-14 -6.46,24.34 24.34,0 0,1 0,-31.49 18.35,18.35 0,0 1,14 -6.47,18.07 18.07,0 0,1 8.39,2 14.84,14.84 0,0 1,5.83 5.19h0.34l-0.34,-5.78L531.47,222.69h7.82v60.93h-7.48v-5.78h-0.34a14.84,14.84 0,0 1,-5.83 5.19A18.07,18.07 0,0 1,517.25 285ZM518.53,277.86a12,12 0,0 0,9.45 -4.17q3.82,-4.17 3.83,-10.9A15.54,15.54 0,0 0,528 252a12.05,12.05 0,0 0,-9.45 -4.26,12.19 12.19,0 0,0 -9.44,4.26 15.5,15.5 0,0 0,-3.83 10.8,15.32 15.32,0 0,0 3.83,10.81A12.19,12.19 0,0 0,518.53 277.84Z" android:pathData="M517.25,285a18.34,18.34 0,0 1,-14 -6.46,24.34 24.34,0 0,1 0,-31.49 18.35,18.35 0,0 1,14 -6.47,18.07 18.07,0 0,1 8.39,2 14.84,14.84 0,0 1,5.83 5.19h0.34l-0.34,-5.78L531.47,222.69h7.82v60.93h-7.48v-5.78h-0.34a14.84,14.84 0,0 1,-5.83 5.19A18.07,18.07 0,0 1,517.25 285ZM518.53,277.86a12,12 0,0 0,9.45 -4.17q3.82,-4.17 3.83,-10.9A15.54,15.54 0,0 0,528 252a12.05,12.05 0,0 0,-9.45 -4.26,12.19 12.19,0 0,0 -9.44,4.26 15.5,15.5 0,0 0,-3.83 10.8,15.32 15.32,0 0,0 3.83,10.81A12.19,12.19 0,0 0,518.53 277.84Z"
android:fillColor="#39A11D"/> android:fillColor="#2e24ff"/>
<path <path
android:pathData="M587.8,267.33a15.91,15.91 0,0 1,-5.87 12.88A22.43,22.43 0,0 1,567.46 285a21.39,21.39 0,0 1,-13.36 -4.42,22.65 22.65,0 0,1 -8,-12.08l7.49,-3.07a19.3,19.3 0,0 0,2.13 4.94,15.72 15.72,0 0,0 3.19,3.78 14.25,14.25 0,0 0,4 2.47,12.26 12.26,0 0,0 4.68,0.9 13.47,13.47 0,0 0,8.76 -2.77,9 9,0 0,0 3.41,-7.36 8.8,8.8 0,0 0,-2.81 -6.55q-2.64,-2.64 -9.87,-5.11 -7.32,-2.64 -9.11,-3.57 -9.69,-4.94 -9.7,-14.55a14.84,14.84 0,0 1,5.37 -11.49A19.53,19.53 0,0 1,567 221.33a20.5,20.5 0,0 1,12.09 3.58,16.67 16.67,0 0,1 6.8,8.76l-7.31,3.06a10.84,10.84 0,0 0,-4 -5.65,13.1 13.1,0 0,0 -15.11,0.28 7.41,7.41 0,0 0,-3.15 6.19,7.14 7.14,0 0,0 2.47,5.42q2.73,2.29 11.83,5.42 9.27,3.17 13.23,7.72A16.53,16.53 0,0 1,587.8 267.33Z" android:pathData="M587.8,267.33a15.91,15.91 0,0 1,-5.87 12.88A22.43,22.43 0,0 1,567.46 285a21.39,21.39 0,0 1,-13.36 -4.42,22.65 22.65,0 0,1 -8,-12.08l7.49,-3.07a19.3,19.3 0,0 0,2.13 4.94,15.72 15.72,0 0,0 3.19,3.78 14.25,14.25 0,0 0,4 2.47,12.26 12.26,0 0,0 4.68,0.9 13.47,13.47 0,0 0,8.76 -2.77,9 9,0 0,0 3.41,-7.36 8.8,8.8 0,0 0,-2.81 -6.55q-2.64,-2.64 -9.87,-5.11 -7.32,-2.64 -9.11,-3.57 -9.69,-4.94 -9.7,-14.55a14.84,14.84 0,0 1,5.37 -11.49A19.53,19.53 0,0 1,567 221.33a20.5,20.5 0,0 1,12.09 3.58,16.67 16.67,0 0,1 6.8,8.76l-7.31,3.06a10.84,10.84 0,0 0,-4 -5.65,13.1 13.1,0 0,0 -15.11,0.28 7.41,7.41 0,0 0,-3.15 6.19,7.14 7.14,0 0,0 2.47,5.42q2.73,2.29 11.83,5.42 9.27,3.17 13.23,7.72A16.53,16.53 0,0 1,587.8 267.33Z"
android:fillColor="#68C671"/> android:fillColor="#5252ff"/>
<path <path
android:pathData="M610.26,284.3a11.88,11.88 0,0 1,-8.46 -3.15c-2.25,-2.09 -3.4,-5 -3.45,-8.76V249.07H591v-7.14h7.32V229.16h7.83v12.77h10.21v7.14H606.18v20.77c0,2.78 0.54,4.66 1.61,5.66a5.27,5.27 0,0 0,3.66 1.48,7.9 7.9,0 0,0 1.83,-0.21 9,9 0,0 0,1.66 -0.55l2.47,7A21.23,21.23 0,0 1,610.26 284.3Z" android:pathData="M610.26,284.3a11.88,11.88 0,0 1,-8.46 -3.15c-2.25,-2.09 -3.4,-5 -3.45,-8.76V249.07H591v-7.14h7.32V229.16h7.83v12.77h10.21v7.14H606.18v20.77c0,2.78 0.54,4.66 1.61,5.66a5.27,5.27 0,0 0,3.66 1.48,7.9 7.9,0 0,0 1.83,-0.21 9,9 0,0 0,1.66 -0.55l2.47,7A21.23,21.23 0,0 1,610.26 284.3Z"
android:fillColor="#68C671"/> android:fillColor="#5252ff"/>
<path <path
android:pathData="M631.71,283.62h-7.83V241.93h7.48v6.8h0.35a11.31,11.31 0,0 1,4.89 -5.66,13.66 13.66,0 0,1 7.27,-2.34 14.7,14.7 0,0 1,5.79 1l-2.38,7.57a12.93,12.93 0,0 0,-4.6 -0.6,10.11 10.11,0 0,0 -7.7,3.58 12,12 0,0 0,-3.27 8.34Z" android:pathData="M631.71,283.62h-7.83V241.93h7.48v6.8h0.35a11.31,11.31 0,0 1,4.89 -5.66,13.66 13.66,0 0,1 7.27,-2.34 14.7,14.7 0,0 1,5.79 1l-2.38,7.57a12.93,12.93 0,0 0,-4.6 -0.6,10.11 10.11,0 0,0 -7.7,3.58 12,12 0,0 0,-3.27 8.34Z"
android:fillColor="#68C671"/> android:fillColor="#5252ff"/>
<path <path
android:pathData="M670.93,285a19.93,19.93 0,0 1,-15.14 -6.29q-6,-6.3 -6,-15.92a22.65,22.65 0,0 1,5.79 -15.87,19.15 19.15,0 0,1 14.8,-6.34q9.29,0 14.77,6t5.49,16.81l-0.09,0.85L657.83,264.24a13.56,13.56 0,0 0,4.08 9.87,13.06 13.06,0 0,0 9.36,3.75q7.49,0 11.75,-7.49l7,3.4a20.69,20.69 0,0 1,-7.78 8.25A21.51,21.51 0,0 1,670.93 285ZM658.42,257.77h23.92a10.43,10.43 0,0 0,-3.53 -7.19,12.38 12.38,0 0,0 -8.56,-2.85 11.34,11.34 0,0 0,-7.61 2.72A13.09,13.09 0,0 0,658.42 257.75Z" android:pathData="M670.93,285a19.93,19.93 0,0 1,-15.14 -6.29q-6,-6.3 -6,-15.92a22.65,22.65 0,0 1,5.79 -15.87,19.15 19.15,0 0,1 14.8,-6.34q9.29,0 14.77,6t5.49,16.81l-0.09,0.85L657.83,264.24a13.56,13.56 0,0 0,4.08 9.87,13.06 13.06,0 0,0 9.36,3.75q7.49,0 11.75,-7.49l7,3.4a20.69,20.69 0,0 1,-7.78 8.25A21.51,21.51 0,0 1,670.93 285ZM658.42,257.77h23.92a10.43,10.43 0,0 0,-3.53 -7.19,12.38 12.38,0 0,0 -8.56,-2.85 11.34,11.34 0,0 0,-7.61 2.72A13.09,13.09 0,0 0,658.42 257.75Z"
android:fillColor="#68C671"/> android:fillColor="#5252ff"/>
<path <path
android:pathData="M714.08,240.56q8.67,0 13.7,4.64c3.34,3.1 5,7.33 5,12.72v25.7h-7.49v-5.78H725Q720.11,285 712,285a16.83,16.83 0,0 1,-11.53 -4.08,13 13,0 0,1 -4.63,-10.21 12.38,12.38 0,0 1,4.89 -10.3q4.89,-3.83 13.06,-3.83a23.16,23.16 0,0 1,11.49 2.55v-1.78a8.9,8.9 0,0 0,-3.24 -6.94,11.08 11.08,0 0,0 -7.57,-2.85 12,12 0,0 0,-10.38 5.53l-6.89,-4.34Q702.93,240.57 714.08,240.56ZM704,270.86a6.24,6.24 0,0 0,2.59 5.1,9.57 9.57,0 0,0 6.09,2.05 12.5,12.5 0,0 0,8.81 -3.66,11.47 11.47,0 0,0 3.87,-8.6q-3.66,-2.88 -10.21,-2.89a13.22,13.22 0,0 0,-8 2.3A6.81,6.81 0,0 0,704 270.86Z" android:pathData="M714.08,240.56q8.67,0 13.7,4.64c3.34,3.1 5,7.33 5,12.72v25.7h-7.49v-5.78H725Q720.11,285 712,285a16.83,16.83 0,0 1,-11.53 -4.08,13 13,0 0,1 -4.63,-10.21 12.38,12.38 0,0 1,4.89 -10.3q4.89,-3.83 13.06,-3.83a23.16,23.16 0,0 1,11.49 2.55v-1.78a8.9,8.9 0,0 0,-3.24 -6.94,11.08 11.08,0 0,0 -7.57,-2.85 12,12 0,0 0,-10.38 5.53l-6.89,-4.34Q702.93,240.57 714.08,240.56ZM704,270.86a6.24,6.24 0,0 0,2.59 5.1,9.57 9.57,0 0,0 6.09,2.05 12.5,12.5 0,0 0,8.81 -3.66,11.47 11.47,0 0,0 3.87,-8.6q-3.66,-2.88 -10.21,-2.89a13.22,13.22 0,0 0,-8 2.3A6.81,6.81 0,0 0,704 270.86Z"
android:fillColor="#68C671"/> android:fillColor="#5252ff"/>
<path <path
android:pathData="M749.47,283.62h-7.82V241.93h7.48v5.78h0.34a14,14 0,0 1,5.49 -5.1,15.06 15.06,0 0,1 7.36,-2.05 15.22,15.22 0,0 1,8.09 2.13,12.56 12.56,0 0,1 5.1,5.87q5.19,-8 14.39,-8 7.23,0 11.14,4.43T805,257.58v26h-7.83V258.77q0,-5.86 -2.13,-8.46t-7.15,-2.6a9.35,9.35 0,0 0,-7.57 3.83,14 14,0 0,0 -3.06,9v23.06h-7.83V258.77q0,-5.86 -2.13,-8.46t-7.15,-2.6a9.35,9.35 0,0 0,-7.57 3.83,14 14,0 0,0 -3.07,9Z" android:pathData="M749.47,283.62h-7.82V241.93h7.48v5.78h0.34a14,14 0,0 1,5.49 -5.1,15.06 15.06,0 0,1 7.36,-2.05 15.22,15.22 0,0 1,8.09 2.13,12.56 12.56,0 0,1 5.1,5.87q5.19,-8 14.39,-8 7.23,0 11.14,4.43T805,257.58v26h-7.83V258.77q0,-5.86 -2.13,-8.46t-7.15,-2.6a9.35,9.35 0,0 0,-7.57 3.83,14 14,0 0,0 -3.06,9v23.06h-7.83V258.77q0,-5.86 -2.13,-8.46t-7.15,-2.6a9.35,9.35 0,0 0,-7.57 3.83,14 14,0 0,0 -3.07,9Z"
android:fillColor="#68C671"/> android:fillColor="#5252ff"/>
<path <path
android:pathData="M-13.76,555.76c10.3,-20.89 58.91,-113.94 157.31,-139.7C261.3,385.24 405.9,462.43 469.89,613.28"> android:pathData="M-13.76,555.76c10.3,-20.89 58.91,-113.94 157.31,-139.7C261.3,385.24 405.9,462.43 469.89,613.28">
<aapt:attr name="android:fillColor"> <aapt:attr name="android:fillColor">
@ -101,9 +104,9 @@
android:endY="252.3" android:endY="252.3"
android:endX="373.57" android:endX="373.57"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#68C671"/> <item android:offset="0" android:color="#FF5D49EA"/>
<item android:offset="0.45" android:color="#11DD6D"/> <item android:offset="0.45" android:color="#FF452FE4"/>
<item android:offset="1" android:color="#39A11D"/> <item android:offset="1" android:color="#FF2309DB"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
@ -114,9 +117,9 @@
android:startX="400.11" android:startX="400.11"
android:endX="900" android:endX="900"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#68C671"/> <item android:offset="0" android:color="#FF5D49EA"/>
<item android:offset="0.45" android:color="#11DD6D"/> <item android:offset="0.45" android:color="#FF452FE4"/>
<item android:offset="1" android:color="#39A11D"/> <item android:offset="1" android:color="#FF2309DB"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
@ -129,9 +132,9 @@
android:endY="252.3" android:endY="252.3"
android:endX="373.57" android:endX="373.57"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#68C671"/> <item android:offset="0" android:color="#FF5D49EA"/>
<item android:offset="0.45" android:color="#11DD6D"/> <item android:offset="0.45" android:color="#FF452FE4"/>
<item android:offset="1" android:color="#39A11D"/> <item android:offset="1" android:color="#FF2309DB"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
@ -142,9 +145,9 @@
android:startX="700.11" android:startX="700.11"
android:endX="900.57" android:endX="900.57"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#68C671"/> <item android:offset="0" android:color="#FF5D49EA"/>
<item android:offset="0.45" android:color="#11DD6D"/> <item android:offset="0.45" android:color="#FF452FE4"/>
<item android:offset="1" android:color="#39A11D"/> <item android:offset="1" android:color="#FF2309DB"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>
@ -155,9 +158,9 @@
android:startX="400.11" android:startX="400.11"
android:endX="800.57" android:endX="800.57"
android:type="linear"> android:type="linear">
<item android:offset="0" android:color="#68C671"/> <item android:offset="0" android:color="#FF5D49EA"/>
<item android:offset="0.45" android:color="#11DD6D"/> <item android:offset="0.45" android:color="#FF452FE4"/>
<item android:offset="1" android:color="#39A11D"/> <item android:offset="1" android:color="#FF2309DB"/>
</gradient> </gradient>
</aapt:attr> </aapt:attr>
</path> </path>

View file

@ -6,63 +6,16 @@
<uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this --> <uses-permission android:name="android.permission.INTERNET" /> <!-- unless you only use cs3 as a player for downloaded stuff, you need this -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Downloads on low api devices -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- Plugin API --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <!-- Plugin API -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- some dependency needs this -->
<uses-permission android:name="android.permission.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="android.permission.POST_NOTIFICATIONS" /> <!-- Used for app notifications on Android 13+ --> <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="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt --> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- We can use this directly as CS3 is not on Play Store -->
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA" /> <!-- We can use to read the tv channel list -->
<!-- Required for OpenInAppAction and getting arbitrary Aniyomi packages -->
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<queries>
<!--
QUERY_ALL_PACKAGES does not work on some devices running Android 11+ (like Google TV 14),
so we must explicitly specify the packages and intent patterns we query to ensure visibility.
-->
<!-- For external video players -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="video/*" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/x-mpegURL" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/vnd.apple.mpegurl" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="magnet" />
</intent>
<!-- Common players supported in actions/temp -->
<package android:name="org.videolan.vlc" />
<package android:name="org.videolan.vlc.debug" />
<package android:name="is.xyz.mpv" />
<package android:name="is.xyz.mpv.ytdl" />
<package android:name="app.marlboroadvance.mpvex" />
<package android:name="live.mehiz.mpvkt" />
<package android:name="live.mehiz.mpvkt.preview" />
<package android:name="com.brouken.player" />
<package android:name="dev.anilbeesetti.nextplayer" />
<package android:name="com.instantbits.cast.webvideo" />
<package android:name="com.gianlu.aria2android" />
<!-- Torrent clients -->
<package android:name="org.proninyaroslav.libretorrent" />
<package android:name="com.biglybt.android.client" />
</queries>
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> &lt;!&ndash; Used for getting if vlc is installed &ndash;&gt; -->
<!-- Fixes android tv fuckery --> <!-- Fixes android tv fuckery -->
<uses-feature <uses-feature
android:name="android.hardware.touchscreen" android:name="android.hardware.touchscreen"
@ -71,24 +24,28 @@
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
<queries>
<package android:name="org.videolan.vlc" />
<package android:name="com.instantbits.cast.webvideo" />
<package android:name="is.xyz.mpv" />
</queries>
<!-- Without the large heap Exoplayer buffering gets reset due to OOM. --> <!-- Without the large heap Exoplayer buffering gets reset due to OOM. -->
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android--> <!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
<application <application
android:name=".CloudStreamApp" android:name=".AcraApplication"
android:allowBackup="true" android:allowBackup="true"
android:appCategory="video" android:appCategory="video"
android:banner="@mipmap/ic_banner" android:banner="@mipmap/ic_banner"
android:fullBackupContent="@xml/backup_descriptor" android:fullBackupContent="@xml/backup_descriptor"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:pageSizeCompat="enabled"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:targetApi="${target_sdk_version}"> tools:targetApi="o">
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
@ -104,10 +61,7 @@
android:exported="true" android:exported="true"
android:resizeableActivity="true" android:resizeableActivity="true"
android:screenOrientation="userLandscape" android:screenOrientation="userLandscape"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true">
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
android:launchMode="singleTask"
tools:ignore="DiscouragedApi">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -125,65 +79,25 @@
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="magnet" />
</intent-filter>
<!--<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.OPEN_DOCUMENT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/x-bittorrent" />
</intent-filter>-->
</activity> </activity>
<!-- <!--
android:launchMode="singleTask" android:launchMode="singleTask"
is a bit experimental, it makes loading repositories from browser still stay on the same page is a bit experimental, it makes loading repositories from browser still stay on the same page
no idea about side effects no idea about side effects
Not exported to prevent bypassing the AccountSelectActivity
--> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:exported="false" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:resizeableActivity="true" android:resizeableActivity="true"
android:supportsPictureInPicture="true" /> android:supportsPictureInPicture="true">
<activity
android:name=".ui.account.AccountSelectActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:exported="true">
<intent-filter android:exported="true"> <intent-filter android:exported="true">
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<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>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</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" />
@ -200,14 +114,7 @@
<data android:scheme="cloudstreamrepo" /> <data android:scheme="cloudstreamrepo" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="csshare" />
</intent-filter>
<!-- Allow searching with intents: cloudstreamsearch://Your%20Name --> <!-- Allow searching with intents: cloudstreamsearch://Your%20Name -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -231,7 +138,7 @@
<data android:scheme="cloudstreamcontinuewatching" /> <data android:scheme="cloudstreamcontinuewatching" />
</intent-filter> </intent-filter>
<intent-filter android:autoVerify="false"> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -244,11 +151,15 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.EasterEggMonke"
android:exported="true" />
<receiver <receiver
android:name=".receivers.VideoDownloadRestartReceiver" android:name=".receivers.VideoDownloadRestartReceiver"
android:enabled="false" android:enabled="false"
android:exported="false"> android:exported="true">
<intent-filter android:exported="false"> <intent-filter android:exported="true">
<action android:name="restart_service" /> <action android:name="restart_service" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@ -256,28 +167,14 @@
<service <service
android:name=".services.VideoDownloadService" android:name=".services.VideoDownloadService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<service
android:name=".services.DownloadQueueService"
android:enabled="true"
android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- Necessary for WorkManager services: https://stackoverflow.com/a/77186316 -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<activity <activity
android:name=".ui.ControllerActivity" android:name=".ui.ControllerActivity"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".services.PackageInstallerService" android:name=".utils.PackageInstallerService"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<provider <provider
@ -291,5 +188,5 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
</application> </application>
</manifest> </manifest>

View file

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

View file

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

View file

@ -1,23 +1,15 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.annotation.SuppressLint 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
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.Manifest
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.*
import android.view.KeyEvent import android.widget.TextView
import android.view.View
import android.view.View.NO_ID
import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -26,126 +18,44 @@ 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.core.content.ContextCompat
import androidx.core.view.children
import androidx.core.view.isNotEmpty
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.CastSession
import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.removeKey
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.player.PlayerPipHelper.isPIPPossible import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.ui.player.Torrent import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.ui.result.ActorAdaptor
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter
import com.lagradost.cloudstream3.ui.result.ImageAdapter
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.extensions.PluginAdapter
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper.showInputMethod import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.UiText
import java.lang.ref.WeakReference
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.NewPipe
import java.util.*
enum class FocusDirection {
Start,
End,
Up,
Down,
}
object CommonActivity { object CommonActivity {
private var _activity: WeakReference<Activity>? = null
var activity
get() = _activity?.get()
private set(value) {
_activity = WeakReference(value)
}
@MainThread
fun setActivityInstance(newActivity: Activity?) {
activity = newActivity
}
@MainThread @MainThread
fun Activity?.getCastSession(): CastSession? { fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession return (this as MainActivity?)?.mSessionManager?.currentCastSession
} }
val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
// screenWidth and screenHeight does always var canEnterPipMode: Boolean = false
// refer to the screen while in landscape mode var canShowPipMode: Boolean = false
val screenWidth: Int
get() {
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
val screenHeight: Int
get() {
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
val screenWidthWithOrientation: Int
get() {
return displayMetrics.widthPixels
}
val screenHeightWithOrientation: Int
get() {
return displayMetrics.heightPixels
}
var isPipDesired: Boolean = false
var isInPIPMode: Boolean = false var isInPIPMode: Boolean = false
val onColorSelectedEvent = Event<Pair<Int, Int>>() val onColorSelectedEvent = Event<Pair<Int, Int>>()
val onDialogDismissedEvent = Event<Int>() val onDialogDismissedEvent = Event<Int>()
var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
var appliedTheme: Int = 0
var appliedColor: Int = 0
private var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return
act.runOnUiThread {
showToast(act, act.getString(message), duration)
}
}
fun showToast(message: String?, duration: Int? = null) {
val act = activity ?: return
act.runOnUiThread {
showToast(act, message, duration)
}
}
fun showToast(message: UiText?, duration: Int? = null) {
val act = activity ?: return
if (message == null) return
act.runOnUiThread {
showToast(act, message.asString(act), duration)
}
}
@MainThread var currentToast: Toast? = null
fun showToast(act: Activity?, text: UiText, duration: Int) { fun showToast(act: Activity?, text: UiText, duration: Int) {
if (act == null) return if (act == null) return
text.asStringNull(act)?.let { text.asStringNull(act)?.let {
@ -176,50 +86,42 @@ object CommonActivity {
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
try { try {
val binding = ToastBinding.inflate(act.layoutInflater) val inflater =
binding.text.text = message.trim() act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val layout: View = inflater.inflate(
R.layout.toast,
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
)
val text = layout.findViewById(R.id.text) as TextView
text.text = message.trim()
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
val toast = Toast(act) val toast = Toast(act)
toast.duration = duration ?: Toast.LENGTH_SHORT
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
@Suppress("DEPRECATION") toast.duration = duration ?: Toast.LENGTH_SHORT
toast.view = toast.view = layout
binding.root // FIXME Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version. //https://github.com/PureWriter/ToastCompat
currentToast = toast
toast.show() toast.show()
currentToast = toast
val handler = Handler(Looper.getMainLooper())
val ref = WeakReference(toast)
/* Clean up activity leak */
handler.postDelayed({
if (ref.get() == currentToast) {
currentToast = null
}
}, 10_000)
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
} }
/** /**
* Set locale * Not all languages can be fetched from locale with a code.
* @param languageTag shall a IETF BCP 47 conformant tag. * This map allows sidestepping the default Locale(languageCode)
* Check [com.lagradost.cloudstream3.utils.SubtitleHelper]. * when setting the app language.
* **/
* See locales on: val appLanguageExceptions = hashMapOf(
* https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-core/availableLocales.json "zh-rTW" to Locale.TRADITIONAL_CHINESE
* https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry )
* https://android.googlesource.com/platform/frameworks/base/+/android-16.0.0_r2/core/res/res/values/locale_config.xml
* https://iso639-3.sil.org/code_tables/639/data/all fun setLocale(context: Context?, languageCode: String?) {
*/ if (context == null || languageCode == null) return
fun setLocale(context: Context?, languageTag: String?) { val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
if (context == null || languageTag == null) return
val locale = Locale.forLanguageTag(languageTag)
val resources: Resources = context.resources val resources: Resources = context.resources
val config = resources.configuration val config = resources.configuration
Locale.setDefault(locale) Locale.setDefault(locale)
@ -227,12 +129,7 @@ object CommonActivity {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
context.createConfigurationContext(config) context.createConfigurationContext(config)
resources.updateConfiguration(config, resources.displayMetrics)
@Suppress("DEPRECATION")
resources.updateConfiguration(
config,
resources.displayMetrics
) // FIXME this should be replaced
} }
fun Context.updateLocale() { fun Context.updateLocale() {
@ -241,38 +138,43 @@ object CommonActivity {
setLocale(this, localeCode) setLocale(this, localeCode)
} }
fun init(act: Activity) { fun init(act: ComponentActivity?) {
setActivityInstance(act) if (act == null) return
ioSafe { Torrent.deleteAllFiles() } //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
val componentActivity = activity as? ComponentActivity ?: return //https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN
act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS
componentActivity.updateLocale() act.updateLocale()
componentActivity.updateTv() act.updateTv()
AccountManager.initMainAPI()
NewPipe.init(DownloaderTestImpl.getInstance()) NewPipe.init(DownloaderTestImpl.getInstance())
MainActivity.activityResultLauncher = for (resumeApp in resumeApps) {
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> resumeApp.launcher =
if (result.resultCode == AppCompatActivity.RESULT_OK) { act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
val actionUid = val resultCode = result.resultCode
getKey<String>("last_click_action") ?: return@registerForActivityResult val data = result.data
Log.d(TAG, "Loading action $actionUid result handler") if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
val action = VideoClickActionHolder.getByUniqueId(actionUid) as? OpenInAppAction val pos = resumeApp.getPosition(data)
?: return@registerForActivityResult val dur = resumeApp.getDuration(data)
action.onResultSafe(act, result.data) if (dur > 0L && pos > 0L)
removeKey("last_click_action") DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
removeKey("last_opened") removeKey(resumeApp.lastId)
ResultFragment.updateUI()
}
} }
} }
// Ask for notification permissions on Android 13 // Ask for notification permissions on Android 13
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
componentActivity, act,
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED ) != PackageManager.PERMISSION_GRANTED
) { ) {
val requestPermissionLauncher = componentActivity.registerForActivityResult( val requestPermissionLauncher = act.registerForActivityResult(
ActivityResultContracts.RequestPermission() ActivityResultContracts.RequestPermission()
) { isGranted: Boolean -> ) { isGranted: Boolean ->
Log.d(TAG, "Notification permission: $isGranted") Log.d(TAG, "Notification permission: $isGranted")
@ -283,22 +185,17 @@ object CommonActivity {
} }
} }
/** Enters pip mode if it is both possible and desired to do so*/
private fun Activity.enterPIPMode() { private fun Activity.enterPIPMode() {
if (!isPipDesired || !this.isPIPPossible()) return if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
try { try {
enterPictureInPictureMode(PictureInPictureParams.Builder().build()) enterPictureInPictureMode(PictureInPictureParams.Builder().build())
} catch (_: Exception) { } catch (e: Exception) {
// Use fallback just in case
@Suppress("DEPRECATION")
enterPictureInPictureMode() enterPictureInPictureMode()
} }
} else { } else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@Suppress("DEPRECATION")
enterPictureInPictureMode() enterPictureInPictureMode()
} }
} }
@ -307,32 +204,9 @@ object CommonActivity {
} }
} }
fun onUserLeaveHint(act: Activity) { fun onUserLeaveHint(act: Activity?) {
// On Android 12 and later we use setAutoEnterEnabled() instead. if (canEnterPipMode && canShowPipMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) return act?.enterPIPMode()
act.enterPIPMode()
}
fun updateTheme(act: Activity) {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(act)
if (settingsManager
.getString(act.getString(R.string.app_theme_key), "AmoledLight") == "System"
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
) {
loadThemes(act)
}
}
private fun mapSystemTheme(act: Activity): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val currentNightMode =
act.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
return when (currentNightMode) {
Configuration.UI_MODE_NIGHT_NO -> R.style.LightMode // Night mode is not active, we're using the light theme
else -> R.style.AppTheme // Night mode is active, we're using dark theme
}
} else {
return R.style.AppTheme
} }
} }
@ -342,33 +216,24 @@ object CommonActivity {
val currentTheme = val currentTheme =
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) { when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
"System" -> mapSystemTheme(act)
"Black" -> R.style.AppTheme "Black" -> R.style.AppTheme
"Light" -> R.style.LightMode "Light" -> R.style.LightMode
"Amoled" -> R.style.AmoledMode "Amoled" -> R.style.AmoledMode
"AmoledLight" -> R.style.AmoledModeLight "AmoledLight" -> R.style.AmoledModeLight
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.MonetMode else R.style.AppTheme R.style.MonetMode else R.style.AppTheme
"Dracula" -> R.style.DraculaMode
"Lavender" -> R.style.LavenderMode
"SilentBlue" -> R.style.SilentBlueMode
else -> R.style.AppTheme else -> R.style.AppTheme
} }
val currentOverlayTheme = val currentOverlayTheme =
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) { when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
"Normal" -> R.style.OverlayPrimaryColorNormal "Normal" -> R.style.OverlayPrimaryColorNormal
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink "CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
"Orange" -> R.style.OverlayPrimaryColorOrange
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen "DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
"Maroon" -> R.style.OverlayPrimaryColorMaroon "Maroon" -> R.style.OverlayPrimaryColorMaroon
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue "NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
"Grey" -> R.style.OverlayPrimaryColorGrey "Grey" -> R.style.OverlayPrimaryColorGrey
"White" -> R.style.OverlayPrimaryColorWhite "White" -> R.style.OverlayPrimaryColorWhite
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
"Brown" -> R.style.OverlayPrimaryColorBrown "Brown" -> R.style.OverlayPrimaryColorBrown
"Purple" -> R.style.OverlayPrimaryColorPurple "Purple" -> R.style.OverlayPrimaryColorPurple
"Green" -> R.style.OverlayPrimaryColorGreen "Green" -> R.style.OverlayPrimaryColorGreen
@ -377,227 +242,208 @@ object CommonActivity {
"Banana" -> R.style.OverlayPrimaryColorBanana "Banana" -> R.style.OverlayPrimaryColorBanana
"Party" -> R.style.OverlayPrimaryColorParty "Party" -> R.style.OverlayPrimaryColorParty
"Pink" -> R.style.OverlayPrimaryColorPink "Pink" -> R.style.OverlayPrimaryColorPink
"Lavender" -> R.style.OverlayPrimaryColorLavender
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) "Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
else -> R.style.OverlayPrimaryColorNormal else -> R.style.OverlayPrimaryColorNormal
} }
act.theme.applyStyle(currentTheme, true) act.theme.applyStyle(currentTheme, true)
act.theme.applyStyle(currentOverlayTheme, true) act.theme.applyStyle(currentOverlayTheme, true)
appliedTheme = currentTheme
appliedColor = currentOverlayTheme
act.updateTv()
if (isLayout(TV)) act.theme.applyStyle(R.style.AppThemeTvOverlay, true)
act.theme.applyStyle( act.theme.applyStyle(
R.style.LoadedStyle, R.style.LoadedStyle,
true true
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW ) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
} }
/** because we want closes find, aka when multiple have the same id, we go to parent private fun getNextFocus(
until the correct one is found */ act: Activity?,
private fun localLook(from: View, id: Int): View? {
if (id == NO_ID) return null
var currentLook: View = from
// limit to 15 look depth
for (i in 0..15) {
currentLook.findViewById<View?>(id)?.let { return it }
currentLook = (currentLook.parent as? View) ?: break
}
return null
}
/*var currentLook: View = view
while (true) {
val tmpNext = currentLook.findViewById<View?>(nextId)
if (tmpNext != null) {
next = tmpNext
break
}
currentLook = currentLook.parent as? View ?: break
}*/
private fun View.hasContent(): Boolean {
return isShown && when (this) {
is ViewGroup -> this.isNotEmpty()
else -> true
}
}
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
fun continueGetNextFocus(
root: Any?,
view: View,
direction: FocusDirection,
nextId: Int,
depth: Int = 0
): View? {
if (nextId == NO_ID) return null
// do an initial search for the view, in case the localLook is too deep we can use this as
// an early break and backup view
var next =
when (root) {
is Activity -> root.findViewById(nextId)
is View -> root.rootView.findViewById<View?>(nextId)
else -> null
} ?: return null
next = localLook(view, nextId) ?: next
val shown = next.hasContent()
// if cant focus but visible then break and let android decide
// the exception if is the view is a parent and has children that wants focus
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.isNotEmpty()
} ?: false
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
// if not shown then continue because we will "skip" over views to get to a replacement
if (!shown) {
// we don't want a while true loop, so we let android decide if we find a recursive view
if (next == view) return null
return getNextFocus(root, next, direction, depth + 1)
}
(when (next) {
is ChipGroup -> {
next.children.firstOrNull { it.isFocusable && it.isShown }
}
is NavigationRailView -> {
next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home)
}
else -> null
})?.let {
return it
}
// nothing wrong with the view found, return it
return next
}
/** recursively looks for a next focus up to a depth of 10,
* this is used to override the normal shit focus system
* because this application has a lot of invisible views that messes with some tv devices*/
fun getNextFocus(
root: Any?,
view: View?, view: View?,
direction: FocusDirection, direction: FocusDirection,
depth: Int = 0 depth: Int = 0
): View? { ): Int? {
// if input is invalid let android decide + depth test to not crash if loop is found if (view == null || depth >= 10 || act == null) {
if (view == null || depth >= 10 || root == null) {
return null return null
} }
var nextId = when (direction) { val nextId = when (direction) {
FocusDirection.Start -> { FocusDirection.Left -> {
if (view.isRtl()) view.nextFocusLeftId
view.nextFocusRightId
else
view.nextFocusLeftId
} }
FocusDirection.Up -> { FocusDirection.Up -> {
view.nextFocusUpId view.nextFocusUpId
} }
FocusDirection.Right -> {
FocusDirection.End -> { view.nextFocusRightId
if (view.isRtl())
view.nextFocusLeftId
else
view.nextFocusRightId
} }
FocusDirection.Down -> { FocusDirection.Down -> {
view.nextFocusDownId view.nextFocusDownId
} }
} }
if (nextId == NO_ID) { return if (nextId != -1) {
// if not specified then use forward id val next = act.findViewById<View?>(nextId)
nextId = view.nextFocusForwardId //println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
// if view is still not found to next focus then return and let android decide
if (nextId == NO_ID) if (next?.isShown == false) {
return null getNextFocus(act, next, direction, depth + 1)
} else {
if (depth == 0) {
null
} else {
nextId
}
}
} else {
null
} }
return continueGetNextFocus(root, view, direction, nextId, depth)
} }
enum class FocusDirection {
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?): Boolean? { Left,
return null Right,
Up,
Down,
}
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
// Tested keycodes on remote:
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
// KeyEvent.KEYCODE_MEDIA_REWIND
// KeyEvent.KEYCODE_MENU
// KeyEvent.KEYCODE_MEDIA_NEXT
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
// 149 keycode_numpad 5
when (keyCode) {
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
PlayerEventType.SeekForward
}
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
PlayerEventType.SeekBack
}
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
PlayerEventType.NextEpisode
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
PlayerEventType.PrevEpisode
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
PlayerEventType.Pause
}
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
PlayerEventType.Play
}
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
PlayerEventType.Lock
}
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
PlayerEventType.ToggleHide
}
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
PlayerEventType.ToggleMute
}
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
PlayerEventType.ShowMirrors
}
// OpenSubtitles shortcut
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
PlayerEventType.ShowSpeed
}
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
PlayerEventType.Resize
}
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp
}
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
PlayerEventType.SkipCurrentChapter
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}
else -> null
}?.let { playerEvent ->
playerEventListener?.invoke(playerEvent)
}
//when (keyCode) {
// KeyEvent.KEYCODE_DPAD_CENTER -> {
// println("DPAD PRESSED")
// }
//}
} }
/** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? { fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null if (act == null) return null
val currentFocus = act.currentFocus
event?.keyCode?.let { keyCode -> event?.keyCode?.let { keyCode ->
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let when (event.action) {
val nextView = when (keyCode) { KeyEvent.ACTION_DOWN -> {
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus( if (act.currentFocus != null) {
act, val next = when (keyCode) {
currentFocus, KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
FocusDirection.Start act,
) act.currentFocus,
FocusDirection.Left
)
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Right
)
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Up
)
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Down
)
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus( else -> null
act, }
currentFocus,
FocusDirection.End
)
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus( if (next != null && next != -1) {
act, val nextView = act.findViewById<View?>(next)
currentFocus, if (nextView != null) {
FocusDirection.Up nextView.requestFocus()
) keyEventListener?.invoke(Pair(event, true))
return true
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus( when (keyCode) {
act, KeyEvent.KEYCODE_DPAD_CENTER -> {
currentFocus, if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
FocusDirection.Down UIHelper.showInputMethod(act.currentFocus?.findFocus())
) }
}
else -> null }
}
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
}
} }
// println("NEXT FOCUS : $nextView")
if (nextView != null) {
nextView.requestFocus()
keyEventListener?.invoke(Pair(event, true))
return true
}
// TODO: Figure out why removing the check for SearchAutoComplete seems
// to break focus on TV as it shouldn't need to be used.
// Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote)
// send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button.
@SuppressLint("RestrictedApi")
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) {
showInputMethod(act.currentFocus?.findFocus())
}
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
} }
// if someone else want to override the focus then don't handle the event as it is already
// consumed. used in video player
if (keyEventListener?.invoke(Pair(event, false)) == true) { if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true return true
} }

View file

@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.schabi.newpipe.extractor.downloader.Downloader import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response import org.schabi.newpipe.extractor.downloader.Response
@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() { class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Downloader() {
private val client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build() private val client: OkHttpClient
override fun execute(request: Request): Response { override fun execute(request: Request): Response {
val httpMethod: String = request.httpMethod() val httpMethod: String = request.httpMethod()
val url: String = request.url() val url: String = request.url()
@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
val dataToSend: ByteArray? = request.dataToSend() val dataToSend: ByteArray? = request.dataToSend()
var requestBody: RequestBody? = null var requestBody: RequestBody? = null
if (dataToSend != null) { if (dataToSend != null) {
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size) requestBody = RequestBody.create(null, dataToSend)
} }
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder() val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url) .method(httpMethod, requestBody).url(url)
@ -51,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
companion object { companion object {
private const val USER_AGENT = private const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36" "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
private var instance: DownloaderTestImpl? = null private var instance: DownloaderTestImpl? = null
/** /**
@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
return instance return instance
} }
} }
init {
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
}
} }

View file

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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,88 @@
package com.lagradost.cloudstream3
import com.lagradost.cloudstream3.mvvm.logError
import kotlinx.coroutines.*
//https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections
/*
fun <T, R> Iterable<T>.pmap(
numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1),
exec: ExecutorService = Executors.newFixedThreadPool(numThreads),
transform: (T) -> R,
): List<R> {
// default size is just an inlined version of kotlin.collections.collectionSizeOrDefault
val defaultSize = if (this is Collection<*>) this.size else 10
val destination = Collections.synchronizedList(ArrayList<R>(defaultSize))
for (item in this) {
exec.submit { destination.add(transform(item)) }
}
exec.shutdown()
exec.awaitTermination(1, TimeUnit.DAYS)
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 {
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 {
map { async { f(it) } }.map { it.await() }
}
fun <A, B> List<A>.apmapIndexed(f: suspend (index: Int, A) -> B): List<B> = runBlocking {
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
/*fun <R> argpmap(
vararg transforms: () -> R,
numThreads: Int = maxOf(Runtime.getRuntime().availableProcessors() - 2, 1),
exec: ExecutorService = Executors.newFixedThreadPool(numThreads)
) {
for (item in transforms) {
exec.submit { item.invoke() }
}
exec.shutdown()
exec.awaitTermination(1, TimeUnit.DAYS)
}*/
// built in try catch
fun <R> argamap(
vararg transforms: suspend () -> R,
) = runBlocking {
transforms.map {
async {
try {
it.invoke()
} catch (e: Exception) {
logError(e)
}
}
}.map { it.await() }
}

View file

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

View file

@ -1,135 +0,0 @@
package com.lagradost.cloudstream3.actions
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.AppContextUtils.isAppInstalled
import com.lagradost.cloudstream3.utils.DataStoreHelper
import java.io.File
fun updateDurationAndPosition(position: Long, duration: Long) {
if (position <= 0 || duration <= 0) return
val episode = getKey<ResultEpisode>("last_opened") ?: return
DataStoreHelper.setViewPosAndResume(episode.id, position, duration, episode, null)
ResultFragment.updateUI()
}
/**
* Util method that may be helpful for creating intents for apps that support m3u8 files.
* All sources are written to a temporary m3u8 file, which is then sent to the app.
*/
fun makeTempM3U8Intent(
context: Context,
intent: Intent,
result: LinkLoadingResult
) {
if (result.links.size == 1) {
intent.setDataAndType(result.links.first().url.toUri(), "video/*")
return
}
intent.apply {
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
}
val outputFile = File.createTempFile("mirrorlist", ".m3u8", context.cacheDir)
var text = "#EXTM3U\n#EXT-X-VERSION:3"
result.links.forEach { link ->
text += "\n#EXTINF:0,${link.name}\n${link.url}"
}
//With subtitles it doesn't work for no reason :(
/*for (sub in result.subs) {
val normalizedName = sub.name.replace("[^a-zA-Z0-9 ]".toRegex(), "")
text += "\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"subs\",NAME=\"${normalizedName}\",DEFAULT=NO,AUTOSELECT=NO,FORCED=NO,LANGUAGE=\"${sub.languageCode}\",URI=\"${sub.url}\""
}*/
text += "\n#EXT-X-ENDLIST"
outputFile.writeText(text)
intent.setDataAndType(
FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".provider",
outputFile
), "application/x-mpegURL"
)
}
abstract class OpenInAppAction(
open val appName: UiText,
open val packageName: String,
private val intentClass: String? = null,
private val action: String = Intent.ACTION_VIEW
) : VideoClickAction() {
override val name: UiText
get() = txt(R.string.episode_action_play_in_format, appName)
override val isPlayer = true
override fun shouldShow(context: Context?, video: ResultEpisode?) =
context?.isAppInstalled(packageName) != false
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
if (context == null) return
val intent = Intent(action)
intent.setPackage(packageName)
if (intentClass != null) {
intent.component = ComponentName(packageName, intentClass)
}
putExtra(context, intent, video, result, index)
setKey("last_opened", video)
launchResult(intent)
}
/**
* Before intent is sent, this function is called to put extra data into the intent.
* @see VideoClickAction.runAction
* */
@Throws
abstract suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
)
/**
* This function is called when the app is opened again after the intent was sent.
* You can use it to for example update duration and position.
* @see updateDurationAndPosition
*/
@Throws
abstract fun onResult(activity: Activity, intent: Intent?)
/** Safe version of onResult, we don't trust extension devs to not crash the app */
fun onResultSafe(activity: Activity, intent: Intent?) {
try {
onResult(activity, intent)
} catch (t: Throwable) {
logError(t)
}
}
}

View file

@ -1,204 +0,0 @@
package com.lagradost.cloudstream3.actions
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat
import com.lagradost.api.Log
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.ErrorLoadingException
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.temp.BiglyBTPackage
import com.lagradost.cloudstream3.actions.temp.CopyClipboardAction
import com.lagradost.cloudstream3.actions.temp.JustPlayerPackage
import com.lagradost.cloudstream3.actions.temp.LibreTorrentPackage
import com.lagradost.cloudstream3.actions.temp.MpvExPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPackage
import com.lagradost.cloudstream3.actions.temp.MpvKtPreviewPackage
import com.lagradost.cloudstream3.actions.temp.MpvPackage
import com.lagradost.cloudstream3.actions.temp.MpvRxPackage
import com.lagradost.cloudstream3.actions.temp.MpvYTDLPackage
import com.lagradost.cloudstream3.actions.temp.NextPlayerPackage
import com.lagradost.cloudstream3.actions.temp.OnlyPlayer
import com.lagradost.cloudstream3.actions.temp.PlayInBrowserAction
import com.lagradost.cloudstream3.actions.temp.PlayMirrorAction
import com.lagradost.cloudstream3.actions.temp.ViewM3U8Action
import com.lagradost.cloudstream3.actions.temp.VlcNightlyPackage
import com.lagradost.cloudstream3.actions.temp.VlcPackage
import com.lagradost.cloudstream3.actions.temp.WebVideoCastPackage
import com.lagradost.cloudstream3.actions.temp.fcast.FcastAction
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.atomicListOf
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.UiText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
object VideoClickActionHolder {
val allVideoClickActions = atomicListOf(
// Default
PlayInBrowserAction(),
CopyClipboardAction(),
ViewM3U8Action(),
PlayMirrorAction(),
// main support external apps
VlcPackage(),
MpvPackage(),
MpvExPackage(),
NextPlayerPackage(),
JustPlayerPackage(),
FcastAction(),
LibreTorrentPackage(),
BiglyBTPackage(),
// forks/backup apps
VlcNightlyPackage(),
WebVideoCastPackage(),
MpvYTDLPackage(),
MpvKtPackage(),
MpvKtPreviewPackage(),
OnlyPlayer(),
MpvRxPackage(),
// Always Ask option
AlwaysAskAction(),
// added by plugins
// ...
)
init {
Log.d("VideoClickActionHolder", "allVideoClickActions: ${allVideoClickActions.map { it.uniqueId() }}")
}
private const val ACTION_ID_OFFSET = 1000
fun makeOptionMap(activity: Activity?, video: ResultEpisode) = allVideoClickActions
// We need to have index before filtering
.mapIndexed { id, it -> it to id + ACTION_ID_OFFSET }
.filter { it.first.shouldShowSafe(activity, video) }
.map { it.first.name to it.second }
fun getActionById(id: Int): VideoClickAction? = allVideoClickActions.getOrNull(id - ACTION_ID_OFFSET)
fun getByUniqueId(uniqueId: String): VideoClickAction? = allVideoClickActions.firstOrNull { it.uniqueId() == uniqueId }
fun uniqueIdToId(uniqueId: String?): Int? {
if (uniqueId == null) return null
return allVideoClickActions
.mapIndexed { id, it -> it to id + ACTION_ID_OFFSET }
.firstOrNull { it.first.uniqueId() == uniqueId }
?.second
}
fun getPlayers(activity: Activity? = null) = allVideoClickActions.filter { it.isPlayer && it.shouldShowSafe(activity, null) }
}
abstract class VideoClickAction {
abstract val name: UiText
/** if true, the app will show dialog to select source - result.links[index] */
open val oneSource : Boolean = false
/** if true, this action could be selected as default player (one press action) in settings */
open val isPlayer: Boolean = false
/** Which type of sources this action can handle. */
open val sourceTypes: Set<ExtractorLinkType> = ExtractorLinkType.entries.toSet()
/** Determines which plugin a given provider is from. This is the full path to the plugin. */
var sourcePlugin: String? = null
/** Even if VideoClickAction should not run any UI code, startActivity requires it,
* this is a wrapper for runOnUiThread in a suspended safe context that bubble up exceptions */
@Throws
suspend fun <T> uiThread(callable : Callable<T>) : T? {
val future = FutureTask{
try {
Result.success(callable.call())
} catch (t : Throwable) {
Result.failure(t)
}
}
CommonActivity.activity?.runOnUiThread(future) ?: throw ErrorLoadingException("No UI Activity, this should never happened")
val result = withContext(Dispatchers.IO) {
return@withContext future.get()
}
return result.getOrThrow()
}
/** Internally uses activityResultLauncher,
* use this when the activity has a result like watched position */
@Throws
suspend fun launchResult(intent : Intent?, options : ActivityOptionsCompat? = null) {
if (intent == null) {
return
}
uiThread {
MainActivity.activityResultLauncher?.launch(intent,options)
}
}
/** Internally uses startActivity, use this when you don't
* have any result that needs to be stored when exiting the activity */
@Throws
suspend fun launch(intent : Intent?, bundle : Bundle? = null) {
if (intent == null) {
return
}
uiThread {
CommonActivity.activity?.startActivity(intent, bundle)
}
}
fun uniqueId() = "$sourcePlugin:${this::class.qualifiedName}"
@Throws
abstract fun shouldShow(context: Context?, video: ResultEpisode?): Boolean
/** Safe version of shouldShow, as we don't trust extension devs to handle exceptions,
* however no dev *should* throw in shouldShow */
fun shouldShowSafe(context: Context?, video: ResultEpisode?): Boolean {
return try {
shouldShow(context,video)
} catch (t : Throwable) {
logError(t)
false
}
}
/**
* This function is called when the action is clicked.
* @param context The current activity
* @param video The episode/movie that was clicked
* @param result The result of the link loading, contains video & subtitle links
* @param index if oneSource is true, this is the index of the selected source
*/
@Throws
abstract suspend fun runAction(context: Context?, video: ResultEpisode, result: LinkLoadingResult, index: Int?)
/** Safe version of runAction, as we don't trust extension devs to handle exceptions */
fun runActionSafe(context: Context?, video: ResultEpisode, result: LinkLoadingResult, index: Int?) = ioSafe {
try {
runAction(context, video, result, index)
} catch (_ : NotImplementedError) {
CommonActivity.showToast("runAction has not been implemented for ${name.asStringNull(context)}, please contact the extension developer of $sourcePlugin", Toast.LENGTH_LONG)
} catch (error : ErrorLoadingException) {
CommonActivity.showToast(error.message, Toast.LENGTH_LONG)
} catch (_: ActivityNotFoundException) {
CommonActivity.showToast(R.string.app_not_found_error, Toast.LENGTH_LONG)
} catch (t : Throwable) {
logError(t)
CommonActivity.showToast(t.toString(), Toast.LENGTH_LONG)
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,27 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.content.Context
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
class CopyClipboardAction: VideoClickAction() {
override val name = txt("Copy to clipboard")
override val oneSource = true
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
if (index == null) return
val link = result.links.getOrNull(index) ?: return
clipboardHelper(txt(link.name), link.url)
}
}

View file

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

View file

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

View file

@ -1,68 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLinkType
class MpvKtPreviewPackage: MpvKtPackage(
appName = "mpvKt Preview",
packageName = "live.mehiz.mpvkt.preview",
)
open class MpvKtPackage(
appName: String = "mpvKt",
packageName: String = "live.mehiz.mpvkt",
): OpenInAppAction(
appName = txt(appName),
packageName = packageName,
intentClass = "live.mehiz.mpvkt.ui.player.PlayerActivity"
) {
override val oneSource = true
override val sourceTypes = setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
val link = result.links.getOrNull(index ?: 0) ?: return
intent.apply {
putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray())
setDataAndType(link.url.toUri(), "video/*")
// m3u8 plays, but changing sources feature is not available
// makeTempM3U8Intent(activity, this, result)
//putExtra("headers", link.headers.flatMap { listOf(it.key, it.value) }.toTypedArray())
val position = getViewPos(video.id)?.position
if (position != null)
putExtra("position", position.toInt())
putExtra("secure_uri", true)
}
}
override fun onResult(activity: Activity, intent: Intent?) {
val position = intent?.getIntExtra("position", -1)?.toLong() ?: -1
val duration = intent?.getIntExtra("duration", -1)?.toLong() ?: -1
updateDurationAndPosition(position, duration)
}
}

View file

@ -1,68 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.api.Log
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLinkType
// https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904
// https://mpv-android.github.io/mpv-android/intent.html
//https://github.com/marlboro-advance/mpvEx
class MpvExPackage: MpvPackage("mpvEx","app.marlboroadvance.mpvex","app.marlboroadvance.mpvex.ui.player.PlayerActivity")
class MpvYTDLPackage : MpvPackage("MPV YTDL", "is.xyz.mpv.ytdl") {
override val sourceTypes = setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
}
open class MpvPackage(appName: String = "MPV", packageName: String = "is.xyz.mpv",intentClass:String = "is.xyz.mpv.MPVActivity"): OpenInAppAction(
txt(appName),
packageName,
intentClass
) {
override val oneSource = true // mpv has poor playlist support on TV
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
intent.apply {
putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray())
putExtra("title", video.name)
if (index != null) {
setDataAndType((result.links.getOrNull(index)?.url ?: return).toUri(), "video/*")
} else {
makeTempM3U8Intent(context, this, result)
}
val position = getViewPos(video.id)?.position
if (position != null)
putExtra("position", position.toInt())
putExtra("secure_uri", true)
}
}
override fun onResult(activity: Activity, intent: Intent?) {
val position = intent?.getIntExtra("position", -1) ?: -1
val duration = intent?.getIntExtra("duration", -1) ?: -1
Log.d("MPV", "Position: $position, Duration: $duration")
updateDurationAndPosition(position.toLong(), duration.toLong())
}
}

View file

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

View file

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

View file

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

View file

@ -1,39 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.content.Context
import android.content.Intent
import androidx.core.net.toUri
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.ExtractorLinkType
class PlayInBrowserAction: VideoClickAction() {
override val name = txt(R.string.episode_action_play_in_format, "Browser")
override val oneSource = true
override val isPlayer = true
override val sourceTypes: Set<ExtractorLinkType> = setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
val link = result.links.getOrNull(index ?: 0) ?: return
val i = Intent(Intent.ACTION_VIEW)
i.data = link.url.toUri()
launch(i)
}
}

View file

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

View file

@ -1,30 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.content.Context
import android.content.Intent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
class ViewM3U8Action: VideoClickAction() {
override val name = txt(R.string.episode_action_play_in_format, "m3u8 player")
override val isPlayer = true
override fun shouldShow(context: Context?, video: ResultEpisode?) = true
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
if (context == null) return
val i = Intent(Intent.ACTION_VIEW)
makeTempM3U8Intent(context, i, result)
launch(i)
}
}

View file

@ -1,77 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.net.toUri
import com.lagradost.api.Log
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getKey
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.actions.makeTempM3U8Intent
import com.lagradost.cloudstream3.actions.updateDurationAndPosition
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
// https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898
// https://wiki.videolan.org/Android_Player_Intents/
class VlcNightlyPackage : VlcPackage() {
override val packageName = "org.videolan.vlc.debug"
override val appName = txt("VLC Nightly")
}
open class VlcPackage: OpenInAppAction(
appName = txt("VLC"),
packageName = "org.videolan.vlc",
intentClass = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.gui.video.VideoPlayerActivity"
} else {
null
},
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
"org.videolan.vlc.player.result"
} else {
Intent.ACTION_VIEW
}
) {
// while VLC supports multi links, it has poor support, so we disable it for now
override val oneSource = true
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
if (index != null) {
intent.setDataAndType(result.links[index].url.toUri(), "video/*")
} else {
makeTempM3U8Intent(context, intent, result)
}
val position = getViewPos(video.id)?.position ?: 0L
intent.putExtra("from_start", false)
intent.putExtra("position", position)
intent.putExtra("secure_uri", true)
intent.putExtra("title", video.name)
val subsLang = getKey<String>(SUBTITLE_AUTO_SELECT_KEY) ?: "en"
result.subs.firstOrNull {
subsLang == it.languageCode
}?.let {
intent.putExtra("subtitles_location", it.url)
}
}
override fun onResult(activity: Activity, intent: Intent?) {
val position = intent?.getLongExtra("extra_position", -1) ?: -1
val duration = intent?.getLongExtra("extra_duration", -1) ?: -1
Log.d("VLC", "Position: $position, Duration: $duration")
updateDurationAndPosition(position, duration)
}
}

View file

@ -1,61 +0,0 @@
package com.lagradost.cloudstream3.actions.temp
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.actions.OpenInAppAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.ExtractorLinkType
// https://www.webvideocaster.com/integrations
class WebVideoCastPackage: OpenInAppAction(
txt("Web Video Cast"),
"com.instantbits.cast.webvideo"
) {
override val oneSource = true
override val sourceTypes = setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
override suspend fun putExtra(
context: Context,
intent: Intent,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
val link = result.links[index ?: 0]
intent.apply {
setDataAndType(link.url.toUri(), "video/*")
val title = video.name ?: video.headerName
putExtra("subs", result.subs.map { it.url.toUri() }.toTypedArray())
putExtra("title", title)
video.poster?.let { putExtra("poster", it) }
val headers = Bundle().apply {
if (link.referer.isNotBlank())
putString("Referer", link.referer)
putString("User-Agent", USER_AGENT)
for ((key, value) in link.headers) {
putString(key, value)
}
}
putExtra("android.media.intent.extra.HTTP_HEADERS", headers)
putExtra("secure_uri", true)
}
}
override fun onResult(activity: Activity, intent: Intent?) = Unit
}

View file

@ -1,69 +0,0 @@
package com.lagradost.cloudstream3.actions.temp.fcast
import android.content.Context
import com.lagradost.cloudstream3.CloudStreamApp.Companion.getActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.actions.VideoClickAction
import com.lagradost.cloudstream3.ui.result.LinkLoadingResult
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
class FcastAction: VideoClickAction() {
override val name = txt("Fcast to device")
override val oneSource = true
override val sourceTypes = setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
)
override fun shouldShow(context: Context?, video: ResultEpisode?) = FcastManager.currentDevices.isNotEmpty()
override suspend fun runAction(
context: Context?,
video: ResultEpisode,
result: LinkLoadingResult,
index: Int?
) {
val link = result.links.getOrNull(index ?: 0) ?: return
val devices = FcastManager.currentDevices.toList()
uiThread {
context?.getActivity()?.showBottomDialog(
devices.map { it.name },
-1,
txt(R.string.player_settings_select_cast_device).asString(context),
false,
{}) {
val position = getViewPos(video.id)?.position
castTo(devices.getOrNull(it), link, position)
}
}
}
private fun castTo(device: PublicDeviceInfo?, link: ExtractorLink, position: Long?) {
val host = device?.host ?: return
FcastSession(host).use { session ->
session.sendMessage(
Opcode.Play,
PlayMessage(
link.type.getMimeType(),
link.url,
time = position?.let { it / 1000.0 },
headers = mapOf(
"referer" to link.referer,
"user-agent" to USER_AGENT
) + link.headers
)
)
}
}
}

View file

@ -1,195 +0,0 @@
package com.lagradost.cloudstream3.actions.temp.fcast
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdManager.ResolveListener
import android.net.nsd.NsdServiceInfo
import android.os.Build
import android.os.ext.SdkExtensions
import android.util.Log
import com.lagradost.cloudstream3.mvvm.safe
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
class FcastManager {
private var nsdManager: NsdManager? = null
// Used for receiver
private val registrationListenerTcp = DefaultRegistrationListener()
private fun getDeviceName(): String {
return "${Build.MANUFACTURER}-${Build.MODEL}"
}
/**
* Start the fcast service
* @param registerReceiver If true will register the app as a compatible fcast receiver for discovery in other app
*/
fun init(context: Context, registerReceiver: Boolean) = ioSafe {
nsdManager = context.getSystemService(Context.NSD_SERVICE) as NsdManager
val serviceType = "_fcast._tcp"
if (registerReceiver) {
val serviceName = "$APP_PREFIX-${getDeviceName()}"
val serviceInfo = NsdServiceInfo().apply {
this.serviceName = serviceName
this.serviceType = serviceType
this.port = TCP_PORT
}
nsdManager?.registerService(
serviceInfo,
NsdManager.PROTOCOL_DNS_SD,
registrationListenerTcp
)
}
nsdManager?.discoverServices(
serviceType,
NsdManager.PROTOCOL_DNS_SD,
DefaultDiscoveryListener()
)
}
fun stop() {
nsdManager?.unregisterService(registrationListenerTcp)
}
inner class DefaultDiscoveryListener : NsdManager.DiscoveryListener {
val tag = "DiscoveryListener"
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
Log.d(tag, "Discovery failed: $serviceType, error code: $errorCode")
}
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
Log.d(tag, "Stop discovery failed: $serviceType, error code: $errorCode")
}
override fun onDiscoveryStarted(serviceType: String?) {
Log.d(tag, "Discovery started: $serviceType")
}
override fun onDiscoveryStopped(serviceType: String?) {
Log.d(tag, "Discovery stopped: $serviceType")
}
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
// Safe here as, java.lang.NoClassDefFoundError: Failed resolution of: Landroid/net/nsd/NsdManager$ServiceInfoCallback
safe {
if (serviceInfo == null) return@safe
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(
Build.VERSION_CODES.TIRAMISU
) >= 7
) {
nsdManager?.registerServiceInfoCallback(
serviceInfo,
Runnable::run,
object : NsdManager.ServiceInfoCallback {
override fun onServiceInfoCallbackRegistrationFailed(errorCode: Int) {
Log.e(tag, "Service registration failed: $errorCode")
}
override fun onServiceUpdated(serviceInfo: NsdServiceInfo) {
Log.d(
tag,
"Service updated: ${serviceInfo.serviceName}," +
"Net: ${serviceInfo.hostAddresses.firstOrNull()?.hostAddress}"
)
synchronized(_currentDevices) {
_currentDevices.removeIf { it.rawName == serviceInfo.serviceName }
_currentDevices.add(PublicDeviceInfo(serviceInfo))
}
}
override fun onServiceLost() {
Log.d(tag, "Service lost: ${serviceInfo.serviceName},")
synchronized(_currentDevices) {
_currentDevices.removeIf { it.rawName == serviceInfo.serviceName }
}
}
override fun onServiceInfoCallbackUnregistered() {}
})
} else {
@Suppress("DEPRECATION")
nsdManager?.resolveService(serviceInfo, object : ResolveListener {
override fun onResolveFailed(
serviceInfo: NsdServiceInfo?,
errorCode: Int
) {
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
if (serviceInfo == null) return
synchronized(_currentDevices) {
_currentDevices.add(PublicDeviceInfo(serviceInfo))
}
Log.d(
tag,
"Service found: ${serviceInfo.serviceName}, Net: ${serviceInfo.host.hostAddress}"
)
}
})
}
}
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
if (serviceInfo == null) return
// May remove duplicates, but net and port is null here, preventing device specific identification
synchronized(_currentDevices) {
_currentDevices.removeAll {
it.rawName == serviceInfo.serviceName
}
}
Log.d(tag, "Service lost: ${serviceInfo.serviceName}")
}
}
companion object {
const val APP_PREFIX = "CloudStream"
private val _currentDevices: MutableList<PublicDeviceInfo> = mutableListOf()
val currentDevices: List<PublicDeviceInfo> = _currentDevices
class DefaultRegistrationListener : NsdManager.RegistrationListener {
val tag = "DiscoveryService"
override fun onServiceRegistered(serviceInfo: NsdServiceInfo) {
Log.d(tag, "Service registered: ${serviceInfo.serviceName}")
}
override fun onRegistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
Log.e(tag, "Service registration failed: errorCode=$errorCode")
}
override fun onServiceUnregistered(serviceInfo: NsdServiceInfo) {
Log.d(tag, "Service unregistered: ${serviceInfo.serviceName}")
}
override fun onUnregistrationFailed(serviceInfo: NsdServiceInfo, errorCode: Int) {
Log.e(tag, "Service unregistration failed: errorCode=$errorCode")
}
}
const val TCP_PORT = 46899
}
}
class PublicDeviceInfo(serviceInfo: NsdServiceInfo) {
val rawName: String = serviceInfo.serviceName
val host: String? = if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
SdkExtensions.getExtensionVersion(
Build.VERSION_CODES.TIRAMISU
) >= 7
) {
serviceInfo.hostAddresses.firstOrNull()?.hostAddress
} else {
@Suppress("DEPRECATION")
serviceInfo.host.hostAddress
}
val name = rawName.replace("-", " ") + host?.let { " $it" }
}

View file

@ -1,60 +0,0 @@
package com.lagradost.cloudstream3.actions.temp.fcast
import android.util.Log
import androidx.annotation.WorkerThread
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.safefile.closeQuietly
import java.io.DataOutputStream
import java.net.Socket
import kotlin.jvm.Throws
class FcastSession(private val hostAddress: String): AutoCloseable {
val tag = "FcastSession"
private var socket: Socket? = null
@Throws
@WorkerThread
fun open(): Socket {
val socket = Socket(hostAddress, FcastManager.TCP_PORT)
this.socket = socket
return socket
}
override fun close() {
socket?.closeQuietly()
socket = null
}
@Throws
private fun acquireSocket(): Socket {
return socket ?: open()
}
fun ping() {
sendMessage(Opcode.Ping, null)
}
fun <T> sendMessage(opcode: Opcode, message: T) {
ioSafe {
val socket = acquireSocket()
val outputStream = DataOutputStream(socket.getOutputStream())
val json = message?.toJson()
val content = json?.toByteArray() ?: ByteArray(0)
// Little endian starting from 1
// https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
val size = content.size + 1
val sizeArray = ByteArray(4) { num ->
(size shr 8 * num and 0xff).toByte()
}
Log.d(tag, "Sending message with size: $size, opcode: $opcode")
outputStream.write(sizeArray)
outputStream.write(ByteArray(1) { opcode.value })
outputStream.write(content)
}
}
}

View file

@ -1,66 +0,0 @@
package com.lagradost.cloudstream3.actions.temp.fcast
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
// See https://gitlab.com/futo-org/fcast/-/wikis/Protocol-version-1
enum class Opcode(val value: Byte) {
None(0),
Play(1),
Pause(2),
Resume(3),
Stop(4),
Seek(5),
PlaybackUpdate(6),
VolumeUpdate(7),
SetVolume(8),
PlaybackError(9),
SetSpeed(10),
Version(11),
Ping(12),
Pong(13);
}
@Serializable
data class PlayMessage(
@JsonProperty("container") @SerialName("container") val container: String,
@JsonProperty("url") @SerialName("url") val url: String? = null,
@JsonProperty("content") @SerialName("content") val content: String? = null,
@JsonProperty("time") @SerialName("time") val time: Double? = null,
@JsonProperty("speed") @SerialName("speed") val speed: Double? = null,
@JsonProperty("headers") @SerialName("headers") val headers: Map<String, String>? = null,
)
data class SeekMessage(
val time: Double,
)
data class PlaybackUpdateMessage(
val generationTime: Long,
val time: Double,
val duration: Double,
val state: Int,
val speed: Double,
)
data class VolumeUpdateMessage(
val generationTime: Long,
val volume: Double,
)
data class PlaybackErrorMessage(
val message: String,
)
data class SetSpeedMessage(
val speed: Double,
)
data class SetVolumeMessage(
val volume: Double,
)
data class VersionMessage(
val version: Long,
)

View file

@ -0,0 +1,40 @@
package com.lagradost.cloudstream3.extractors
import android.util.Log
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
open class AStreamHub : ExtractorApi() {
override val name = "AStreamHub"
override val mainUrl = "https://astreamhub.com"
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
app.get(url).document.selectFirst("body > script").let { script ->
val text = script?.html() ?: ""
Log.i("Dev", "text => $text")
if (text.isNotBlank()) {
val m3link = "(?<=file:)(.*)(?=,)".toRegex().find(text)
?.groupValues?.get(0)?.trim()?.trim('"') ?: ""
Log.i("Dev", "m3link => $m3link")
if (m3link.isNotBlank()) {
sources.add(
ExtractorLink(
name = name,
source = name,
url = m3link,
isM3u8 = true,
quality = Qualities.Unknown.value,
referer = referer ?: url
)
)
}
}
}
return sources
}
}

View file

@ -0,0 +1,39 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.base64Decode
import com.lagradost.cloudstream3.utils.*
open class Acefile : ExtractorApi() {
override val name = "Acefile"
override val mainUrl = "https://acefile.co"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
app.get(url).document.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val data = getAndUnpack(script.data())
val id = data.substringAfter("{\"id\":\"").substringBefore("\",")
val key = data.substringAfter("var nfck=\"").substringBefore("\";")
app.get("https://acefile.co/local/$id?key=$key").text.let {
base64Decode(
it.substringAfter("JSON.parse(atob(\"").substringBefore("\"))")
).let { res ->
sources.add(
ExtractorLink(
name,
name,
res.substringAfter("\"file\":\"").substringBefore("\","),
"$mainUrl/",
Qualities.Unknown.value,
)
)
}
}
}
}
return sources
}
}

View file

@ -0,0 +1,46 @@
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.M3u8Helper
import com.lagradost.cloudstream3.utils.getQualityFromName
import java.net.URI
open class AsianLoad : ExtractorApi() {
override var name = "AsianLoad"
override var mainUrl = "https://asianembed.io"
override val requiresReferer = true
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
with(app.get(url, referer = referer)) {
sourceRegex.findAll(this.text).forEach { sourceMatch ->
val extractedUrl = sourceMatch.groupValues[1]
// Trusting this isn't mp4, may fuck up stuff
if (URI(extractedUrl).path.endsWith(".m3u8")) {
M3u8Helper.generateM3u8(
name,
extractedUrl,
url,
headers = mapOf("referer" to this.url)
).forEach { link ->
extractedLinksList.add(link)
}
} else if (extractedUrl.endsWith(".mp4")) {
extractedLinksList.add(
ExtractorLink(
name,
name,
extractedUrl,
url.replace(" ", "%20"),
getQualityFromName(sourceMatch.groupValues[2]),
)
)
}
}
return extractedLinksList
}
}
}

View file

@ -19,18 +19,17 @@ open class Blogger : ExtractorApi() {
.substringBefore("]") .substringBefore("]")
tryParseJson<List<ResponseSource>>("[$data]")?.map { tryParseJson<List<ResponseSource>>("[$data]")?.map {
sources.add( sources.add(
newExtractorLink( ExtractorLink(
name, name,
name, name,
it.play_url, it.play_url,
) { referer = "https://www.youtube.com/",
this.referer = "https://www.youtube.com/" quality = when (it.format_id) {
this.quality = when (it.format_id) {
18 -> 360 18 -> 360
22 -> 720 22 -> 720
else -> Qualities.Unknown.value else -> Qualities.Unknown.value
} }
} )
) )
} }
} }

View file

@ -0,0 +1,29 @@
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.M3u8Helper
open class BullStream : ExtractorApi() {
override val name = "BullStream"
override val mainUrl = "https://bullstream.xyz"
override val requiresReferer = false
val regex = Regex("(?<=sniff\\()(.*)(?=\\)\\);)")
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val data = regex.find(app.get(url).text)?.value
?.replace("\"", "")
?.split(",")
?: return null
val m3u8 = "$mainUrl/m3u8/${data[1]}/${data[2]}/master.txt?s=1&cache=${data[4]}"
//println("shiv : $m3u8")
return M3u8Helper.generateM3u8(
name,
m3u8,
url,
headers = mapOf("referer" to url, "accept" to "*/*")
)
}
}

View file

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

View file

@ -1,42 +1,39 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.utils.Qualities
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.Qualities import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.newExtractorLink import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import android.util.Log
import java.net.URLDecoder
open class Cda : ExtractorApi() { open class Cda: ExtractorApi() {
override var mainUrl = "https://ebd.cda.pl" override var mainUrl = "https://ebd.cda.pl"
override var name = "Cda" override var name = "Cda"
override val requiresReferer = false override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? { override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val mediaId = url val mediaId = url
.split("/").last() .split("/").last()
.split("?").first() .split("?").first()
val doc = app.get( val doc = app.get("https://ebd.cda.pl/647x500/$mediaId", headers=mapOf(
"https://ebd.cda.pl/647x500/$mediaId", headers = mapOf( "Referer" to "https://ebd.cda.pl/647x500/$mediaId",
"Referer" to "https://ebd.cda.pl/647x500/$mediaId", "User-Agent" to USER_AGENT,
"User-Agent" to USER_AGENT, "Cookie" to "cda.player=html5"
"Cookie" to "cda.player=html5" )).document
)
).document
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null
return listOf( return listOf(ExtractorLink(
newExtractorLink( name,
source = name, name,
name = name, getFile(playerData.video.file),
url = getFile(playerData.video.file), referer = "https://ebd.cda.pl/647x500/$mediaId",
) { quality = Qualities.Unknown.value
this.referer = "https://ebd.cda.pl/647x500/$mediaId" ))
this.quality = Qualities.Unknown.value
}
)
} }
private fun rot13(a: String): String { private fun rot13(a: String): String {
@ -51,7 +48,7 @@ open class Cda : ExtractorApi() {
private fun cdaUggc(a: String): String { private fun cdaUggc(a: String): String {
val decoded = rot13(a) val decoded = rot13(a)
return if (decoded.endsWith("adc.mp4")) decoded.replace("adc.mp4", ".mp4") return if (decoded.endsWith("adc.mp4")) decoded.replace("adc.mp4",".mp4")
else decoded else decoded
} }
@ -64,10 +61,10 @@ open class Cda : ExtractorApi() {
.replace("_QWE", "") .replace("_QWE", "")
.replace("_Q5", "") .replace("_Q5", "")
.replace("_IKSDE", "") .replace("_IKSDE", "")
a = a.decodeUrl() a = URLDecoder.decode(a, "UTF-8")
a = a.map { char -> a = a.map { char ->
if (char.code in 33..126) { if (32 < char.toInt() && char.toInt() < 127) {
return@map (33 + (char.code + 14) % 94).toChar().toString() return@map String.format("%c", 33 + (char.toInt() + 14) % 94)
} else { } else {
return@map char return@map char
} }
@ -77,7 +74,7 @@ open class Cda : ExtractorApi() {
.replace(".2cda.pl", ".cda.pl") .replace(".2cda.pl", ".cda.pl")
.replace(".3cda.pl", ".cda.pl") .replace(".3cda.pl", ".cda.pl")
return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream") return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream")
else "https://${a}.mp4" else "https://${a}.mp4"
} }
private fun getFile(a: String) = when { private fun getFile(a: String) = when {

View file

@ -0,0 +1,102 @@
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.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 { (key, video) ->
video.forEach {
callback.invoke(
ExtractorLink(
name,
"$name $key",
it.url,
"",
Qualities.Unknown.value,
true
)
)
}
}
}
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
}
data class Config(
val context: Context,
val dmInternalData: InternalData
)
data class InternalData(
val ts: Int,
val v1st: String
)
data class Context(
@JsonProperty("access_token") val accessToken: String?,
val dmvk: String,
)
data class MetaData(
val qualities: Map<String, List<VideoLink>>
)
data class VideoLink(
val type: String,
val url: String
)
}

View file

@ -0,0 +1,68 @@
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.getQualityFromName
import kotlinx.coroutines.delay
class DoodWfExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.wf"
}
class DoodCxExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.cx"
}
class DoodShExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.sh"
}
class DoodWatchExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.watch"
}
class DoodPmExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.pm"
}
class DoodToExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.to"
}
class DoodSoExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.so"
}
class DoodWsExtractor : DoodLaExtractor() {
override var mainUrl = "https://dood.ws"
}
open class DoodLaExtractor : ExtractorApi() {
override var name = "DoodStream"
override var mainUrl = "https://dood.la"
override val requiresReferer = false
override fun getExtractorUrl(id: String): String {
return "$mainUrl/d/$id"
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
return listOf(
ExtractorLink(
trueUrl,
this.name,
trueUrl,
mainUrl,
getQualityFromName(quality),
false
)
) // links are valid in 8h
}
}

View file

@ -6,7 +6,6 @@ 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
import com.lagradost.cloudstream3.utils.httpsify import com.lagradost.cloudstream3.utils.httpsify
import com.lagradost.cloudstream3.utils.newExtractorLink
open class Embedgram : ExtractorApi() { open class Embedgram : ExtractorApi() {
override val name = "Embedgram" override val name = "Embedgram"
@ -23,17 +22,16 @@ open class Embedgram : ExtractorApi() {
val link = document.select("video source:last-child").attr("src") val link = document.select("video source:last-child").attr("src")
val quality = document.select("video source:last-child").attr("title") val quality = document.select("video source:last-child").attr("title")
callback.invoke( callback.invoke(
newExtractorLink( ExtractorLink(
this.name, this.name,
this.name, this.name,
httpsify(link), httpsify(link),
) { "$mainUrl/",
this.referer = "$mainUrl/" getQualityFromName(quality),
this.quality = getQualityFromName(quality) headers = mapOf(
this.headers = mapOf(
"Range" to "bytes=0-" "Range" to "bytes=0-"
) )
} )
) )
} }
} }

View file

@ -16,21 +16,39 @@ 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 id = url.replace("https://evoload.io/e/", "") // wanted media id val lang = url.substring(0, 2)
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)
val r = app.post("https://evoload.io/SecurePlayer", data=(payload)).text val r = app.post("https://evoload.io/SecurePlayer", data=(payload)).text
val link = Regex("src\":\"(.*?)\"").find(r)?.destructured?.component1() ?: return listOf() val link = Regex("src\":\"(.*?)\"").find(r)?.destructured?.component1() ?: return listOf()
return listOf( return listOf(
newExtractorLink( ExtractorLink(
name,
name, name,
name + flag,
link, link,
) { cleaned_url,
this.referer = url Qualities.Unknown.value,
this.quality = Qualities.Unknown.value )
}
) )
} }
} }

View file

@ -0,0 +1,54 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
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.getAndUnpack
import org.jsoup.nodes.Document
open class Fastream: ExtractorApi() {
override var mainUrl = "https://fastream.to"
override var name = "Fastream"
override val requiresReferer = false
suspend fun getstream(
response: Document,
sources: ArrayList<ExtractorLink>): Boolean{
response.select("script").amap { script ->
if (script.data().contains(Regex("eval\\(function\\(p,a,c,k,e,[rd]"))) {
val unpacked = getAndUnpack(script.data())
//val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
val newm3u8link = unpacked.substringAfter("file:\"").substringBefore("\"")
//val m3u8link = m3u8regex.find(unpacked)?.value ?: return@forEach
generateM3u8(
name,
newm3u8link,
mainUrl
).forEach { 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
}
}

View file

@ -0,0 +1,38 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
open class Filesim : ExtractorApi() {
override val name = "Filesim"
override val mainUrl = "https://files.im"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
with(app.get(url).document) {
this.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
tryParseJson<List<ResponseSource>>("[$data]")?.map {
M3u8Helper.generateM3u8(
name,
it.file,
"$mainUrl/",
).forEach { m3uData -> sources.add(m3uData) }
}
}
}
}
return sources
}
private data class ResponseSource(
@JsonProperty("file") val file: String,
@JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String?
)
}

View file

@ -0,0 +1,44 @@
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
open class GMPlayer : ExtractorApi() {
override val name = "GM Player"
override val mainUrl = "https://gmplayer.xyz"
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val ref = referer ?: return null
val id = url.substringAfter("/video/").substringBefore("/")
val m3u8 = app.post(
"$mainUrl/player/index.php?data=$id&do=getVideo",
mapOf(
"accept" to "*/*",
"referer" to ref,
"x-requested-with" to "XMLHttpRequest",
"origin" to mainUrl
),
data = mapOf("hash" to id, "r" to ref)
).parsed<GmResponse>().videoSource ?: return null
return listOf(
ExtractorLink(
this.name,
this.name,
m3u8,
ref,
Qualities.Unknown.value,
headers = mapOf("accept" to "*/*"),
isM3u8 = true
)
)
}
private data class GmResponse(
val videoSource: String? = null
)
}

View file

@ -2,10 +2,14 @@ package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
class DatabaseGdrive2 : Gdriveplayer() { class DatabaseGdrive2 : Gdriveplayer() {
override var mainUrl = "https://databasegdriveplayer.co" override var mainUrl = "https://databasegdriveplayer.co"
@ -61,6 +65,78 @@ open class Gdriveplayer : ExtractorApi() {
?.data()?.let { getAndUnpack(it) } ?.data()?.let { getAndUnpack(it) }
} }
private fun String.decodeHex(): ByteArray {
check(length % 2 == 0) { "Must have an even length" }
return chunked(2)
.map { it.toInt(16).toByte() }
.toByteArray()
}
// https://stackoverflow.com/a/41434590/8166854
private fun GenerateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.digestLength
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
val generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0)
md.update(
generatedData,
generatedLength - digestLength,
digestLength
)
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
return listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize)
)
} catch (e: DigestException) {
return null
}
}
private fun cryptoAESHandler(
data: AesData,
pass: ByteArray,
encrypt: Boolean = true
): String? {
val (key, iv) = GenerateKeyAndIv(pass, data.s.decodeHex()) ?: return null
val cipher = Cipher.getInstance("AES/CBC/NoPadding")
return if (!encrypt) {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
String(cipher.doFinal(base64DecodeArray(data.ct)))
} else {
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(iv))
base64Encode(cipher.doFinal(data.ct.toByteArray()))
}
}
private fun Regex.first(str: String): String? { private fun Regex.first(str: String): String? {
return find(str)?.groupValues?.getOrNull(1) return find(str)?.groupValues?.getOrNull(1)
} }
@ -78,14 +154,14 @@ open class Gdriveplayer : ExtractorApi() {
val document = app.get(url).document val document = app.get(url).document
val eval = unpackJs(document)?.replace("\\", "") ?: return val eval = unpackJs(document)?.replace("\\", "") ?: return
val data = Regex("data='(\\S+?)'").first(eval) ?: return val data = tryParseJson<AesData>(Regex("data='(\\S+?)'").first(eval)) ?: return
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval) val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
?.split(Regex("\\D+")) ?.split(Regex("\\D+"))
?.joinToString("") { ?.joinToString("") {
it.toInt().toChar().toString() Char(it.toInt()).toString()
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.encodeToByteArray() } }.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
?: throw ErrorLoadingException("can't find password") ?: throw ErrorLoadingException("can't find password")
val decryptedData = cryptoAESHandler(data, password, false, false)?.let { getAndUnpack(it) }?.replace("\\", "") val decryptedData = cryptoAESHandler(data, password, false)?.let { getAndUnpack(it) }?.replace("\\", "")
val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],") val sourceData = decryptedData?.substringAfter("sources:[")?.substringBefore("],")
val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],") val subData = decryptedData?.substringAfter("tracks:[")?.substringBefore("],")
@ -94,33 +170,40 @@ open class Gdriveplayer : ExtractorApi() {
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(
newExtractorLink( ExtractorLink(
source = this.name, source = this.name,
name = this.name, name = this.name,
url = "${httpsify(link)}&res=$quality", url = "${httpsify(link)}&res=$quality",
) { referer = mainUrl,
this.referer = mainUrl quality = quality.toIntOrNull() ?: Qualities.Unknown.value,
this.quality = quality.toIntOrNull() ?: Qualities.Unknown.value headers = mapOf("Range" to "bytes=0-")
this.headers = mapOf("Range" to "bytes=0-") )
}
) )
} }
subData?.addMarks("file")?.addMarks("kind")?.addMarks("label").let { dataSub -> subData?.addMarks("file")?.addMarks("kind")?.addMarks("label").let { dataSub ->
tryParseJson<List<Tracks>>("[$dataSub]")?.map { sub -> tryParseJson<List<Tracks>>("[$dataSub]")?.map { sub ->
subtitleCallback.invoke( subtitleCallback.invoke(
newSubtitleFile( SubtitleFile(
sub.label, sub.label,
httpsify(sub.file) httpsify(sub.file)
) )
) )
} }
} }
} }
data class AesData(
@JsonProperty("ct") val ct: String,
@JsonProperty("iv") val iv: String,
@JsonProperty("s") val s: String
)
data class Tracks( data class Tracks(
@JsonProperty("file") val file: String, @JsonProperty("file") val file: String,
@JsonProperty("kind") val kind: String, @JsonProperty("kind") val kind: String,
@JsonProperty("label") val label: String, @JsonProperty("label") val label: String
) )
}
}

View file

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

View file

@ -0,0 +1,100 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
class Neonime7n : Hxfile() {
override val name = "Neonime7n"
override val mainUrl = "https://7njctn.neonime.watch"
override val redirect = false
}
class Neonime8n : Hxfile() {
override val name = "Neonime8n"
override val mainUrl = "https://8njctn.neonime.net"
override val redirect = false
}
class KotakAnimeid : Hxfile() {
override val name = "KotakAnimeid"
override val mainUrl = "https://kotakanimeid.com"
override val requiresReferer = true
}
class Yufiles : Hxfile() {
override val name = "Yufiles"
override val mainUrl = "https://yufiles.com"
}
class Aico : Hxfile() {
override val name = "Aico"
override val mainUrl = "https://aico.pw"
}
open class Hxfile : ExtractorApi() {
override val name = "Hxfile"
override val mainUrl = "https://hxfile.co"
override val requiresReferer = false
open val redirect = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val sources = mutableListOf<ExtractorLink>()
val document = app.get(url, allowRedirects = redirect, referer = referer).document
with(document) {
this.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val data =
getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
tryParseJson<List<ResponseSource>>("[$data]")?.map {
sources.add(
ExtractorLink(
name,
name,
it.file,
referer = mainUrl,
quality = when {
url.contains("hxfile.co") -> getQualityFromName(
Regex("\\d\\.(.*?).mp4").find(
document.select("title").text()
)?.groupValues?.get(1).toString()
)
else -> getQualityFromName(it.label)
}
)
)
}
} else if (script.data().contains("\"sources\":[")) {
val data = script.data().substringAfter("\"sources\":[").substringBefore("]")
tryParseJson<List<ResponseSource>>("[$data]")?.map {
sources.add(
ExtractorLink(
name,
name,
it.file,
referer = mainUrl,
quality = when {
it.label?.contains("HD") == true -> Qualities.P720.value
it.label?.contains("SD") == true -> Qualities.P480.value
else -> getQualityFromName(it.label)
}
)
)
}
}
else {
null
}
}
}
return sources
}
private data class ResponseSource(
@JsonProperty("file") val file: String,
@JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String?
)
}

View file

@ -0,0 +1,81 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
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.Qualities
import com.lagradost.cloudstream3.utils.getQualityFromName
class Meownime : JWPlayer() {
override val name = "Meownime"
override val mainUrl = "https://meownime.ltd"
}
class DesuOdchan : JWPlayer() {
override val name = "DesuOdchan"
override val mainUrl = "https://desustream.me/odchan/"
}
class DesuArcg : JWPlayer() {
override val name = "DesuArcg"
override val mainUrl = "https://desustream.me/arcg/"
}
class DesuDrive : JWPlayer() {
override val name = "DesuDrive"
override val mainUrl = "https://desustream.me/desudrive/"
}
class DesuOdvip : JWPlayer() {
override val name = "DesuOdvip"
override val mainUrl = "https://desustream.me/odvip/"
}
open class JWPlayer : ExtractorApi() {
override val name = "JWPlayer"
override val mainUrl = "https://www.jwplayer.com"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val sources = mutableListOf<ExtractorLink>()
with(app.get(url).document) {
val data = this.select("script").mapNotNull { script ->
if (script.data().contains("sources: [")) {
script.data().substringAfter("sources: [")
.substringBefore("],").replace("'", "\"")
} else if (script.data().contains("otakudesu('")) {
script.data().substringAfter("otakudesu('")
.substringBefore("');")
} else {
null
}
}
tryParseJson<List<ResponseSource>>("$data")?.map {
sources.add(
ExtractorLink(
name,
name,
it.file,
referer = url,
quality = getQualityFromName(
Regex("(\\d{3,4}p)").find(it.file)?.groupValues?.get(
1
)
)
)
)
}
}
return sources
}
private data class ResponseSource(
@JsonProperty("file") val file: String,
@JsonProperty("type") val type: String?,
@JsonProperty("label") val label: String?
)
}

View file

@ -0,0 +1,27 @@
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.M3u8Helper
open class Jawcloud : ExtractorApi() {
override var name = "Jawcloud"
override var mainUrl = "https://jawcloud.co"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val doc = app.get(url).document
val urlString = doc.select("html body div source").attr("src")
val sources = mutableListOf<ExtractorLink>()
if (urlString.contains("m3u8"))
M3u8Helper.generateM3u8(
name,
urlString,
url,
headers = app.get(url).headers.toMap()
).forEach { link -> sources.add(link) }
return sources
}
}

View file

@ -3,12 +3,8 @@ 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.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.extractors.helper.JwPlayerHelper import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
import com.lagradost.cloudstream3.utils.getAndUnpack
import com.lagradost.cloudstream3.utils.getPacked
open class Jeniusplay : ExtractorApi() { open class Jeniusplay : ExtractorApi() {
override val name = "Jeniusplay" override val name = "Jeniusplay"
@ -37,17 +33,40 @@ open class Jeniusplay : ExtractorApi() {
url, url,
).forEach(callback) ).forEach(callback)
document.select("script").map { script -> document.select("script").map { script ->
if (getPacked(script.data()) != null) { if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val unpacked = getAndUnpack(script.data()) val subData =
JwPlayerHelper.extractStreamLinks(unpacked, name, mainUrl, callback, subtitleCallback) 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( data class ResponseSource(
@JsonProperty("hls") val hls: Boolean, @JsonProperty("hls") val hls: Boolean,
@JsonProperty("videoSource") val videoSource: String, @JsonProperty("videoSource") val videoSource: String,
@JsonProperty("securedLink") val securedLink: String?, @JsonProperty("securedLink") val securedLink: String?,
) )
data class Tracks(
@JsonProperty("kind") val kind: String?,
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?,
)
} }

View file

@ -0,0 +1,48 @@
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.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getQualityFromName
open class Linkbox : ExtractorApi() {
override val name = "Linkbox"
override val mainUrl = "https://www.linkbox.to"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val id = Regex("""(/file/|id=)(\S+)[&/?]""").find(url)?.groupValues?.get(2)
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
callback.invoke(
ExtractorLink(
name,
name,
link.url,
url,
getQualityFromName(link.resolution)
)
)
}
}
data class RList(
@JsonProperty("url") val url: String,
@JsonProperty("resolution") val resolution: String?,
)
data class Data(
@JsonProperty("rList") val rList: List<RList>?,
)
data class Responses(
@JsonProperty("data") val data: Data?,
)
}

View file

@ -3,30 +3,10 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
class MixDropPs : MixDrop() {
override var mainUrl = "https://mixdrop.ps"
}
class Mdy : MixDrop() {
override var mainUrl = "https://mdy48tn97.com"
}
class MxDropTo : MixDrop() {
override var mainUrl = "https://mxdrop.to"
}
class MixDropSi : MixDrop() {
override var mainUrl = "https://mixdrop.si"
}
class MixDropBz : MixDrop(){ class MixDropBz : MixDrop(){
override var mainUrl = "https://mixdrop.bz" override var mainUrl = "https://mixdrop.bz"
} }
class MixDropAg : MixDrop(){
override var mainUrl = "https://mixdrop.ag"
}
class MixDropCh : MixDrop(){ class MixDropCh : MixDrop(){
override var mainUrl = "https://mixdrop.ch" override var mainUrl = "https://mixdrop.ch"
} }
@ -45,22 +25,21 @@ open class MixDrop : 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.replaceFirst("/f/", "/e/"))) { with(app.get(url)) {
getAndUnpack(this.text).let { unpackedText -> getAndUnpack(this.text).let { unpackedText ->
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link -> srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
return listOf( return listOf(
newExtractorLink( ExtractorLink(
name, name,
name, name,
httpsify(link), httpsify(link),
) { url,
this.referer = url Qualities.Unknown.value,
this.quality = Qualities.Unknown.value )
}
) )
} }
} }
} }
return null return null
} }
} }

View file

@ -2,13 +2,12 @@ package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.newSubtitleFile
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.M3u8Helper
class MoviehabNet : Moviehab() { class MoviehabNet : Moviehab() {
override var mainUrl = "https://play.moviehab.asia" override var mainUrl = "https://play.moviehab.net"
} }
open class Moviehab : ExtractorApi() { open class Moviehab : ExtractorApi() {
@ -34,7 +33,7 @@ open class Moviehab : ExtractorApi() {
Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub -> Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub ->
subtitleCallback.invoke( subtitleCallback.invoke(
newSubtitleFile( SubtitleFile(
it.select("track").attr("label"), it.select("track").attr("label"),
"$mainUrl/$sub" "$mainUrl/$sub"
) )
@ -42,4 +41,4 @@ open class Moviehab : ExtractorApi() {
} }
} }
} }
} }

View file

@ -0,0 +1,34 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.getAndUnpack
open class Mp4Upload : ExtractorApi() {
override var name = "Mp4Upload"
override var mainUrl = "https://www.mp4upload.com"
private val srcRegex = Regex("""player\.src\("(.*?)"""")
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
with(app.get(url)) {
getAndUnpack(this.text).let { unpackedText ->
val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
return listOf(
ExtractorLink(
name,
name,
link,
url,
quality ?: Qualities.Unknown.value,
)
)
}
}
}
return null
}
}

View file

@ -0,0 +1,59 @@
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.getQualityFromName
import java.net.URI
open class MultiQuality : ExtractorApi() {
override var name = "MultiQuality"
override var mainUrl = "https://gogo-play.net"
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
private val urlRegex = Regex("""(.*?)([^/]+$)""")
override val requiresReferer = false
override fun getExtractorUrl(id: String): String {
return "$mainUrl/loadserver.php?id=$id"
}
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
with(app.get(url)) {
sourceRegex.findAll(this.text).forEach { sourceMatch ->
val extractedUrl = sourceMatch.groupValues[1]
// Trusting this isn't mp4, may fuck up stuff
if (URI(extractedUrl).path.endsWith(".m3u8")) {
with(app.get(extractedUrl)) {
m3u8Regex.findAll(this.text).forEach { match ->
extractedLinksList.add(
ExtractorLink(
name,
name = name,
urlRegex.find(this.url)!!.groupValues[1] + match.groupValues[0],
url,
getQualityFromName(match.groupValues[1]),
isM3u8 = true
)
)
}
}
} else if (extractedUrl.endsWith(".mp4")) {
extractedLinksList.add(
ExtractorLink(
name,
"$name ${sourceMatch.groupValues[2]}",
extractedUrl,
url.replace(" ", "%20"),
Qualities.Unknown.value,
)
)
}
}
return extractedLinksList
}
}
}

View file

@ -5,7 +5,6 @@ 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.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.newExtractorLink
open class Mvidoo : ExtractorApi() { open class Mvidoo : ExtractorApi() {
override val name = "Mvidoo" override val name = "Mvidoo"
@ -14,10 +13,11 @@ open class Mvidoo : ExtractorApi() {
private fun String.decodeHex(): String { private fun String.decodeHex(): String {
require(length % 2 == 0) { "Must have an even length" } require(length % 2 == 0) { "Must have an even length" }
return chunked(2) return String(
.map { it.toInt(16).toByte() } chunked(2)
.toByteArray() .map { it.toInt(16).toByte() }
.decodeToString() .toByteArray()
)
} }
override suspend fun getUrl( override suspend fun getUrl(
@ -31,17 +31,16 @@ open class Mvidoo : ExtractorApi() {
?.removeSurrounding("[", "]")?.replace("\"", "")?.replace("\\x", "")?.split(",")?.map { it.decodeHex() }?.reversed()?.joinToString("") ?: return ?.removeSurrounding("[", "]")?.replace("\"", "")?.replace("\\x", "")?.split(",")?.map { it.decodeHex() }?.reversed()?.joinToString("") ?: return
Regex("source\\s*src=\"([^\"]+)").find(data)?.groupValues?.get(1)?.let { link -> Regex("source\\s*src=\"([^\"]+)").find(data)?.groupValues?.get(1)?.let { link ->
callback.invoke( callback.invoke(
newExtractorLink( ExtractorLink(
this.name, this.name,
this.name, this.name,
link link,
) { "$mainUrl/",
this.referer = "$mainUrl/" Qualities.Unknown.value,
this.quality = Qualities.Unknown.value headers = mapOf(
this.headers = mapOf(
"Range" to "bytes=0-" "Range" to "bytes=0-"
) )
} )
) )
} }
} }

View file

@ -0,0 +1,67 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.parseJson
data class DataOptionsJson (
@JsonProperty("flashvars") var flashvars : Flashvars? = Flashvars(),
)
data class Flashvars (
@JsonProperty("metadata") var metadata : String? = null,
@JsonProperty("hlsManifestUrl") var hlsManifestUrl : String? = null, //m3u8
)
data class MetadataOkru (
@JsonProperty("videos") var videos: ArrayList<Videos> = arrayListOf(),
)
data class Videos (
@JsonProperty("name") var name : String,
@JsonProperty("url") var url : String,
@JsonProperty("seekSchema") var seekSchema : Int? = null,
@JsonProperty("disallowed") var disallowed : Boolean? = null
)
class OkRuHttps: OkRu(){
override var mainUrl = "https://ok.ru"
}
open class OkRu : ExtractorApi() {
override var name = "Okru"
override var mainUrl = "http://ok.ru"
override val requiresReferer = false
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
val doc = app.get(url).document
val sources = ArrayList<ExtractorLink>()
val datajson = doc.select("div[data-options]").attr("data-options")
if (datajson.isNotBlank()) {
val main = parseJson<DataOptionsJson>(datajson)
val metadatajson = parseJson<MetadataOkru>(main.flashvars?.metadata!!)
val servers = metadatajson.videos
servers.forEach {
val quality = it.name.uppercase()
.replace("MOBILE","144p")
.replace("LOWEST","240p")
.replace("LOW","360p")
.replace("SD","480p")
.replace("HD","720p")
.replace("FULL","1080p")
.replace("QUAD","1440p")
.replace("ULTRA","4k")
val extractedurl = it.url.replace("\\\\u0026", "&")
sources.add(ExtractorLink(
name,
name = this.name,
extractedurl,
url,
getQualityFromName(quality),
isM3u8 = false
))
}
}
return sources
}
}

View file

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

View file

@ -0,0 +1,99 @@
package com.lagradost.cloudstream3.extractors
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.extractorApis
import com.lagradost.cloudstream3.utils.getQualityFromName
import com.lagradost.cloudstream3.utils.loadExtractor
import org.jsoup.Jsoup
/**
* overrideMainUrl is necessary for for other vidstream clones like vidembed.cc
* If they diverge it'd be better to make them separate.
* */
open class Pelisplus(val mainUrl: String) {
val name: String = "Vidstream"
private fun getExtractorUrl(id: String): String {
return "$mainUrl/play?id=$id"
}
private fun getDownloadUrl(id: String): String {
return "$mainUrl/download?id=$id"
}
private val normalApis = arrayListOf(MultiQuality())
// https://gogo-stream.com/streaming.php?id=MTE3NDg5
suspend fun getUrl(
id: String,
isCasting: Boolean = false,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
): Boolean {
try {
normalApis.amap { api ->
val url = api.getExtractorUrl(id)
api.getSafeUrl(url, subtitleCallback = subtitleCallback, callback = callback)
}
val extractorUrl = getExtractorUrl(id)
/** Stolen from GogoanimeProvider.kt extractor */
suspendSafeApiCall {
val link = getDownloadUrl(id)
println("Generated vidstream download link: $link")
val page = app.get(link, referer = extractorUrl)
val pageDoc = Jsoup.parse(page.text)
val qualityRegex = Regex("(\\d+)P")
//a[download]
pageDoc.select(".dowload > a")?.amap { element ->
val href = element.attr("href") ?: return@amap
val qual = if (element.text()
.contains("HDP")
) "1080" else qualityRegex.find(element.text())?.destructured?.component1()
.toString()
if (!loadExtractor(href, link, subtitleCallback, callback)) {
callback.invoke(
ExtractorLink(
this.name,
name = this.name,
href,
page.url,
getQualityFromName(qual),
element.attr("href").contains(".m3u8")
)
)
}
}
}
with(app.get(extractorUrl)) {
val document = Jsoup.parse(this.text)
val primaryLinks = document.select("ul.list-server-items > li.linkserver")
//val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
// All vidstream links passed to extractors
primaryLinks.distinctBy { it.attr("data-video") }.forEach { element ->
val link = element.attr("data-video")
//val name = element.text()
// Matches vidstream links with extractors
extractorApis.filter { !it.requiresReferer || !isCasting }.amap { api ->
if (link.startsWith(api.mainUrl)) {
api.getSafeUrl(link, extractorUrl, subtitleCallback, callback)
}
}
}
return true
}
} catch (e: Exception) {
return false
}
}
}

View file

@ -1,6 +1,6 @@
package com.lagradost.cloudstream3.extractors package com.lagradost.cloudstream3.extractors
import com.lagradost.api.Log import android.util.Log
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
@ -24,7 +24,7 @@ open class PlayLtXyz: ExtractorApi() {
val doc = app.get(url, referer = referer).document val doc = app.get(url, referer = referer).document
//Log.i(this.name, "Result => (url, script) $url / ${doc.select("script")}") //Log.i(this.name, "Result => (url, script) $url / ${doc.select("script")}")
bodyText = doc.select("script").firstOrNull { bodyText = doc.select("script").firstOrNull {
val text = it.toString() val text = it?.toString() ?: ""
text.contains("var idUser") text.contains("var idUser")
}?.toString() ?: "" }?.toString() ?: ""
//Log.i(this.name, "Result => (bodyText) $bodyText") //Log.i(this.name, "Result => (bodyText) $bodyText")
@ -61,15 +61,14 @@ open class PlayLtXyz: ExtractorApi() {
val linkUrl = item.data ?: "" val linkUrl = item.data ?: ""
if (linkUrl.isNotBlank()) { if (linkUrl.isNotBlank()) {
extractedLinksList.add( extractedLinksList.add(
newExtractorLink( ExtractorLink(
source = name, source = name,
name = name, name = name,
url = linkUrl, url = linkUrl,
type = ExtractorLinkType.M3U8 referer = url,
) { quality = Qualities.Unknown.value,
this.referer = url isM3u8 = true
this.quality = Qualities.Unknown.value )
}
) )
} }
} }

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