mirror of
https://github.com/recloudstream/cloudstream.git
synced 2026-06-14 19:19:43 +00:00
Compare commits
6 commits
master
...
Aria2cStre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bef34c33e9 | ||
|
|
fca8a55e05 |
||
|
|
49b905c089 | ||
|
|
afe82140fd | ||
|
|
8105231a6b | ||
|
|
d394f0e1d0 |
941 changed files with 36920 additions and 86113 deletions
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/application-bug.yml
vendored
|
|
@ -80,13 +80,13 @@ body:
|
|||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
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.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I have updated the app to pre-release version **[Latest](https://github.com/recloudstream/cloudstream/releases)**.
|
||||
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.
|
||||
required: true
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -2,7 +2,7 @@ blank_issues_enabled: false
|
|||
contact_links:
|
||||
- name: Request a new provider or report bug with an existing provider
|
||||
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
|
||||
url: https://discord.gg/5Hus6fM
|
||||
about: Join our discord for faster support on smaller issues.
|
||||
|
|
|
|||
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
|
|
@ -27,7 +27,9 @@ body:
|
|||
label: Acknowledgements
|
||||
description: Your issue will be closed if you haven't done these steps.
|
||||
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.
|
||||
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
|
||||
|
|
|
|||
28
.github/locales.py
vendored
28
.github/locales.py
vendored
|
|
@ -7,7 +7,7 @@ import lxml.etree as ET # builtin library doesn't preserve comments
|
|||
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
||||
START_MARKER = "/* begin language list */"
|
||||
END_MARKER = "/* end language list */"
|
||||
XML_NAME = "app/src/main/res/values-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"
|
||||
INDENT = " "*4
|
||||
|
||||
|
|
@ -20,29 +20,29 @@ rest, after_src = rest.split(END_MARKER)
|
|||
|
||||
# Load already added langs
|
||||
languages = {}
|
||||
for lang in re.finditer(r'Pair\("(.*)", "(.*)"\)', rest):
|
||||
name, iso = lang.groups()
|
||||
languages[iso] = name
|
||||
for lang in re.finditer(r'Triple\("(.*)", "(.*)", "(.*)"\)', rest):
|
||||
flag, name, iso = lang.groups()
|
||||
languages[iso] = (flag, name)
|
||||
|
||||
# Add not yet added langs
|
||||
for folder in glob.glob(f"{XML_NAME}*"):
|
||||
iso = folder[len(XML_NAME):].replace("+", "-")
|
||||
iso = folder[len(XML_NAME):]
|
||||
if iso not in languages.keys():
|
||||
entry = iso_map.get(iso.lower(), {'nativeName':iso}) # fallback to iso code if not found
|
||||
languages[iso] = entry['nativeName'].split(',')[0] # first name if there are multiple
|
||||
entry = iso_map.get(iso.lower(),{'nativeName':iso})
|
||||
languages[iso] = ("", entry['nativeName'].split(',')[0])
|
||||
|
||||
# Create pairs
|
||||
pairs = []
|
||||
for iso in sorted(languages, key=lambda iso: languages[iso].lower()): # sort by language name
|
||||
name = languages[iso]
|
||||
pairs.append(f'{INDENT}Pair("{name}", "{iso}"),')
|
||||
# Create triples
|
||||
triples = []
|
||||
for iso in sorted(languages.keys()):
|
||||
flag, name = languages[iso]
|
||||
triples.append(f'{INDENT}Triple("{flag}", "{name}", "{iso}"),')
|
||||
|
||||
# Update settings file
|
||||
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
|
||||
before_src +
|
||||
START_MARKER +
|
||||
"\n" +
|
||||
"\n".join(pairs) +
|
||||
"\n".join(triples) +
|
||||
"\n" +
|
||||
END_MARKER +
|
||||
after_src
|
||||
|
|
@ -53,8 +53,6 @@ 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/")
|
||||
|
|
|
|||
171
.github/workflows/build_to_archive.yml
vendored
171
.github/workflows/build_to_archive.yml
vendored
|
|
@ -1,93 +1,78 @@
|
|||
name: Archive build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.json'
|
||||
- '**/wcokey.txt'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: "Archive-build"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/secrets"
|
||||
|
||||
- name: Generate access token (archive)
|
||||
id: generate_archive_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream-archive"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- 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: Fetch keystore
|
||||
id: fetch_keystore
|
||||
run: |
|
||||
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
|
||||
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
|
||||
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
|
||||
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
|
||||
KEY_PWD="$(cat keystore_password.txt)"
|
||||
echo "::add-mask::${KEY_PWD}"
|
||||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
|
||||
- name: Run Gradle
|
||||
run: ./gradlew assemblePrereleaseRelease
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: "key0"
|
||||
SIGNING_KEY_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 }}
|
||||
|
||||
- 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
|
||||
name: Archive build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
- '*.json'
|
||||
- '**/wcokey.txt'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: "Archive-build"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/secrets"
|
||||
- name: Generate access token (archive)
|
||||
id: generate_archive_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream-archive"
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Fetch keystore
|
||||
id: fetch_keystore
|
||||
run: |
|
||||
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
|
||||
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
|
||||
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
|
||||
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
|
||||
KEY_PWD="$(cat keystore_password.txt)"
|
||||
echo "::add-mask::${KEY_PWD}"
|
||||
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||
- name: Run Gradle
|
||||
run: |
|
||||
./gradlew assemblePrerelease
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: "key0"
|
||||
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||
SIMKL_CLIENT_ID: ${{ secrets.SIMKL_CLIENT_ID }}
|
||||
SIMKL_CLIENT_SECRET: ${{ secrets.SIMKL_CLIENT_SECRET }}
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: "recloudstream/cloudstream-archive"
|
||||
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||
path: "archive"
|
||||
|
||||
- name: Move build
|
||||
run: |
|
||||
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
|
||||
|
||||
- name: Push archive
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/archive
|
||||
git config --local user.email "actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
git add .
|
||||
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
|
||||
git push --force
|
||||
45
.github/workflows/generate_dokka.yml
vendored
45
.github/workflows/generate_dokka.yml
vendored
|
|
@ -1,67 +1,64 @@
|
|||
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:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches:
|
||||
# choose your default branch
|
||||
- master
|
||||
- main
|
||||
paths-ignore:
|
||||
- '*.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: "dokka"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/dokka"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
path: "src"
|
||||
|
||||
- name: Checkout dokka
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@master
|
||||
with:
|
||||
repository: "recloudstream/dokka"
|
||||
path: "dokka"
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
|
||||
- name: Clean old builds
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/dokka/
|
||||
rm -rf "./app"
|
||||
rm -rf "./library"
|
||||
rm -rf "./-cloudstream"
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- name: Setup JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v2
|
||||
|
||||
- name: Generate Dokka
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE/src/
|
||||
chmod +x gradlew
|
||||
./gradlew docs:dokkaGeneratePublicationHtml
|
||||
./gradlew app:dokkaHtml
|
||||
|
||||
- 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
|
||||
run: |
|
||||
|
|
|
|||
88
.github/workflows/issue_action.yml
vendored
Normal file
88
.github/workflows/issue_action.yml
vendored
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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
|
||||
id: similarity
|
||||
uses: actions-cool/issues-similarity-analysis@v1
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
filter-threshold: 0.60
|
||||
title-excludes: ''
|
||||
comment-title: |
|
||||
### Your issue looks similar to these issues:
|
||||
Please close if duplicate.
|
||||
comment-body: '${index}. ${similarity} #${number}'
|
||||
- name: Label if possible duplicate
|
||||
if: steps.similarity.outputs.similar-issues-found =='true'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["possible duplicate"]
|
||||
})
|
||||
- uses: actions/checkout@v2
|
||||
- name: Automatically close issues that dont follow the issue template
|
||||
uses: lucasbento/auto-close-issues@v1.0.2
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
issue-close-message: |
|
||||
@${issue.user.login}: hello! :wave:
|
||||
This issue is being automatically closed because it does not follow the issue template."
|
||||
closed-issues-label: "invalid"
|
||||
- name: Check if issue mentions a provider
|
||||
id: provider_check
|
||||
env:
|
||||
GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
|
||||
run: |
|
||||
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
|
||||
pip3 install httpx
|
||||
RES="$(python3 ./check_issue.py)"
|
||||
echo "name=${RES}" >> $GITHUB_OUTPUT
|
||||
- name: Comment if issue mentions a provider
|
||||
if: steps.provider_check.outputs.name != 'none'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
body: |
|
||||
Hello ${{ github.event.issue.user.login }}.
|
||||
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
|
||||
|
||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||
- name: Label if mentions provider
|
||||
if: steps.provider_check.outputs.name != 'none'
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
script: |
|
||||
github.rest.issues.addLabels({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
labels: ["possible provider issue"]
|
||||
})
|
||||
- name: Add eyes reaction to all issues
|
||||
uses: actions-cool/emoji-helper@v1.0.0
|
||||
with:
|
||||
type: 'issue'
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
emoji: 'eyes'
|
||||
|
||||
|
||||
33
.github/workflows/prerelease.yml
vendored
33
.github/workflows/prerelease.yml
vendored
|
|
@ -8,36 +8,29 @@ on:
|
|||
- '*.json'
|
||||
- '**/wcokey.txt'
|
||||
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: "pre-release"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/secrets"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Fetch keystore
|
||||
id: fetch_keystore
|
||||
run: |
|
||||
|
|
@ -48,25 +41,17 @@ jobs:
|
|||
KEY_PWD="$(cat keystore_password.txt)"
|
||||
echo "::add-mask::${KEY_PWD}"
|
||||
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
|
||||
run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar
|
||||
run: |
|
||||
./gradlew assemblePrerelease makeJar androidSourcesJar
|
||||
env:
|
||||
SIGNING_KEY_ALIAS: "key0"
|
||||
SIGNING_KEY_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 }}
|
||||
|
||||
- name: Create pre-release
|
||||
uses: marvinpinto/action-automatic-releases@latest
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
automatic_release_tag: "pre-release"
|
||||
|
|
|
|||
25
.github/workflows/pull_request.yml
vendored
25
.github/workflows/pull_request.yml
vendored
|
|
@ -2,35 +2,22 @@ name: Artifact Build
|
|||
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
- 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: Run Gradle
|
||||
run: ./gradlew assemblePrereleaseDebug lint check
|
||||
|
||||
run: ./gradlew assemblePrereleaseDebug
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: pull-request-build
|
||||
path: "app/build/outputs/apk/prerelease/debug/*.apk"
|
||||
|
|
|
|||
24
.github/workflows/update_locales.yml
vendored
24
.github/workflows/update_locales.yml
vendored
|
|
@ -1,41 +1,37 @@
|
|||
name: Fix locale issues
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**.xml'
|
||||
workflow_dispatch:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
concurrency:
|
||||
group: "locale"
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
create:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip3 install lxml requests
|
||||
|
||||
run: |
|
||||
pip3 install lxml
|
||||
- name: Edit files
|
||||
run: python3 .github/locales.py
|
||||
|
||||
run: |
|
||||
python3 .github/locales.py
|
||||
- name: Commit to the repo
|
||||
run: |
|
||||
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
|
||||
|
|
|
|||
220
.gitignore
vendored
220
.gitignore
vendored
|
|
@ -1,3 +1,5 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/misc.xml
|
||||
|
|
@ -9,220 +11,6 @@
|
|||
.DS_Store
|
||||
/build
|
||||
/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
|
||||
|
||||
# NDK
|
||||
obj/
|
||||
|
||||
# IntelliJ IDEA
|
||||
*.iws
|
||||
/out/
|
||||
|
||||
# User-specific configurations
|
||||
.idea/caches/
|
||||
.idea/libraries/
|
||||
.idea/shelf/
|
||||
.idea/workspace.xml
|
||||
.idea/tasks.xml
|
||||
.idea/.name
|
||||
.idea/compiler.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
.idea/encodings.xml
|
||||
.idea/misc.xml
|
||||
.idea/modules.xml
|
||||
.idea/scopes/scope_settings.xml
|
||||
.idea/dictionaries
|
||||
.idea/vcs.xml
|
||||
.idea/jsLibraryMappings.xml
|
||||
.idea/datasources.xml
|
||||
.idea/dataSources.ids
|
||||
.idea/sqlDataSources.xml
|
||||
.idea/dynamic.xml
|
||||
.idea/uiDesigner.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.idea/gradle.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Legacy Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
.cproject
|
||||
.settings/
|
||||
|
||||
# Mobile Tools for Java (J2ME)
|
||||
|
||||
# Package Files #
|
||||
|
||||
# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
|
||||
|
||||
## Plugin-specific files:
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/mongoSettings.xml
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
### AndroidStudio Patch ###
|
||||
|
||||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/kotlin,java,android,androidstudio,visualstudiocode
|
||||
.cxx
|
||||
local.properties
|
||||
|
|
|
|||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
CloudStream
|
||||
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal 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
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal 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
6
.idea/compiler.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal 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>
|
||||
20
.idea/gradle.xml
generated
Normal file
20
.idea/gradle.xml
generated
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?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="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
40
.idea/jarRepositories.xml
generated
Normal file
40
.idea/jarRepositories.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
6
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"githubPullRequests.ignoredPullRequestBranches": [
|
||||
"master"
|
||||
],
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
11
AI-POLICY.md
11
AI-POLICY.md
|
|
@ -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.
|
||||
98
README.md
98
README.md
|
|
@ -1,46 +1,11 @@
|
|||
# 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.**
|
||||
|
||||
|
||||
[](https://discord.gg/5Hus6fM)
|
||||
|
||||
|
||||
## 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:
|
||||
### Features:
|
||||
+ **AdFree**, No ads whatsoever
|
||||
+ No tracking/analytics
|
||||
+ Bookmarks
|
||||
|
|
@ -48,64 +13,7 @@ Our documentation is unmaintained and open to contributions; therefore, apps and
|
|||
+ Chromecast
|
||||
+ Extension system for personal customization
|
||||
|
||||
|
||||
<a id="install_rules"></a>
|
||||
|
||||
## 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:
|
||||
|
||||
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/">
|
||||
<img src="https://hosted.weblate.org/widgets/cloudstream/-/app/multi-auto.svg" alt="Translation status" />
|
||||
</a>
|
||||
|
|
|
|||
6
app/CMakeLists.txt
Normal file
6
app/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# Set this to the minimum version your project supports.
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
project(CrashHandler)
|
||||
find_library(log-lib log)
|
||||
add_library(native-lib SHARED src/main/cpp/native-lib.cpp)
|
||||
target_link_libraries(native-lib ${log-lib})
|
||||
|
|
@ -1,96 +1,48 @@
|
|||
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||
import org.jetbrains.dokka.gradle.engine.parameters.KotlinPlatform
|
||||
import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile
|
||||
import org.jetbrains.dokka.gradle.DokkaTask
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URL
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.dokka)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
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() {
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val headFile: RegularFileProperty
|
||||
|
||||
@get:InputDirectory
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val headsDir: DirectoryProperty
|
||||
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
@TaskAction
|
||||
fun generate() {
|
||||
val head = headFile.get().asFile
|
||||
|
||||
val hash = try {
|
||||
if (head.exists()) {
|
||||
// Read the commit hash from .git/HEAD
|
||||
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"))
|
||||
fun String.execute() = ByteArrayOutputStream().use { baot ->
|
||||
if (project.exec {
|
||||
workingDir = projectDir
|
||||
commandLine = this@execute.split(Regex("\\s"))
|
||||
standardOutput = baot
|
||||
}.exitValue == 0)
|
||||
String(baot.toByteArray()).trim()
|
||||
else null
|
||||
}
|
||||
|
||||
android {
|
||||
@Suppress("UnstableApiUsage")
|
||||
testOptions {
|
||||
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
|
||||
viewBinding {
|
||||
enable = true
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(
|
||||
generateGitHash,
|
||||
GenerateGitHashTask::outputDir
|
||||
)
|
||||
}
|
||||
}
|
||||
// disable this for now
|
||||
//externalNativeBuild {
|
||||
// cmake {
|
||||
// path("CMakeLists.txt")
|
||||
// }
|
||||
//}
|
||||
|
||||
signingConfigs {
|
||||
// We just use SIGNING_KEY_ALIAS here since it won't change
|
||||
// so won't kill the configuration cache.
|
||||
if (System.getenv("SIGNING_KEY_ALIAS") != null) {
|
||||
create("prerelease") {
|
||||
val tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||
val prereleaseStoreFile: File? = File(tmpFilePath).listFiles()?.first()
|
||||
|
||||
storeFile = prereleaseStoreFile?.let { file(it) }
|
||||
create("prerelease") {
|
||||
if (prereleaseStoreFile != null) {
|
||||
storeFile = file(prereleaseStoreFile)
|
||||
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
|
||||
|
|
@ -98,24 +50,28 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
compileSdk = 33
|
||||
buildToolsVersion = "34.0.0"
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.lagradost.cloudstream3"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = libs.versions.versionCode.get().toInt()
|
||||
versionName = libs.versions.versionName.get()
|
||||
minSdk = 21
|
||||
targetSdk = 33
|
||||
|
||||
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
|
||||
versionCode = 59
|
||||
versionName = "4.1.8"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
resValue("bool", "is_prerelease", "false")
|
||||
|
||||
// Reads local.properties
|
||||
val localProperties = gradleLocalProperties(rootDir, project.providers)
|
||||
val localProperties = gradleLocalProperties(rootDir)
|
||||
|
||||
buildConfigField(
|
||||
"long",
|
||||
"BUILD_DATE",
|
||||
"${System.currentTimeMillis()}"
|
||||
"String",
|
||||
"BUILDDATE",
|
||||
"new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));"
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
|
|
@ -128,6 +84,10 @@ android {
|
|||
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
|
||||
)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
kapt {
|
||||
includeCompileClasspath = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
|
@ -149,195 +109,186 @@ android {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions.add("state")
|
||||
productFlavors {
|
||||
create("stable") {
|
||||
dimension = "state"
|
||||
resValue("bool", "is_prerelease", "false")
|
||||
}
|
||||
create("prerelease") {
|
||||
dimension = "state"
|
||||
resValue("bool", "is_prerelease", "true")
|
||||
buildConfigField("boolean", "BETA", "true")
|
||||
applicationIdSuffix = ".prerelease"
|
||||
if (signingConfigs.names.contains("prerelease")) {
|
||||
signingConfig = signingConfigs.getByName("prerelease")
|
||||
} else {
|
||||
logger.warn("No prerelease signing config!")
|
||||
}
|
||||
signingConfig = signingConfigs.getByName("prerelease")
|
||||
versionNameSuffix = "-PRE"
|
||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||
}
|
||||
}
|
||||
//toolchain {
|
||||
// languageVersion.set(JavaLanguageVersion.of(17))
|
||||
// }
|
||||
// jvmToolchain(17)
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = JavaVersion.toVersion(javaTarget.target)
|
||||
targetCompatibility = JavaVersion.toVersion(javaTarget.target)
|
||||
}
|
||||
|
||||
java {
|
||||
// Use Java 17 toolchain even if a higher JDK runs the build.
|
||||
// We still use Java 8 for now which higher JDKs have deprecated.
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(libs.versions.jdkToolchain.get()))
|
||||
}
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjvm-default=compatibility")
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = 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"
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://jitpack.io")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Testing
|
||||
testImplementation(libs.junit)
|
||||
testImplementation(libs.json)
|
||||
androidTestImplementation(libs.core)
|
||||
androidTestImplementation(libs.classgraph)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.instancio.core)
|
||||
androidTestImplementation(libs.junit.ktx)
|
||||
androidTestImplementation(libs.kotlin.test)
|
||||
implementation("com.google.android.mediahome:video:1.0.0")
|
||||
implementation("androidx.test.ext:junit-ktx:1.1.5")
|
||||
testImplementation("org.json:json:20180813")
|
||||
|
||||
// Android Core & Lifecycle
|
||||
implementation(libs.core.ktx)
|
||||
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
|
||||
implementation("androidx.core:core-ktx:1.10.1")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0
|
||||
|
||||
// Design & UI
|
||||
implementation(libs.preference.ktx)
|
||||
implementation(libs.material)
|
||||
implementation(libs.constraintlayout)
|
||||
// dont change this to 1.6.0 it looks ugly af
|
||||
implementation("com.google.android.material:material:1.5.0")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.6.0")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.6.0")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation("androidx.test:core")
|
||||
|
||||
// Coil Image Loading
|
||||
implementation(libs.bundles.coil)
|
||||
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
||||
// implementation("org.jsoup:jsoup:1.13.1")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||
|
||||
// Media 3 (ExoPlayer)
|
||||
implementation(libs.bundles.media3)
|
||||
implementation(libs.video)
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
|
||||
// FFmpeg Decoding
|
||||
implementation(libs.bundles.nextlib)
|
||||
implementation("com.github.bumptech.glide:glide:4.13.1")
|
||||
kapt("com.github.bumptech.glide:compiler:4.13.1")
|
||||
implementation("com.github.bumptech.glide:okhttp3-integration:4.13.0")
|
||||
|
||||
// Anime-db for filler
|
||||
implementation(libs.anime.db)
|
||||
implementation("jp.wasabeef:glide-transformations:4.3.0")
|
||||
|
||||
// PlayBack
|
||||
implementation(libs.colorpicker) // Subtitle Color Picker
|
||||
implementation(libs.newpipeextractor) // For Trailers
|
||||
implementation(libs.juniversalchardet) // Subtitle Decoding
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
// UI Stuff
|
||||
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
|
||||
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
||||
|
||||
// Extensions & Other Libs
|
||||
implementation(libs.jsoup) // HTML Parser
|
||||
implementation(libs.rhino) // Run JavaScript
|
||||
implementation(libs.safefile) // To Prevent the URI File Fu*kery
|
||||
coreLibraryDesugaring(libs.desugar.jdk.libs.nio) // NIO Flavor Needed for NewPipeExtractor
|
||||
implementation(libs.conscrypt.android) // To Fix SSL Fu*kery on Android 9
|
||||
implementation(libs.jackson.module.kotlin) // JSON Parser
|
||||
implementation(libs.zipline)
|
||||
// Media 3
|
||||
implementation("androidx.media3:media3-common:1.1.1")
|
||||
implementation("androidx.media3:media3-exoplayer:1.1.1")
|
||||
implementation("androidx.media3:media3-datasource-okhttp:1.1.1")
|
||||
implementation("androidx.media3:media3-ui:1.1.1")
|
||||
implementation("androidx.media3:media3-session:1.1.1")
|
||||
implementation("androidx.media3:media3-cast:1.1.1")
|
||||
implementation("androidx.media3:media3-exoplayer-hls:1.1.1")
|
||||
implementation("androidx.media3:media3-exoplayer-dash:1.1.1")
|
||||
// Custom ffmpeg extension for audio codecs
|
||||
implementation("com.github.recloudstream:media-ffmpeg:1.1.0")
|
||||
|
||||
// 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.11.0")
|
||||
implementation("ch.acra:acra-toast:5.11.0")
|
||||
|
||||
compileOnly("com.google.auto.service:auto-service-annotations:1.0")
|
||||
//either for java sources:
|
||||
annotationProcessor("com.google.auto.service:auto-service:1.0")
|
||||
//or for kotlin sources (requires kapt gradle plugin):
|
||||
kapt("com.google.auto.service:auto-service:1.0")
|
||||
|
||||
// subtitle color picker
|
||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||
|
||||
//run JS
|
||||
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
|
||||
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
|
||||
implementation("org.mozilla:rhino:1.7.13")
|
||||
|
||||
// TorrentStream
|
||||
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
||||
|
||||
// Downloading
|
||||
implementation("androidx.work:work-runtime:2.8.1")
|
||||
implementation("androidx.work:work-runtime-ktx:2.8.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.3")
|
||||
// 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.LagradOst:SafeFile:0.0.5")
|
||||
|
||||
// API because cba maintaining it myself
|
||||
implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0")
|
||||
|
||||
implementation("com.github.discord:OverlappingPanels:0.1.5")
|
||||
// debugImplementation because LeakCanary should only run in debug builds.
|
||||
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
|
||||
// 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")
|
||||
|
||||
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev
|
||||
// this should be updated frequently to avoid trailer fu*kery
|
||||
implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||
|
||||
// Library/extensions searching with Levenshtein distance
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
|
||||
// Torrent Support
|
||||
implementation(libs.torrentserver)
|
||||
// color palette for images -> colors
|
||||
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||
|
||||
// Downloading & Networking
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.nicehttp) // HTTP Lib
|
||||
|
||||
implementation(project(":library"))
|
||||
implementation("com.github.recloudstream:Aria2cStream:0.0.3")
|
||||
}
|
||||
|
||||
tasks.register<Jar>("androidSourcesJar") {
|
||||
tasks.register("androidSourcesJar", Jar::class) {
|
||||
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") {
|
||||
dependsOn("build", ":library:jvmJar")
|
||||
from(
|
||||
"build/intermediates/compile_app_classes_jar/prereleaseDebug/bundlePrereleaseDebugClassesToCompileJar",
|
||||
"../library/build/libs"
|
||||
)
|
||||
into("build/app-classes")
|
||||
include("classes.jar", "library-jvm*.jar")
|
||||
// Remove the version
|
||||
rename("library-jvm.*.jar", "library-jvm.jar")
|
||||
// this is used by the gradlew plugin
|
||||
tasks.register("makeJar", Copy::class) {
|
||||
from("build/intermediates/compile_app_classes_jar/prereleaseDebug")
|
||||
into("build")
|
||||
include("classes.jar")
|
||||
dependsOn("build")
|
||||
}
|
||||
|
||||
// Merge the app classes and the library classes into classes.jar
|
||||
tasks.register<Jar>("makeJar") {
|
||||
// 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"
|
||||
tasks.withType<DokkaTask>().configureEach {
|
||||
moduleName.set("Cloudstream")
|
||||
dokkaSourceSets {
|
||||
configureEach {
|
||||
suppress = name != "prereleaseDebug"
|
||||
analysisPlatform = KotlinPlatform.JVM
|
||||
displayName = "JVM"
|
||||
documentedVisibilities(
|
||||
VisibilityModifier.Public,
|
||||
VisibilityModifier.Protected
|
||||
)
|
||||
|
||||
named("main") {
|
||||
sourceLink {
|
||||
localDirectory = file("..")
|
||||
remoteUrl("https://github.com/recloudstream/cloudstream/tree/master")
|
||||
remoteLineSuffix = "#L"
|
||||
// Unix based directory relative path to the root of the project (where you execute gradle respectively).
|
||||
localDirectory.set(file("src/main/java"))
|
||||
|
||||
// URL showing where the source code can be accessed through the web browser
|
||||
remoteUrl.set(URL("https://github.com/recloudstream/cloudstream/tree/master/app/src/main/java"))
|
||||
// Suffix which is used to append the line number to the URL. Use #L for GitHub
|
||||
remoteLineSuffix.set("#L")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
app/lint.xml
13
app/lint.xml
|
|
@ -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>
|
||||
|
|
@ -7,11 +7,8 @@ import android.view.LayoutInflater
|
|||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
|
||||
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
|
||||
|
|
@ -20,7 +17,6 @@ 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
|
||||
|
|
@ -89,8 +85,6 @@ class ExampleInstrumentedTest {
|
|||
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
||||
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv)
|
||||
|
||||
testAllLayouts<BottomResultviewPreviewBinding>(activity, R.layout.bottom_resultview_preview,R.layout.bottom_resultview_preview_tv)
|
||||
|
||||
testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
|
||||
testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
|
||||
|
||||
|
|
@ -123,12 +117,9 @@ class ExampleInstrumentedTest {
|
|||
// 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<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -136,14 +127,14 @@ class ExampleInstrumentedTest {
|
|||
@Test
|
||||
@Throws(AssertionError::class)
|
||||
fun providerCorrectData() {
|
||||
val langTagsIETF = SubtitleHelper.languages.map { it.IETF_tag }
|
||||
Assert.assertFalse("IETFTagNames does not contain any languages", langTagsIETF.isNullOrEmpty())
|
||||
val isoNames = SubtitleHelper.languages.map { it.ISO_639_1 }
|
||||
Assert.assertFalse("ISO does not contain any languages", isoNames.isNullOrEmpty())
|
||||
for (api in getAllProviders()) {
|
||||
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 ${api.name} does not contain a valid language code",
|
||||
langTagsIETF.contains(api.lang)
|
||||
isoNames.contains(api.lang)
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} does not contain any supported types",
|
||||
|
|
@ -157,7 +148,7 @@ class ExampleInstrumentedTest {
|
|||
fun providerCorrectHomepage() {
|
||||
runBlocking {
|
||||
getAllProviders().toList().amap { api ->
|
||||
TestingUtils.testHomepage(api, TestingUtils.Logger())
|
||||
TestingUtils.testHomepage(api, ::println)
|
||||
}
|
||||
}
|
||||
println("Done providerCorrectHomepage")
|
||||
|
|
@ -169,6 +160,7 @@ class ExampleInstrumentedTest {
|
|||
TestingUtils.getDeferredProviderTests(
|
||||
this,
|
||||
getAllProviders(),
|
||||
::println
|
||||
) { _, _ -> }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.toJson
|
||||
import dalvik.system.DexFile
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.InternalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.serializer
|
||||
import kotlinx.serialization.serializerOrNull
|
||||
import org.instancio.Instancio
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class SerializationClassTester {
|
||||
// Same as app, or using app reference
|
||||
val jacksonMapper = mapper
|
||||
val kotlinxMapper = json
|
||||
|
||||
@Test
|
||||
fun isIdenticalSerialization() {
|
||||
val serializableClasses = findSerializableClasses("com.lagradost")
|
||||
println("Number of serializable classes: ${serializableClasses.size}")
|
||||
|
||||
serializableClasses.forEach { kClass ->
|
||||
val instance = Instancio.create(kClass.java)
|
||||
|
||||
val jacksonJson = jacksonMapper.writeValueAsString(instance)
|
||||
val kotlinxJson = serializeWithKotlinx(kClass, instance)
|
||||
|
||||
assertEquals(
|
||||
jacksonJson,
|
||||
kotlinxJson,
|
||||
"""
|
||||
Serialization mismatch for:
|
||||
${kClass.qualifiedName}
|
||||
|
||||
Jackson:
|
||||
$jacksonJson
|
||||
|
||||
Kotlinx:
|
||||
$kotlinxJson
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
println("Identical serialization for: ${kClass.jvmName}")
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
|
||||
@Test
|
||||
fun isIdenticalDeserialization() {
|
||||
val serializableClasses = findSerializableClasses("com.lagradost")
|
||||
println("Number of serializable classes: ${serializableClasses.size}")
|
||||
|
||||
serializableClasses.forEach { kClass ->
|
||||
val instance = Instancio.create(kClass.java)
|
||||
// Convert to JSON to get example JSON object
|
||||
// We prefer jackson here because the app may have many jackson JSON strings in local storage
|
||||
val originalJson = jacksonMapper.writeValueAsString(instance)
|
||||
|
||||
// Create an object from the JSON using kotlinx
|
||||
val serializer =
|
||||
kClass.serializerOrNull() ?: kotlinxMapper.serializersModule.getContextual(kClass)
|
||||
assertNotNull(serializer, "The class: ${kClass.jvmName} must be serializable!")
|
||||
val kotlinxDecoded = kotlinxMapper.decodeFromString(serializer, originalJson)
|
||||
|
||||
// Create an object from the JSON using jackson
|
||||
val mapperDecoded = jacksonMapper.readValue(originalJson, kClass.java)
|
||||
|
||||
|
||||
// Deep inspect both object using the mapper toJson function.
|
||||
// This deep equality check can be performed using other methods, but this just works.
|
||||
val jacksonJson = mapperDecoded.toJson()
|
||||
val kotlinxJson = kotlinxDecoded.toJson()
|
||||
|
||||
assertEquals(
|
||||
jacksonJson,
|
||||
kotlinxJson,
|
||||
"""
|
||||
Serialization mismatch for:
|
||||
${kClass.qualifiedName}
|
||||
|
||||
Jackson:
|
||||
$jacksonJson
|
||||
|
||||
Kotlinx:
|
||||
$kotlinxJson
|
||||
|
||||
""".trimIndent()
|
||||
)
|
||||
println("Identical deserialization for: ${kClass.jvmName}")
|
||||
}
|
||||
}
|
||||
|
||||
// DEX files are the best solution to read all our classes dynamically.
|
||||
// ClassGraph() can be used instead, but it only gives results on the JVM, not Android.
|
||||
@Suppress("DEPRECATION")
|
||||
private fun findSerializableClasses(packageName: String): List<KClass<*>> {
|
||||
val context = InstrumentationRegistry
|
||||
.getInstrumentation()
|
||||
.targetContext
|
||||
|
||||
val dexFile = DexFile(context.packageCodePath)
|
||||
|
||||
return dexFile.entries()
|
||||
.toList()
|
||||
.filter { it.startsWith(packageName) }
|
||||
.mapNotNull {
|
||||
runCatching { Class.forName(it).kotlin }.getOrNull()
|
||||
}.filter { kClass ->
|
||||
// Not possible to use .hasAnnotation() on newer Android versions.
|
||||
kClass.java.annotations.any {
|
||||
it is Serializable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(InternalSerializationApi::class)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun serializeWithKotlinx(
|
||||
kClass: KClass<*>,
|
||||
value: Any
|
||||
): String {
|
||||
val serializer = kClass.serializer() as KSerializer<Any>
|
||||
return kotlinxMapper.encodeToString(serializer, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +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.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KeepGeneratedSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = NonEmptyData.Serializer::class)
|
||||
data class NonEmptyData(
|
||||
val title: String = "",
|
||||
val tags: List<String> = emptyList(),
|
||||
val meta: Map<String, String> = emptyMap(),
|
||||
val name: String = "hello",
|
||||
) {
|
||||
object Serializer : NonEmptySerializer<NonEmptyData>(NonEmptyData.generatedSerializer())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = WriteOnlyData.Serializer::class)
|
||||
data class WriteOnlyData(
|
||||
val fieldA: String = "",
|
||||
val fieldB: String = "",
|
||||
) {
|
||||
object Serializer : WriteOnlySerializer<WriteOnlyData>(
|
||||
WriteOnlyData.generatedSerializer(),
|
||||
setOf("fieldB"),
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@KeepGeneratedSerializer
|
||||
@Serializable(with = MultiWriteOnly.Serializer::class)
|
||||
data class MultiWriteOnly(
|
||||
val fieldA: String = "",
|
||||
val fieldB: String = "",
|
||||
val fieldC: String = "",
|
||||
) {
|
||||
object Serializer : WriteOnlySerializer<MultiWriteOnly>(
|
||||
MultiWriteOnly.generatedSerializer(),
|
||||
setOf("fieldB", "fieldC"),
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class UriData(
|
||||
@Serializable(with = UriSerializer::class)
|
||||
val uri: Uri = Uri.EMPTY,
|
||||
)
|
||||
|
||||
class SerializerTest {
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyStrings() {
|
||||
val data = NonEmptyData(title = "", name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("title"))
|
||||
assertTrue(result.contains("name"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyLists() {
|
||||
val data = NonEmptyData(tags = emptyList(), name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("tags"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerOmitsEmptyMaps() {
|
||||
val data = NonEmptyData(meta = emptyMap(), name = "hello")
|
||||
val result = data.toJson()
|
||||
assertFalse(result.contains("meta"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerKeepsNonEmptyFields() {
|
||||
val data = NonEmptyData(title = "hello", tags = listOf("a"), meta = mapOf("k" to "v"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("title"))
|
||||
assertTrue(result.contains("tags"))
|
||||
assertTrue(result.contains("meta"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonEmptySerializerDoesNotAffectDeserialization() {
|
||||
val input = """{"title":"hello","tags":["a"],"meta":{"k":"v"},"name":"world"}"""
|
||||
val result = parseJson<NonEmptyData>(input)
|
||||
assertEquals("hello", result.title)
|
||||
assertEquals(listOf("a"), result.tags)
|
||||
assertEquals(mapOf("k" to "v"), result.meta)
|
||||
assertEquals("world", result.name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerOmitsFieldOnSerialize() {
|
||||
val data = WriteOnlyData(fieldA = "hello", fieldB = "secret")
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("fieldA"))
|
||||
assertFalse(result.contains("fieldB"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerDeserializesNormally() {
|
||||
val input = """{"fieldA":"hello","fieldB":"secret"}"""
|
||||
val result = parseJson<WriteOnlyData>(input)
|
||||
assertEquals("hello", result.fieldA)
|
||||
assertEquals("secret", result.fieldB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerDeserializesMissingAsDefault() {
|
||||
val input = """{"fieldA":"hello"}"""
|
||||
val result = parseJson<WriteOnlyData>(input)
|
||||
assertEquals("hello", result.fieldA)
|
||||
assertEquals("", result.fieldB)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeOnlySerializerHandlesMultipleKeys() {
|
||||
val data = MultiWriteOnly(fieldA = "hello", fieldB = "secret1", fieldC = "secret2")
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("fieldA"))
|
||||
assertFalse(result.contains("fieldB"))
|
||||
assertFalse(result.contains("fieldC"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerSerializesUriToString() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val result = data.toJson()
|
||||
assertTrue(result.contains("https://example.com/path?query=1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerDeserializesStringToUri() {
|
||||
val input = """{"uri":"https://example.com/path?query=1"}"""
|
||||
val result = parseJson<UriData>(input)
|
||||
assertEquals(Uri.parse("https://example.com/path?query=1"), result.uri)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun uriSerializerRoundtripsCorrectly() {
|
||||
val data = UriData(uri = Uri.parse("https://example.com/path?query=1"))
|
||||
val encoded = data.toJson()
|
||||
val decoded = parseJson<UriData>(encoded)
|
||||
assertEquals(data.uri, decoded.uri)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,8 +25,9 @@
|
|||
android:endY="245.72"
|
||||
android:endX="292.58"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#3FAA11"/>
|
||||
<item android:offset="1" android:color="#39A11D"/>
|
||||
<item android:offset="0" android:color="#FF5D49EA"/>
|
||||
<item android:offset="0.45" android:color="#FF452FE4"/>
|
||||
<item android:offset="1" android:color="#FF2309DB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
@ -39,8 +40,9 @@
|
|||
android:endY="245.72"
|
||||
android:endX="248.76"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#37DB25"/>
|
||||
<item android:offset="1" android:color="#11DD6D"/>
|
||||
<item android:offset="0" android:color="#FF4F6DFB"/>
|
||||
<item android:offset="0.6" android:color="#FF3559E7"/>
|
||||
<item android:offset="1" android:color="#FF2149D8"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
@ -53,45 +55,46 @@
|
|||
android:endY="245.69"
|
||||
android:endX="210.03"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#40F15D"/>
|
||||
<item android:offset="1" android:color="#42C54F"/>
|
||||
<item android:offset="0" android:color="#FF56B6FE"/>
|
||||
<item android:offset="0.61" android:color="#FF599CFA"/>
|
||||
<item android:offset="1" android:color="#FF5C89F7"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</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:fillColor="#39A11D"/>
|
||||
android:fillColor="#2e24ff"/>
|
||||
<path
|
||||
android:pathData="M397.78,222.69v60.93H390V222.69Z"
|
||||
android:fillColor="#39A11D"/>
|
||||
android:fillColor="#2e24ff"/>
|
||||
<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:fillColor="#39A11D"/>
|
||||
android:fillColor="#2e24ff"/>
|
||||
<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:fillColor="#39A11D"/>
|
||||
android:fillColor="#2e24ff"/>
|
||||
<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:fillColor="#39A11D"/>
|
||||
android:fillColor="#2e24ff"/>
|
||||
<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:fillColor="#68C671"/>
|
||||
android:fillColor="#5252ff"/>
|
||||
<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:fillColor="#68C671"/>
|
||||
android:fillColor="#5252ff"/>
|
||||
<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:fillColor="#68C671"/>
|
||||
android:fillColor="#5252ff"/>
|
||||
<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:fillColor="#68C671"/>
|
||||
android:fillColor="#5252ff"/>
|
||||
<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:fillColor="#68C671"/>
|
||||
android:fillColor="#5252ff"/>
|
||||
<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:fillColor="#68C671"/>
|
||||
android:fillColor="#5252ff"/>
|
||||
<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">
|
||||
<aapt:attr name="android:fillColor">
|
||||
|
|
@ -101,9 +104,9 @@
|
|||
android:endY="252.3"
|
||||
android:endX="373.57"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#68C671"/>
|
||||
<item android:offset="0.45" android:color="#11DD6D"/>
|
||||
<item android:offset="1" android:color="#39A11D"/>
|
||||
<item android:offset="0" android:color="#FF5D49EA"/>
|
||||
<item android:offset="0.45" android:color="#FF452FE4"/>
|
||||
<item android:offset="1" android:color="#FF2309DB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
@ -114,9 +117,9 @@
|
|||
android:startX="400.11"
|
||||
android:endX="900"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#68C671"/>
|
||||
<item android:offset="0.45" android:color="#11DD6D"/>
|
||||
<item android:offset="1" android:color="#39A11D"/>
|
||||
<item android:offset="0" android:color="#FF5D49EA"/>
|
||||
<item android:offset="0.45" android:color="#FF452FE4"/>
|
||||
<item android:offset="1" android:color="#FF2309DB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
@ -129,9 +132,9 @@
|
|||
android:endY="252.3"
|
||||
android:endX="373.57"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#68C671"/>
|
||||
<item android:offset="0.45" android:color="#11DD6D"/>
|
||||
<item android:offset="1" android:color="#39A11D"/>
|
||||
<item android:offset="0" android:color="#FF5D49EA"/>
|
||||
<item android:offset="0.45" android:color="#FF452FE4"/>
|
||||
<item android:offset="1" android:color="#FF2309DB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
@ -142,9 +145,9 @@
|
|||
android:startX="700.11"
|
||||
android:endX="900.57"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#68C671"/>
|
||||
<item android:offset="0.45" android:color="#11DD6D"/>
|
||||
<item android:offset="1" android:color="#39A11D"/>
|
||||
<item android:offset="0" android:color="#FF5D49EA"/>
|
||||
<item android:offset="0.45" android:color="#FF452FE4"/>
|
||||
<item android:offset="1" android:color="#FF2309DB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
@ -155,9 +158,9 @@
|
|||
android:startX="400.11"
|
||||
android:endX="800.57"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#68C671"/>
|
||||
<item android:offset="0.45" android:color="#11DD6D"/>
|
||||
<item android:offset="1" android:color="#39A11D"/>
|
||||
<item android:offset="0" android:color="#FF5D49EA"/>
|
||||
<item android:offset="0.45" android:color="#FF452FE4"/>
|
||||
<item android:offset="1" android:color="#FF2309DB"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
|
|
|
|||
|
|
@ -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.READ_EXTERNAL_STORAGE" /> <!-- Downloads -->
|
||||
<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.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="com.android.providers.tv.permission.WRITE_EPG_DATA" /> <!-- Used for Android TV watch next -->
|
||||
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- Used for updates without prompt -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- Used for update service -->
|
||||
<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>
|
||||
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Required for getting arbitrary Aniyomi packages -->
|
||||
<!-- Fixes android tv fuckery -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
|
|
@ -71,24 +24,28 @@
|
|||
android:name="android.software.leanback"
|
||||
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. -->
|
||||
<!--TODO https://stackoverflow.com/questions/41799732/chromecast-button-not-visible-in-android-->
|
||||
<application
|
||||
android:name=".CloudStreamApp"
|
||||
android:name=".AcraApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="video"
|
||||
android:banner="@mipmap/ic_banner"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:pageSizeCompat="enabled"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="${target_sdk_version}">
|
||||
tools:targetApi="o">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
|
|
@ -104,10 +61,7 @@
|
|||
android:exported="true"
|
||||
android:resizeableActivity="true"
|
||||
android:screenOrientation="userLandscape"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer"
|
||||
android:launchMode="singleTask"
|
||||
tools:ignore="DiscouragedApi">
|
||||
android:supportsPictureInPicture="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
@ -125,55 +79,25 @@
|
|||
|
||||
<data android:mimeType="*/*" />
|
||||
</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>
|
||||
<!--
|
||||
android:launchMode="singleTask"
|
||||
is a bit experimental, it makes loading repositories from browser still stay on the same page
|
||||
no idea about side effects
|
||||
|
||||
Not exported to prevent bypassing the AccountSelectActivity
|
||||
-->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode"
|
||||
android:exported="false"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.account.AccountSelectActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
|
||||
android:exported="true">
|
||||
android:supportsPictureInPicture="true">
|
||||
<intent-filter android:exported="true">
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- cloudstreamplayer://encodedUrl?name=Dune -->
|
||||
<intent-filter>
|
||||
|
|
@ -200,14 +124,7 @@
|
|||
|
||||
<data android:scheme="cloudstreamrepo" />
|
||||
</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 -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
@ -231,7 +148,7 @@
|
|||
<data android:scheme="cloudstreamcontinuewatching" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
@ -244,11 +161,15 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.EasterEggMonke"
|
||||
android:exported="true" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.VideoDownloadRestartReceiver"
|
||||
android:enabled="false"
|
||||
android:exported="false">
|
||||
<intent-filter android:exported="false">
|
||||
android:exported="true">
|
||||
<intent-filter android:exported="true">
|
||||
<action android:name="restart_service" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
|
@ -256,28 +177,14 @@
|
|||
<service
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:foregroundServiceType="dataSync"
|
||||
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
|
||||
android:name=".ui.ControllerActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".services.PackageInstallerService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:name=".utils.PackageInstallerService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
|
|
@ -291,5 +198,5 @@
|
|||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
||||
28
app/src/main/cpp/native-lib.cpp
Normal file
28
app/src/main/cpp/native-lib.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include <jni.h>
|
||||
#include <csignal>
|
||||
#include <android/log.h>
|
||||
|
||||
#define TAG "CloudStream Crash Handler"
|
||||
volatile sig_atomic_t gSignalStatus = 0;
|
||||
void handleNativeCrash(int signal) {
|
||||
gSignalStatus = signal;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_lagradost_cloudstream3_NativeCrashHandler_initNativeCrashHandler(JNIEnv *env, jobject) {
|
||||
#define REGISTER_SIGNAL(X) signal(X, handleNativeCrash);
|
||||
REGISTER_SIGNAL(SIGSEGV)
|
||||
#undef REGISTER_SIGNAL
|
||||
}
|
||||
|
||||
//extern "C" JNIEXPORT void JNICALL
|
||||
//Java_com_lagradost_cloudstream3_NativeCrashHandler_triggerNativeCrash(JNIEnv *env, jobject thiz) {
|
||||
// int *p = nullptr;
|
||||
// *p = 0;
|
||||
//}
|
||||
|
||||
extern "C" JNIEXPORT int JNICALL
|
||||
Java_com_lagradost_cloudstream3_NativeCrashHandler_getSignalStatus(JNIEnv *env, jobject) {
|
||||
//__android_log_print(ANDROID_LOG_INFO, TAG, "Got signal status %d", gSignalStatus);
|
||||
return gSignalStatus;
|
||||
}
|
||||
|
|
@ -1,78 +1,224 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
/**
|
||||
* Deprecated alias for CloudStreamApp for backwards compatibility with plugins.
|
||||
* Use CloudStreamApp instead.
|
||||
*/
|
||||
@Deprecated(
|
||||
message = "AcraApplication is deprecated, use CloudStreamApp instead",
|
||||
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp"),
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
class AcraApplication {
|
||||
companion object {
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.auto.service.AutoService
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
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.WARNING
|
||||
)
|
||||
val context get() = CloudStreamApp.context
|
||||
|
||||
@Deprecated(
|
||||
message = "AcraApplication is deprecated, use CloudStreamApp instead",
|
||||
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.removeKeys(folder)"),
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
fun removeKeys(folder: String): Int? =
|
||||
CloudStreamApp.removeKeys(folder)
|
||||
class CustomReportSender : ReportSender {
|
||||
// Sends all your crashes to google forms
|
||||
override fun send(context: Context, errorContent: CrashReportData) {
|
||||
println("Sending report")
|
||||
//Log.i("Acra", "Sending report: ${errorContent.toMap().map { "${it.key}:${it.value}" }.joinToString()}")
|
||||
val url =
|
||||
"https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse"
|
||||
val data = mapOf(
|
||||
"entry.1993829403" to errorContent.toJSON()
|
||||
)
|
||||
|
||||
@Deprecated(
|
||||
message = "AcraApplication is deprecated, use CloudStreamApp instead",
|
||||
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(path, value)"),
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
fun <T> setKey(path: String, value: T) =
|
||||
CloudStreamApp.setKey(path, value)
|
||||
thread { // to not run it on main thread
|
||||
runBlocking {
|
||||
suspendSafeApiCall {
|
||||
app.post(url, data = data)
|
||||
//println("Report response: $post")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated(
|
||||
message = "AcraApplication is deprecated, use CloudStreamApp instead",
|
||||
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.setKey(folder, path, value)"),
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
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.WARNING
|
||||
)
|
||||
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.WARNING
|
||||
)
|
||||
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.WARNING
|
||||
)
|
||||
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.WARNING
|
||||
)
|
||||
inline fun <reified T : Any> getKey(folder: String, path: String, defVal: T?): T? =
|
||||
CloudStreamApp.getKey(folder, path, defVal)
|
||||
}
|
||||
runOnMainThread { // to run it on main looper
|
||||
normalSafeApiCall {
|
||||
Toast.makeText(context, R.string.acra_report_toast, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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()
|
||||
//NativeCrashHandler.initCrashHandler()
|
||||
ExceptionHandler(filesDir.resolve("last_error")) {
|
||||
val intent = context!!.packageManager.getLaunchIntentForPackage(context!!.packageName)
|
||||
startActivity(Intent.makeRestartActivityTask(intent!!.component))
|
||||
}.also {
|
||||
exceptionHandler = it
|
||||
Thread.setDefaultUncaughtExceptionHandler(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
context = base
|
||||
|
||||
initAcra {
|
||||
//core configuration:
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
|
||||
reportContent = listOf(
|
||||
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
||||
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
||||
ReportField.STACK_TRACE,
|
||||
)
|
||||
|
||||
// removed this due to bug when starting the app, moved it to when it actually crashes
|
||||
//each plugin you chose above can be configured in a block like this:
|
||||
/*toast {
|
||||
text = getString(R.string.acra_report_toast)
|
||||
//opening this block automatically enables the plugin.
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
var exceptionHandler: ExceptionHandler? = null
|
||||
|
||||
/** 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 <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 fallback to webview if in TV layout */
|
||||
fun openBrowser(url: String, activity: FragmentActivity?) {
|
||||
openBrowser(
|
||||
url,
|
||||
isTvSettings(),
|
||||
activity?.supportFragmentManager?.fragments?.lastOrNull()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +1,21 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.Manifest
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.View.NO_ID
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -27,41 +25,29 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isNotEmpty
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
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.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager
|
||||
import com.lagradost.cloudstream3.ui.home.HomeChildItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.home.ParentItemAdapter
|
||||
import com.lagradost.cloudstream3.ui.player.PlayerPipHelper.isPIPPossible
|
||||
import com.lagradost.cloudstream3.ui.player.Torrent
|
||||
import com.lagradost.cloudstream3.ui.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.ui.player.PlayerEventType
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
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.UiText
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
|
||||
enum class FocusDirection {
|
||||
Start,
|
||||
|
|
@ -79,11 +65,6 @@ object CommonActivity {
|
|||
_activity = WeakReference(value)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setActivityInstance(newActivity: Activity?) {
|
||||
activity = newActivity
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun Activity?.getCastSession(): CastSession? {
|
||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||
|
|
@ -101,26 +82,20 @@ object CommonActivity {
|
|||
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 canEnterPipMode: Boolean = false
|
||||
var canShowPipMode: Boolean = false
|
||||
var isInPIPMode: Boolean = false
|
||||
|
||||
val onColorSelectedEvent = Event<Pair<Int, Int>>()
|
||||
val onDialogDismissedEvent = Event<Int>()
|
||||
|
||||
var playerEventListener: ((PlayerEventType) -> Unit)? = null
|
||||
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
|
||||
var appliedTheme: Int = 0
|
||||
var appliedColor: Int = 0
|
||||
|
||||
private var currentToast: Toast? = null
|
||||
|
||||
var currentToast: Toast? = null
|
||||
|
||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
|
|
@ -176,50 +151,42 @@ object CommonActivity {
|
|||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
try {
|
||||
val binding = ToastBinding.inflate(act.layoutInflater)
|
||||
binding.text.text = message.trim()
|
||||
val inflater =
|
||||
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)
|
||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
|
||||
@Suppress("DEPRECATION")
|
||||
toast.view =
|
||||
binding.root // FIXME Find an alternative using default Toasts since custom toasts are deprecated and won't appear with api30 set as minSDK version.
|
||||
currentToast = toast
|
||||
toast.duration = duration ?: Toast.LENGTH_SHORT
|
||||
toast.view = layout
|
||||
//https://github.com/PureWriter/ToastCompat
|
||||
toast.show()
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val ref = WeakReference(toast)
|
||||
|
||||
/* Clean up activity leak */
|
||||
handler.postDelayed({
|
||||
if (ref.get() == currentToast) {
|
||||
currentToast = null
|
||||
}
|
||||
}, 10_000)
|
||||
|
||||
currentToast = toast
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set locale
|
||||
* @param languageTag shall a IETF BCP 47 conformant tag.
|
||||
* Check [com.lagradost.cloudstream3.utils.SubtitleHelper].
|
||||
*
|
||||
* See locales on:
|
||||
* https://github.com/unicode-org/cldr-json/blob/main/cldr-json/cldr-core/availableLocales.json
|
||||
* https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/android-16.0.0_r2/core/res/res/values/locale_config.xml
|
||||
* https://iso639-3.sil.org/code_tables/639/data/all
|
||||
*/
|
||||
fun setLocale(context: Context?, languageTag: String?) {
|
||||
if (context == null || languageTag == null) return
|
||||
val locale = Locale.forLanguageTag(languageTag)
|
||||
* Not all languages can be fetched from locale with a code.
|
||||
* This map allows sidestepping the default Locale(languageCode)
|
||||
* when setting the app language.
|
||||
**/
|
||||
val appLanguageExceptions = hashMapOf(
|
||||
"zh-rTW" to Locale.TRADITIONAL_CHINESE
|
||||
)
|
||||
|
||||
fun setLocale(context: Context?, languageCode: String?) {
|
||||
if (context == null || languageCode == null) return
|
||||
val locale = appLanguageExceptions[languageCode] ?: Locale(languageCode)
|
||||
val resources: Resources = context.resources
|
||||
val config = resources.configuration
|
||||
Locale.setDefault(locale)
|
||||
|
|
@ -227,12 +194,7 @@ object CommonActivity {
|
|||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
context.createConfigurationContext(config)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
resources.updateConfiguration(
|
||||
config,
|
||||
resources.displayMetrics
|
||||
) // FIXME this should be replaced
|
||||
resources.updateConfiguration(config, resources.displayMetrics)
|
||||
}
|
||||
|
||||
fun Context.updateLocale() {
|
||||
|
|
@ -241,38 +203,44 @@ object CommonActivity {
|
|||
setLocale(this, localeCode)
|
||||
}
|
||||
|
||||
fun init(act: Activity) {
|
||||
setActivityInstance(act)
|
||||
ioSafe { Torrent.deleteAllFiles() }
|
||||
val componentActivity = activity as? ComponentActivity ?: return
|
||||
fun init(act: ComponentActivity?) {
|
||||
if (act == null) return
|
||||
activity = act
|
||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||
canShowPipMode =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
||||
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()
|
||||
componentActivity.updateTv()
|
||||
AccountManager.initMainAPI()
|
||||
act.updateLocale()
|
||||
act.updateTv()
|
||||
NewPipe.init(DownloaderTestImpl.getInstance())
|
||||
|
||||
MainActivity.activityResultLauncher =
|
||||
componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == AppCompatActivity.RESULT_OK) {
|
||||
val actionUid =
|
||||
getKey<String>("last_click_action") ?: return@registerForActivityResult
|
||||
Log.d(TAG, "Loading action $actionUid result handler")
|
||||
val action = VideoClickActionHolder.getByUniqueId(actionUid) as? OpenInAppAction
|
||||
?: return@registerForActivityResult
|
||||
action.onResultSafe(act, result.data)
|
||||
removeKey("last_click_action")
|
||||
removeKey("last_opened")
|
||||
for (resumeApp in resumeApps) {
|
||||
resumeApp.launcher =
|
||||
act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
val resultCode = result.resultCode
|
||||
val data = result.data
|
||||
if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) {
|
||||
val pos = resumeApp.getPosition(data)
|
||||
val dur = resumeApp.getDuration(data)
|
||||
if (dur > 0L && pos > 0L)
|
||||
DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur)
|
||||
removeKey(resumeApp.lastId)
|
||||
ResultFragment.updateUI()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for notification permissions on Android 13
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
componentActivity,
|
||||
act,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
||||
val requestPermissionLauncher = act.registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
Log.d(TAG, "Notification permission: $isGranted")
|
||||
|
|
@ -283,22 +251,17 @@ object CommonActivity {
|
|||
}
|
||||
}
|
||||
|
||||
/** Enters pip mode if it is both possible and desired to do so*/
|
||||
private fun Activity.enterPIPMode() {
|
||||
if (!isPipDesired || !this.isPIPPossible()) return
|
||||
|
||||
if (!shouldShowPIPMode(canEnterPipMode) || !canShowPipMode) return
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
enterPictureInPictureMode(PictureInPictureParams.Builder().build())
|
||||
} catch (_: Exception) {
|
||||
// Use fallback just in case
|
||||
@Suppress("DEPRECATION")
|
||||
} catch (e: Exception) {
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@Suppress("DEPRECATION")
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
|
@ -307,32 +270,9 @@ object CommonActivity {
|
|||
}
|
||||
}
|
||||
|
||||
fun onUserLeaveHint(act: Activity) {
|
||||
// On Android 12 and later we use setAutoEnterEnabled() instead.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) return
|
||||
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
|
||||
fun onUserLeaveHint(act: Activity?) {
|
||||
if (canEnterPipMode && canShowPipMode) {
|
||||
act?.enterPIPMode()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,7 +282,6 @@ object CommonActivity {
|
|||
|
||||
val currentTheme =
|
||||
when (settingsManager.getString(act.getString(R.string.app_theme_key), "AmoledLight")) {
|
||||
"System" -> mapSystemTheme(act)
|
||||
"Black" -> R.style.AppTheme
|
||||
"Light" -> R.style.LightMode
|
||||
"Amoled" -> R.style.AmoledMode
|
||||
|
|
@ -350,25 +289,18 @@ object CommonActivity {
|
|||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.MonetMode else R.style.AppTheme
|
||||
|
||||
"Dracula" -> R.style.DraculaMode
|
||||
"Lavender" -> R.style.LavenderMode
|
||||
"SilentBlue" -> R.style.SilentBlueMode
|
||||
|
||||
else -> R.style.AppTheme
|
||||
}
|
||||
|
||||
val currentOverlayTheme =
|
||||
when (settingsManager.getString(act.getString(R.string.primary_color_key), "Normal")) {
|
||||
"Normal" -> R.style.OverlayPrimaryColorNormal
|
||||
"DandelionYellow" -> R.style.OverlayPrimaryColorDandelionYellow
|
||||
"CarnationPink" -> R.style.OverlayPrimaryColorCarnationPink
|
||||
"Orange" -> R.style.OverlayPrimaryColorOrange
|
||||
"DarkGreen" -> R.style.OverlayPrimaryColorDarkGreen
|
||||
"Maroon" -> R.style.OverlayPrimaryColorMaroon
|
||||
"NavyBlue" -> R.style.OverlayPrimaryColorNavyBlue
|
||||
"Grey" -> R.style.OverlayPrimaryColorGrey
|
||||
"White" -> R.style.OverlayPrimaryColorWhite
|
||||
"CoolBlue" -> R.style.OverlayPrimaryColorCoolBlue
|
||||
"Brown" -> R.style.OverlayPrimaryColorBrown
|
||||
"Purple" -> R.style.OverlayPrimaryColorPurple
|
||||
"Green" -> R.style.OverlayPrimaryColorGreen
|
||||
|
|
@ -377,7 +309,6 @@ object CommonActivity {
|
|||
"Banana" -> R.style.OverlayPrimaryColorBanana
|
||||
"Party" -> R.style.OverlayPrimaryColorParty
|
||||
"Pink" -> R.style.OverlayPrimaryColorPink
|
||||
"Lavender" -> R.style.OverlayPrimaryColorLavender
|
||||
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
|
||||
|
||||
|
|
@ -386,13 +317,9 @@ object CommonActivity {
|
|||
|
||||
else -> R.style.OverlayPrimaryColorNormal
|
||||
}
|
||||
|
||||
act.theme.applyStyle(currentTheme, 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(
|
||||
R.style.LoadedStyle,
|
||||
true
|
||||
|
|
@ -421,9 +348,10 @@ object CommonActivity {
|
|||
currentLook = currentLook.parent as? View ?: break
|
||||
}*/
|
||||
|
||||
private fun View.hasContent(): Boolean {
|
||||
return isShown && when (this) {
|
||||
is ViewGroup -> this.isNotEmpty()
|
||||
private fun View.hasContent() : Boolean {
|
||||
return isShown && when(this) {
|
||||
//is RecyclerView -> this.childCount > 0
|
||||
is ViewGroup -> this.childCount > 0
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
|
@ -453,7 +381,7 @@ object CommonActivity {
|
|||
// 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()
|
||||
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.childCount > 0
|
||||
} ?: false
|
||||
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
|
||||
|
||||
|
|
@ -531,8 +459,98 @@ object CommonActivity {
|
|||
}
|
||||
|
||||
|
||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?): Boolean? {
|
||||
return null
|
||||
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 */
|
||||
|
|
@ -569,7 +587,6 @@ object CommonActivity {
|
|||
|
||||
else -> null
|
||||
}
|
||||
|
||||
// println("NEXT FOCUS : $nextView")
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus()
|
||||
|
|
@ -577,15 +594,10 @@ object CommonActivity {
|
|||
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) &&
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
|
||||
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||
) {
|
||||
showInputMethod(act.currentFocus?.findFocus())
|
||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||
}
|
||||
|
||||
//println("Keycode: $keyCode")
|
||||
|
|
@ -594,6 +606,7 @@ object CommonActivity {
|
|||
// "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
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3
|
|||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
import org.schabi.newpipe.extractor.downloader.Request
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
|
|
@ -11,7 +10,7 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
|
||||
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 {
|
||||
val httpMethod: String = request.httpMethod()
|
||||
val url: String = request.url()
|
||||
|
|
@ -19,7 +18,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
|||
val dataToSend: ByteArray? = request.dataToSend()
|
||||
var requestBody: RequestBody? = null
|
||||
if (dataToSend != null) {
|
||||
requestBody = dataToSend.toRequestBody(null, 0, dataToSend.size)
|
||||
requestBody = RequestBody.create(null, dataToSend)
|
||||
}
|
||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
|
|
@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
|||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
1837
app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
Normal file
1837
app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
Normal file
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,53 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.lastError
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager.checkSafeModeFile
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object NativeCrashHandler {
|
||||
// external fun triggerNativeCrash()
|
||||
/*private external fun initNativeCrashHandler()
|
||||
private external fun getSignalStatus(): Int
|
||||
|
||||
private fun initSignalPolling() = CoroutineScope(Dispatchers.IO).launch {
|
||||
|
||||
//launch {
|
||||
// delay(10000)
|
||||
// triggerNativeCrash()
|
||||
//}
|
||||
|
||||
while (true) {
|
||||
delay(10_000)
|
||||
val signal = getSignalStatus()
|
||||
// Signal is initialized to zero
|
||||
if (signal == 0) continue
|
||||
|
||||
// Do not crash in safe mode!
|
||||
if (lastError != null) continue
|
||||
if (checkSafeModeFile()) continue
|
||||
|
||||
AcraApplication.exceptionHandler?.uncaughtException(
|
||||
Thread.currentThread(),
|
||||
RuntimeException("Native crash with code: $signal. Try uninstalling extensions.\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun initCrashHandler() {
|
||||
try {
|
||||
System.loadLibrary("native-lib")
|
||||
initNativeCrashHandler()
|
||||
} catch (t: Throwable) {
|
||||
// Make debug crash.
|
||||
if (BuildConfig.DEBUG) throw t
|
||||
logError(t)
|
||||
return
|
||||
}
|
||||
|
||||
initSignalPolling()
|
||||
}*/
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,205 +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
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
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.jvmName}"
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -1,162 +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.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.txt
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
data class MinimalVideoLink(
|
||||
@JsonProperty("uri")
|
||||
val uri: Uri?,
|
||||
@JsonProperty("url")
|
||||
val url: String?,
|
||||
@JsonProperty("mimeType")
|
||||
val mimeType: String = "video/mp4",
|
||||
@JsonProperty("name")
|
||||
val name: String?,
|
||||
@JsonProperty("headers")
|
||||
var headers: Map<String, String> = mapOf(),
|
||||
@JsonProperty("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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class MinimalSubtitleLink(
|
||||
@JsonProperty("url")
|
||||
val url: String,
|
||||
@JsonProperty("mimeType")
|
||||
val mimeType: String = "text/vtt",
|
||||
@JsonProperty("name")
|
||||
val name: String?,
|
||||
@JsonProperty("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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package com.lagradost.cloudstream3.actions.temp.fcast
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
data class PlayMessage(
|
||||
val container: String,
|
||||
val url: String? = null,
|
||||
val content: String? = null,
|
||||
val time: Double? = null,
|
||||
val speed: Double? = null,
|
||||
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
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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://asianhdplay.pro"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,18 +19,17 @@ open class Blogger : ExtractorApi() {
|
|||
.substringBefore("]")
|
||||
tryParseJson<List<ResponseSource>>("[$data]")?.map {
|
||||
sources.add(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
it.play_url,
|
||||
) {
|
||||
this.referer = "https://www.youtube.com/"
|
||||
this.quality = when (it.format_id) {
|
||||
referer = "https://www.youtube.com/",
|
||||
quality = when (it.format_id) {
|
||||
18 -> 360
|
||||
22 -> 720
|
||||
else -> Qualities.Unknown.value
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "*/*")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.to"
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -6,37 +6,32 @@ import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.StringUtils.decodeUrl
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
import java.net.URLDecoder
|
||||
|
||||
open class Cda : ExtractorApi() {
|
||||
open class Cda: ExtractorApi() {
|
||||
override var mainUrl = "https://ebd.cda.pl"
|
||||
override var name = "Cda"
|
||||
override val requiresReferer = false
|
||||
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val mediaId = url
|
||||
.split("/").last()
|
||||
.split("?").first()
|
||||
val doc = app.get(
|
||||
"https://ebd.cda.pl/647x500/$mediaId", headers = mapOf(
|
||||
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Cookie" to "cda.player=html5"
|
||||
)
|
||||
).document
|
||||
val doc = app.get("https://ebd.cda.pl/647x500/$mediaId", headers=mapOf(
|
||||
"Referer" to "https://ebd.cda.pl/647x500/$mediaId",
|
||||
"User-Agent" to USER_AGENT,
|
||||
"Cookie" to "cda.player=html5"
|
||||
)).document
|
||||
val dataRaw = doc.selectFirst("[player_data]")?.attr("player_data") ?: return null
|
||||
val playerData = tryParseJson<PlayerData>(dataRaw) ?: return null
|
||||
return listOf(
|
||||
newExtractorLink(
|
||||
source = name,
|
||||
name = name,
|
||||
url = getFile(playerData.video.file),
|
||||
) {
|
||||
this.referer = "https://ebd.cda.pl/647x500/$mediaId"
|
||||
this.quality = Qualities.Unknown.value
|
||||
}
|
||||
)
|
||||
return listOf(ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
getFile(playerData.video.file),
|
||||
referer = "https://ebd.cda.pl/647x500/$mediaId",
|
||||
quality = Qualities.Unknown.value
|
||||
))
|
||||
}
|
||||
|
||||
private fun rot13(a: String): String {
|
||||
|
|
@ -51,7 +46,7 @@ open class Cda : ExtractorApi() {
|
|||
|
||||
private fun cdaUggc(a: String): String {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -64,10 +59,10 @@ open class Cda : ExtractorApi() {
|
|||
.replace("_QWE", "")
|
||||
.replace("_Q5", "")
|
||||
.replace("_IKSDE", "")
|
||||
a = a.decodeUrl()
|
||||
a = URLDecoder.decode(a, "UTF-8")
|
||||
a = a.map { char ->
|
||||
if (char.code in 33..126) {
|
||||
return@map (33 + (char.code + 14) % 94).toChar().toString()
|
||||
if (32 < char.toInt() && char.toInt() < 127) {
|
||||
return@map String.format("%c", 33 + (char.toInt() + 14) % 94)
|
||||
} else {
|
||||
return@map char
|
||||
}
|
||||
|
|
@ -77,7 +72,7 @@ open class Cda : ExtractorApi() {
|
|||
.replace(".2cda.pl", ".cda.pl")
|
||||
.replace(".3cda.pl", ".cda.pl")
|
||||
return if (a.contains("/upstream")) "https://" + a.replace("/upstream", ".mp4/upstream")
|
||||
else "https://${a}.mp4"
|
||||
else "https://${a}.mp4"
|
||||
}
|
||||
|
||||
private fun getFile(a: String) = when {
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.*
|
||||
import com.lagradost.cloudstream3.extractors.helper.AesHelper.cryptoAESHandler
|
||||
import com.lagradost.cloudstream3.utils.AppUtils
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
class Moviesapi : Chillx() {
|
||||
override val name = "Moviesapi"
|
||||
override val mainUrl = "https://w1.moviesapi.club"
|
||||
}
|
||||
|
||||
class Bestx : Chillx() {
|
||||
override val name = "Bestx"
|
||||
override val mainUrl = "https://bestx.stream"
|
||||
}
|
||||
|
||||
class Watchx : Chillx() {
|
||||
override val name = "Watchx"
|
||||
override val mainUrl = "https://watchx.top"
|
||||
}
|
||||
open class Chillx : ExtractorApi() {
|
||||
override val name = "Chillx"
|
||||
override val mainUrl = "https://chillx.top"
|
||||
override val requiresReferer = true
|
||||
|
||||
companion object {
|
||||
private const val KEY = "m4H6D9%0\$N&F6rQ&"
|
||||
}
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val master = Regex("MasterJS\\s*=\\s*'([^']+)").find(
|
||||
app.get(
|
||||
url,
|
||||
referer = referer
|
||||
).text
|
||||
)?.groupValues?.get(1)
|
||||
val decrypt = cryptoAESHandler(master ?: return, KEY.toByteArray(), false)?.replace("\\", "") ?: throw ErrorLoadingException("failed to decrypt")
|
||||
|
||||
val source = Regex(""""?file"?:\s*"([^"]+)""").find(decrypt)?.groupValues?.get(1)
|
||||
val tracks = Regex("""tracks:\s*\[(.+)]""").find(decrypt)?.groupValues?.get(1)
|
||||
|
||||
// required
|
||||
val headers = mapOf(
|
||||
"Accept" to "*/*",
|
||||
"Connection" to "keep-alive",
|
||||
"Sec-Fetch-Dest" to "empty",
|
||||
"Sec-Fetch-Mode" to "cors",
|
||||
"Sec-Fetch-Site" to "cross-site",
|
||||
"Origin" to mainUrl,
|
||||
)
|
||||
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
source ?: return,
|
||||
"$mainUrl/",
|
||||
Qualities.P1080.value,
|
||||
headers = headers,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
|
||||
AppUtils.tryParseJson<List<Tracks>>("[$tracks]")
|
||||
?.filter { it.kind == "captions" }?.map { track ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
track.label ?: "",
|
||||
track.file ?: return@map null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class Tracks(
|
||||
@JsonProperty("file") val file: String? = null,
|
||||
@JsonProperty("label") val label: String? = null,
|
||||
@JsonProperty("kind") val kind: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import java.net.URL
|
||||
|
||||
open class Dailymotion : ExtractorApi() {
|
||||
override val mainUrl = "https://www.dailymotion.com"
|
||||
override val name = "Dailymotion"
|
||||
override val requiresReferer = false
|
||||
|
||||
@Suppress("RegExpSimplifiable")
|
||||
private val videoIdRegex = "^[kx][a-zA-Z0-9]+\$".toRegex()
|
||||
|
||||
// https://www.dailymotion.com/video/k3JAHfletwk94ayCVIu
|
||||
// https://www.dailymotion.com/embed/video/k3JAHfletwk94ayCVIu
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val embedUrl = getEmbedUrl(url) ?: return
|
||||
val doc = app.get(embedUrl).document
|
||||
val prefix = "window.__PLAYER_CONFIG__ = "
|
||||
val configStr = doc.selectFirst("script:containsData($prefix)")?.data() ?: return
|
||||
val config = tryParseJson<Config>(configStr.substringAfter(prefix)) ?: return
|
||||
val id = getVideoId(embedUrl) ?: return
|
||||
val dmV1st = config.dmInternalData.v1st
|
||||
val dmTs = config.dmInternalData.ts
|
||||
val metaDataUrl =
|
||||
"$mainUrl/player/metadata/video/$id?locale=en&dmV1st=$dmV1st&dmTs=$dmTs&is_native_app=0"
|
||||
val cookies = mapOf(
|
||||
"v1st" to dmV1st,
|
||||
"dmvk" to config.context.dmvk,
|
||||
"ts" to dmTs.toString()
|
||||
)
|
||||
val metaData = app.get(metaDataUrl, referer = embedUrl, cookies = cookies)
|
||||
.parsedSafe<MetaData>() ?: return
|
||||
metaData.qualities.forEach { (_, video) ->
|
||||
video.forEach {
|
||||
getStream(it.url, this.name, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEmbedUrl(url: String): String? {
|
||||
if (url.contains("/embed/")) {
|
||||
return url
|
||||
}
|
||||
val vid = getVideoId(url) ?: return null
|
||||
return "$mainUrl/embed/video/$vid"
|
||||
}
|
||||
|
||||
private fun getVideoId(url: String): String? {
|
||||
val path = URL(url).path
|
||||
val id = path.substringAfter("video/")
|
||||
if (id.matches(videoIdRegex)) {
|
||||
return id
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private suspend fun getStream(
|
||||
streamLink: String,
|
||||
name: String,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
return generateM3u8(
|
||||
name,
|
||||
streamLink,
|
||||
"",
|
||||
).forEach(callback)
|
||||
}
|
||||
data class Config(
|
||||
val context: Context,
|
||||
val dmInternalData: InternalData
|
||||
)
|
||||
|
||||
data class InternalData(
|
||||
val ts: Int,
|
||||
val v1st: String
|
||||
)
|
||||
|
||||
data class Context(
|
||||
@JsonProperty("access_token") val accessToken: String?,
|
||||
val dmvk: String,
|
||||
)
|
||||
|
||||
data class MetaData(
|
||||
val qualities: Map<String, List<VideoLink>>
|
||||
)
|
||||
|
||||
data class VideoLink(
|
||||
val type: String,
|
||||
val url: String
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
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 Dooood : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dooood.com"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
class DoodYtExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.yt"
|
||||
}
|
||||
|
||||
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(
|
||||
this.name,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
getQualityFromName(quality),
|
||||
false
|
||||
)
|
||||
) // links are valid in 8h
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.httpsify
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
|
||||
open class Embedgram : ExtractorApi() {
|
||||
override val name = "Embedgram"
|
||||
|
|
@ -23,17 +22,16 @@ open class Embedgram : ExtractorApi() {
|
|||
val link = document.select("video source:last-child").attr("src")
|
||||
val quality = document.select("video source:last-child").attr("title")
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
httpsify(link),
|
||||
) {
|
||||
this.referer = "$mainUrl/"
|
||||
this.quality = getQualityFromName(quality)
|
||||
this.headers = mapOf(
|
||||
"$mainUrl/",
|
||||
getQualityFromName(quality),
|
||||
headers = mapOf(
|
||||
"Range" to "bytes=0-"
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,14 +23,13 @@ open class Evoload : ExtractorApi() {
|
|||
val r = app.post("https://evoload.io/SecurePlayer", data=(payload)).text
|
||||
val link = Regex("src\":\"(.*?)\"").find(r)?.destructured?.component1() ?: return listOf()
|
||||
return listOf(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link,
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
}
|
||||
url,
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper.Companion.generateM3u8
|
||||
|
||||
class Guccihide : Filesim() {
|
||||
override val name = "Guccihide"
|
||||
override var mainUrl = "https://guccihide.com"
|
||||
}
|
||||
|
||||
class Ahvsh : Filesim() {
|
||||
override val name = "Ahvsh"
|
||||
override var mainUrl = "https://ahvsh.com"
|
||||
}
|
||||
|
||||
class Moviesm4u : Filesim() {
|
||||
override val mainUrl = "https://moviesm4u.com"
|
||||
override val name = "Moviesm4u"
|
||||
}
|
||||
|
||||
class FileMoonIn : Filesim() {
|
||||
override val mainUrl = "https://filemoon.in"
|
||||
override val name = "FileMoon"
|
||||
}
|
||||
|
||||
class StreamhideTo : Filesim() {
|
||||
override val mainUrl = "https://streamhide.to"
|
||||
override val name = "Streamhide"
|
||||
}
|
||||
|
||||
class StreamhideCom : Filesim() {
|
||||
override var name: String = "Streamhide"
|
||||
override var mainUrl: String = "https://streamhide.com"
|
||||
}
|
||||
|
||||
class Movhide : Filesim() {
|
||||
override var name: String = "Movhide"
|
||||
override var mainUrl: String = "https://movhide.pro"
|
||||
}
|
||||
|
||||
class Ztreamhub : Filesim() {
|
||||
override val mainUrl: String = "https://ztreamhub.com" //Here 'cause works
|
||||
override val name = "Zstreamhub"
|
||||
}
|
||||
class FileMoon : Filesim() {
|
||||
override val mainUrl = "https://filemoon.to"
|
||||
override val name = "FileMoon"
|
||||
}
|
||||
|
||||
class FileMoonSx : Filesim() {
|
||||
override val mainUrl = "https://filemoon.sx"
|
||||
override val name = "FileMoonSx"
|
||||
}
|
||||
|
||||
open class Filesim : ExtractorApi() {
|
||||
override val name = "Filesim"
|
||||
override val mainUrl = "https://files.im"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val response = app.get(url, referer = referer)
|
||||
val script = if (!getPacked(response.text).isNullOrEmpty()) {
|
||||
getAndUnpack(response.text)
|
||||
} else {
|
||||
response.document.selectFirst("script:containsData(sources:)")?.data()
|
||||
}
|
||||
val m3u8 =
|
||||
Regex("file:\\s*\"(.*?m3u8.*?)\"").find(script ?: return)?.groupValues?.getOrNull(1)
|
||||
generateM3u8(
|
||||
name,
|
||||
m3u8 ?: return,
|
||||
mainUrl
|
||||
).forEach(callback)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -82,7 +82,7 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
val password = Regex("null,['|\"](\\w+)['|\"]").first(eval)
|
||||
?.split(Regex("\\D+"))
|
||||
?.joinToString("") {
|
||||
it.toInt().toChar().toString()
|
||||
Char(it.toInt()).toString()
|
||||
}.let { Regex("var pass = \"(\\S+?)\"").first(it ?: return)?.toByteArray() }
|
||||
?: throw ErrorLoadingException("can't find password")
|
||||
val decryptedData = cryptoAESHandler(data, password, false, "AES/CBC/NoPadding")?.let { getAndUnpack(it) }?.replace("\\", "")
|
||||
|
|
@ -94,22 +94,21 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
it.groupValues[1] to it.groupValues[2]
|
||||
}.toList().distinctBy { it.second }.map { (link, quality) ->
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
source = this.name,
|
||||
name = this.name,
|
||||
url = "${httpsify(link)}&res=$quality",
|
||||
) {
|
||||
this.referer = mainUrl
|
||||
this.quality = quality.toIntOrNull() ?: Qualities.Unknown.value
|
||||
this.headers = mapOf("Range" to "bytes=0-")
|
||||
}
|
||||
referer = mainUrl,
|
||||
quality = quality.toIntOrNull() ?: Qualities.Unknown.value,
|
||||
headers = mapOf("Range" to "bytes=0-")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
subData?.addMarks("file")?.addMarks("kind")?.addMarks("label").let { dataSub ->
|
||||
tryParseJson<List<Tracks>>("[$dataSub]")?.map { sub ->
|
||||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
SubtitleFile(
|
||||
sub.label,
|
||||
httpsify(sub.file)
|
||||
)
|
||||
|
|
@ -125,4 +124,4 @@ open class Gdriveplayer : ExtractorApi() {
|
|||
@JsonProperty("label") val label: String
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
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.Qualities
|
||||
|
||||
open class Gofile : ExtractorApi() {
|
||||
override val name = "Gofile"
|
||||
override val mainUrl = "https://gofile.io"
|
||||
override val requiresReferer = false
|
||||
private val mainApi = "https://api.gofile.io"
|
||||
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1)
|
||||
val token = app.get("$mainApi/createAccount").parsedSafe<Account>()?.data?.get("token")
|
||||
val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let {
|
||||
Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1)
|
||||
}
|
||||
app.get("$mainApi/getContent?contentId=$id&token=$token&websiteToken=$websiteToken")
|
||||
.parsedSafe<Source>()?.data?.contents?.forEach {
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
it.value["link"] ?: return,
|
||||
"",
|
||||
getQuality(it.value["name"]),
|
||||
headers = mapOf(
|
||||
"Cookie" to "accountToken=$token"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun getQuality(str: String?): Int {
|
||||
return Regex("(\\d{3,4})[pP]").find(str ?: "")?.groupValues?.getOrNull(1)?.toIntOrNull()
|
||||
?: Qualities.Unknown.value
|
||||
}
|
||||
|
||||
data class Account(
|
||||
@JsonProperty("data") val data: HashMap<String, String>? = null,
|
||||
)
|
||||
|
||||
data class Data(
|
||||
@JsonProperty("contents") val contents: HashMap<String, HashMap<String, String>>? = null,
|
||||
)
|
||||
|
||||
data class Source(
|
||||
@JsonProperty("data") val data: Data? = null,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
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 Vanfem : GuardareStream() {
|
||||
override var name = "Vanfem"
|
||||
override var mainUrl = "https://vanfem.com/"
|
||||
}
|
||||
|
||||
class CineGrabber : GuardareStream() {
|
||||
override var name = "CineGrabber"
|
||||
override var mainUrl = "https://cinegrabber.com"
|
||||
}
|
||||
|
||||
open class GuardareStream : ExtractorApi() {
|
||||
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(
|
||||
this.name,
|
||||
this.name,
|
||||
it.file + ".${it.type}",
|
||||
mainUrl,
|
||||
it.label.filter { it.isDigit() }.toInt(),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!jsonVideoData.captions.isNullOrEmpty()){
|
||||
val iframe = app.get(url)
|
||||
// var USER_ID = '224879';
|
||||
val userIdRegex = Regex("""USER_ID.*?(\d+)""")
|
||||
val userId = userIdRegex.find(iframe.text)?.groupValues?.getOrNull(1) ?: return
|
||||
jsonVideoData.captions.forEach {
|
||||
if (it == null) return@forEach
|
||||
val subUrl = it.getUrl(mainUrl, userId)
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
it.language ?: "",
|
||||
subUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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://neonime.fun"
|
||||
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://nontonanimeid.bio"
|
||||
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?
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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?
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -3,12 +3,8 @@ 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.extractors.helper.JwPlayerHelper
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||
import com.lagradost.cloudstream3.utils.getPacked
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
||||
open class Jeniusplay : ExtractorApi() {
|
||||
override val name = "Jeniusplay"
|
||||
|
|
@ -37,17 +33,40 @@ open class Jeniusplay : ExtractorApi() {
|
|||
url,
|
||||
).forEach(callback)
|
||||
|
||||
|
||||
document.select("script").map { script ->
|
||||
if (getPacked(script.data()) != null) {
|
||||
val unpacked = getAndUnpack(script.data())
|
||||
JwPlayerHelper.extractStreamLinks(unpacked, name, mainUrl, callback, subtitleCallback)
|
||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||
val subData =
|
||||
getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],")
|
||||
tryParseJson<List<Tracks>>("[$subData]")?.map { subtitle ->
|
||||
subtitleCallback.invoke(
|
||||
SubtitleFile(
|
||||
getLanguage(subtitle.label ?: ""),
|
||||
subtitle.file
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLanguage(str: String): String {
|
||||
return when {
|
||||
str.contains("indonesia", true) || str
|
||||
.contains("bahasa", true) -> "Indonesian"
|
||||
else -> str
|
||||
}
|
||||
}
|
||||
|
||||
data class ResponseSource(
|
||||
@JsonProperty("hls") val hls: Boolean,
|
||||
@JsonProperty("videoSource") val videoSource: String,
|
||||
@JsonProperty("securedLink") val securedLink: String?,
|
||||
)
|
||||
|
||||
data class Tracks(
|
||||
@JsonProperty("kind") val kind: String?,
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("label") val label: String?,
|
||||
)
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.httpsify
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
|
||||
open class Krakenfiles : ExtractorApi() {
|
||||
override val name = "Krakenfiles"
|
||||
|
|
@ -24,10 +23,12 @@ open class Krakenfiles : ExtractorApi() {
|
|||
val link = doc.selectFirst("source")?.attr("src")
|
||||
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
httpsify(link ?: return),
|
||||
"",
|
||||
Qualities.Unknown.value
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -6,7 +6,6 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getQualityFromName
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
|
||||
open class Linkbox : ExtractorApi() {
|
||||
override val name = "Linkbox"
|
||||
|
|
@ -19,19 +18,17 @@ open class Linkbox : ExtractorApi() {
|
|||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val token = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
||||
val id = app.get("$mainUrl/api/file/share_out_list/?sortField=utime&sortAsc=0&pageNo=1&pageSize=50&shareToken=$token").parsedSafe<Responses>()?.data?.itemId
|
||||
val id = Regex("""(?:/f/|/file/|\?id=)(\w+)""").find(url)?.groupValues?.get(1)
|
||||
app.get("$mainUrl/api/file/detail?itemId=$id", referer = url)
|
||||
.parsedSafe<Responses>()?.data?.itemInfo?.resolutionList?.map { link ->
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link.url ?: return@map null,
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = getQualityFromName(link.resolution)
|
||||
}
|
||||
url,
|
||||
getQualityFromName(link.resolution)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +44,6 @@ open class Linkbox : ExtractorApi() {
|
|||
|
||||
data class Data(
|
||||
@JsonProperty("itemInfo") val itemInfo: ItemInfo? = null,
|
||||
@JsonProperty("itemId") val itemId: String? = null,
|
||||
)
|
||||
|
||||
data class Responses(
|
||||
|
|
@ -3,30 +3,10 @@ package com.lagradost.cloudstream3.extractors
|
|||
import com.lagradost.cloudstream3.app
|
||||
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(){
|
||||
override var mainUrl = "https://mixdrop.bz"
|
||||
}
|
||||
|
||||
class MixDropAg : MixDrop(){
|
||||
override var mainUrl = "https://mixdrop.ag"
|
||||
}
|
||||
|
||||
class MixDropCh : MixDrop(){
|
||||
override var mainUrl = "https://mixdrop.ch"
|
||||
}
|
||||
|
|
@ -45,22 +25,21 @@ open class MixDrop : ExtractorApi() {
|
|||
}
|
||||
|
||||
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 ->
|
||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
httpsify(link),
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
}
|
||||
url,
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package com.lagradost.cloudstream3.extractors
|
|||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.newSubtitleFile
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
|
@ -34,7 +33,7 @@ open class Moviehab : ExtractorApi() {
|
|||
|
||||
Regex("src[\"|'],\\s[\"|'](\\S+)[\"|']\\)").find(res.text)?.groupValues?.get(1).let {sub ->
|
||||
subtitleCallback.invoke(
|
||||
newSubtitleFile(
|
||||
SubtitleFile(
|
||||
it.select("track").attr("label"),
|
||||
"$mainUrl/$sub"
|
||||
)
|
||||
|
|
@ -5,7 +5,6 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||
import com.lagradost.cloudstream3.utils.newExtractorLink
|
||||
|
||||
open class Mp4Upload : ExtractorApi() {
|
||||
override var name = "Mp4Upload"
|
||||
|
|
@ -25,26 +24,24 @@ open class Mp4Upload : ExtractorApi() {
|
|||
unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link,
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = quality ?: Qualities.Unknown.value
|
||||
}
|
||||
url,
|
||||
quality ?: Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
srcRegex2.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link,
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = quality ?: Qualities.Unknown.value
|
||||
}
|
||||
url,
|
||||
quality ?: Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
return null
|
||||
|
|
@ -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://anihdplay.com"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ 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.newExtractorLink
|
||||
|
||||
open class Mvidoo : ExtractorApi() {
|
||||
override val name = "Mvidoo"
|
||||
|
|
@ -14,10 +13,11 @@ open class Mvidoo : ExtractorApi() {
|
|||
|
||||
private fun String.decodeHex(): String {
|
||||
require(length % 2 == 0) { "Must have an even length" }
|
||||
return chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
.decodeToString()
|
||||
return String(
|
||||
chunked(2)
|
||||
.map { it.toInt(16).toByte() }
|
||||
.toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getUrl(
|
||||
|
|
@ -31,17 +31,16 @@ open class Mvidoo : ExtractorApi() {
|
|||
?.removeSurrounding("[", "]")?.replace("\"", "")?.replace("\\x", "")?.split(",")?.map { it.decodeHex() }?.reversed()?.joinToString("") ?: return
|
||||
Regex("source\\s*src=\"([^\"]+)").find(data)?.groupValues?.get(1)?.let { link ->
|
||||
callback.invoke(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
link
|
||||
) {
|
||||
this.referer = "$mainUrl/"
|
||||
this.quality = Qualities.Unknown.value
|
||||
this.headers = mapOf(
|
||||
link,
|
||||
"$mainUrl/",
|
||||
Qualities.Unknown.value,
|
||||
headers = mapOf(
|
||||
"Range" to "bytes=0-"
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
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.INFER_TYPE
|
||||
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),
|
||||
type = INFER_TYPE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.SubtitleFile
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
||||
open class Pixeldrain : ExtractorApi() {
|
||||
override val name = "Pixeldrain"
|
||||
override val mainUrl = "https://pixeldrain.com"
|
||||
override val requiresReferer = false
|
||||
override suspend fun getUrl(
|
||||
url: String,
|
||||
referer: String?,
|
||||
subtitleCallback: (SubtitleFile) -> Unit,
|
||||
callback: (ExtractorLink) -> Unit
|
||||
) {
|
||||
val mId = Regex("/([ul]/[\\da-zA-Z\\-]+)").find(url)?.groupValues?.get(1)?.split("/")
|
||||
callback.invoke(
|
||||
ExtractorLink(
|
||||
this.name,
|
||||
this.name,
|
||||
"$mainUrl/api/file/${mId?.last() ?: return}?download",
|
||||
url,
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.api.Log
|
||||
import android.util.Log
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
|
@ -24,7 +24,7 @@ open class PlayLtXyz: ExtractorApi() {
|
|||
val doc = app.get(url, referer = referer).document
|
||||
//Log.i(this.name, "Result => (url, script) $url / ${doc.select("script")}")
|
||||
bodyText = doc.select("script").firstOrNull {
|
||||
val text = it.toString()
|
||||
val text = it?.toString() ?: ""
|
||||
text.contains("var idUser")
|
||||
}?.toString() ?: ""
|
||||
//Log.i(this.name, "Result => (bodyText) $bodyText")
|
||||
|
|
@ -61,15 +61,14 @@ open class PlayLtXyz: ExtractorApi() {
|
|||
val linkUrl = item.data ?: ""
|
||||
if (linkUrl.isNotBlank()) {
|
||||
extractedLinksList.add(
|
||||
newExtractorLink(
|
||||
ExtractorLink(
|
||||
source = name,
|
||||
name = name,
|
||||
url = linkUrl,
|
||||
type = ExtractorLinkType.M3U8
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
}
|
||||
referer = url,
|
||||
quality = Qualities.Unknown.value,
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue