Compare commits
2 commits
master
...
advancedWe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f09132a7c |
||
|
|
921a1dab37 |
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
|
||||
|
|
|
|||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -1,8 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Request a new provider or report bug with an existing provider
|
||||
- name: Report provider bug
|
||||
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. 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
|
|
@ -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
|
||||
|
|
|
|||
BIN
.github/downloads.jpg
vendored
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
.github/home.jpg
vendored
Normal file
|
After Width: | Height: | Size: 137 KiB |
65
.github/locales.py
vendored
|
|
@ -1,65 +0,0 @@
|
|||
import re
|
||||
import glob
|
||||
import requests
|
||||
import lxml.etree as ET # builtin library doesn't preserve comments
|
||||
|
||||
|
||||
SETTINGS_PATH = "app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt"
|
||||
START_MARKER = "/* begin language list */"
|
||||
END_MARKER = "/* end language list */"
|
||||
XML_NAME = "app/src/main/res/values-b+"
|
||||
ISO_MAP_URL = "https://raw.githubusercontent.com/haliaeetus/iso-639/master/data/iso_639-1.min.json"
|
||||
INDENT = " "*4
|
||||
|
||||
iso_map = requests.get(ISO_MAP_URL, timeout=300).json()
|
||||
|
||||
# Load settings file
|
||||
src = open(SETTINGS_PATH, "r", encoding='utf-8').read()
|
||||
before_src, rest = src.split(START_MARKER)
|
||||
rest, after_src = rest.split(END_MARKER)
|
||||
|
||||
# Load already added langs
|
||||
languages = {}
|
||||
for lang in re.finditer(r'Pair\("(.*)", "(.*)"\)', rest):
|
||||
name, iso = lang.groups()
|
||||
languages[iso] = name
|
||||
|
||||
# Add not yet added langs
|
||||
for folder in glob.glob(f"{XML_NAME}*"):
|
||||
iso = folder[len(XML_NAME):].replace("+", "-")
|
||||
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
|
||||
|
||||
# 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}"),')
|
||||
|
||||
# Update settings file
|
||||
open(SETTINGS_PATH, "w+",encoding='utf-8').write(
|
||||
before_src +
|
||||
START_MARKER +
|
||||
"\n" +
|
||||
"\n".join(pairs) +
|
||||
"\n" +
|
||||
END_MARKER +
|
||||
after_src
|
||||
)
|
||||
|
||||
# Go through each values.xml file and fix escaped \@string
|
||||
for file in glob.glob(f"{XML_NAME}*/strings.xml"):
|
||||
try:
|
||||
tree = ET.parse(file)
|
||||
for child in tree.getroot():
|
||||
if not child.text:
|
||||
continue
|
||||
if child.text.startswith("\\@string/"):
|
||||
print(f"[{file}] fixing {child.attrib['name']}")
|
||||
child.text = child.text.replace("\\@string/", "@string/")
|
||||
with open(file, 'wb') as fp:
|
||||
fp.write(b'<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
tree.write(fp, encoding="utf-8", method="xml", pretty_print=True, xml_declaration=False)
|
||||
except ET.ParseError as ex:
|
||||
print(f"[{file}] {ex}")
|
||||
BIN
.github/player.jpg
vendored
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
.github/results.jpg
vendored
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
.github/search.jpg
vendored
Normal file
|
After Width: | Height: | Size: 150 KiB |
93
.github/workflows/build_to_archive.yml
vendored
|
|
@ -1,93 +0,0 @@
|
|||
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
|
||||
48
.github/workflows/generate_dokka.yml
vendored
|
|
@ -1,67 +1,65 @@
|
|||
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: |
|
||||
shopt -s extglob
|
||||
cd $GITHUB_WORKSPACE/dokka/
|
||||
rm -rf "./app"
|
||||
rm -rf "./library"
|
||||
rm -rf !(.git)
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- name: Setup JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
java-version: 11
|
||||
|
||||
- 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: |
|
||||
|
|
|
|||
63
.github/workflows/issue-action.yml
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
name: Issue automatic actions
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
jobs:
|
||||
issue-moderator:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate access token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v1
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
- name: Similarity analysis
|
||||
uses: actions-cool/issues-similarity-analysis@v1
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
filter-threshold: 0.5
|
||||
title-excludes: ''
|
||||
comment-title: |
|
||||
### Your issue looks similar to these issues:
|
||||
Please close if duplicate.
|
||||
comment-body: '${index}. ${similarity} #${number}'
|
||||
- uses: actions/checkout@v2
|
||||
- name: Automatically close issues that dont follow the issue template
|
||||
uses: lucasbento/auto-close-issues@v1.0.2
|
||||
with:
|
||||
github-token: ${{ steps.generate_token.outputs.token }}
|
||||
issue-close-message: |
|
||||
@${issue.user.login}: hello! :wave:
|
||||
This issue is being automatically closed because it does not follow the issue template."
|
||||
closed-issues-label: "invalid"
|
||||
- name: Check if issue mentions a provider
|
||||
id: provider_check
|
||||
env:
|
||||
GH_TEXT: "${{ github.event.issue.title }} ${{ github.event.issue.body }}"
|
||||
run: |
|
||||
wget --output-document check_issue.py "https://raw.githubusercontent.com/recloudstream/.github/master/.github/check_issue.py"
|
||||
pip3 install httpx
|
||||
RES="$(python3 ./check_issue.py)"
|
||||
echo "::set-output name=name::${RES}"
|
||||
- name: Comment if issue mentions a provider
|
||||
if: steps.provider_check.outputs.name != 'none'
|
||||
uses: actions-cool/issues-helper@v3
|
||||
with:
|
||||
actions: 'create-comment'
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
body: |
|
||||
Hello ${{ github.event.issue.user.login }}.
|
||||
Please do not report any provider bugs here. This repository does not contain any providers. Please find the appropriate repository and report your issue there or join the [discord](https://discord.gg/5Hus6fM).
|
||||
|
||||
Found provider name: `${{ steps.provider_check.outputs.name }}`
|
||||
- name: Add eyes reaction to all issues
|
||||
uses: actions-cool/emoji-helper@v1.0.0
|
||||
with:
|
||||
type: 'issue'
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
emoji: 'eyes'
|
||||
|
||||
|
||||
45
.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
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Fetch keystore
|
||||
id: fetch_keystore
|
||||
run: |
|
||||
|
|
@ -47,32 +40,24 @@ jobs:
|
|||
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 }}
|
||||
|
||||
echo "::set-output name=key_pwd::$KEY_PWD"
|
||||
- name: Run Gradle
|
||||
run: ./gradlew assemblePrereleaseRelease androidSourcesJar makeJar
|
||||
run: |
|
||||
./gradlew assemblePrerelease
|
||||
./gradlew androidSourcesJar
|
||||
./gradlew makeJar
|
||||
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"
|
||||
prerelease: true
|
||||
prerelease: false
|
||||
title: "Pre-release Build"
|
||||
files: |
|
||||
app/build/outputs/apk/prerelease/release/*.apk
|
||||
app/build/outputs/apk/prerelease/*.apk
|
||||
app/build/libs/app-sources.jar
|
||||
app/build/classes.jar
|
||||
|
|
|
|||
29
.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
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: 17
|
||||
|
||||
java-version: '11'
|
||||
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 assembleDebug
|
||||
- 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"
|
||||
path: "app/build/outputs/apk/debug/*.apk"
|
||||
|
|
|
|||
46
.github/workflows/update_locales.yml
vendored
|
|
@ -1,46 +0,0 @@
|
|||
name: Fix locale issues
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- '**.xml'
|
||||
workflow_dispatch:
|
||||
|
||||
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
|
||||
with:
|
||||
app_id: ${{ secrets.GH_APP_ID }}
|
||||
private_key: ${{ secrets.GH_APP_KEY }}
|
||||
repository: "recloudstream/cloudstream"
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip3 install lxml requests
|
||||
|
||||
- name: Edit files
|
||||
run: python3 .github/locales.py
|
||||
|
||||
- name: Commit to the repo
|
||||
run: |
|
||||
git config --local user.email "111277985+recloudstream[bot]@users.noreply.github.com"
|
||||
git config --local user.name "recloudstream[bot]"
|
||||
git add .
|
||||
# "echo" returns true so the build succeeds, even if no changed files
|
||||
git commit -m 'chore(locales): fix locale issues' || echo
|
||||
git push
|
||||
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
|
|
@ -0,0 +1 @@
|
|||
CloudStream
|
||||
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
|
|
@ -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
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
||||
7
.idea/discord.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
||||
21
.idea/gradle.xml
generated
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="delegatedBuild" value="true" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="11" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
35
.idea/jarRepositories.xml
generated
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?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>
|
||||
</component>
|
||||
</project>
|
||||
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
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"githubPullRequests.ignoredPullRequestBranches": [
|
||||
"master"
|
||||
],
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
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.
|
||||
129
README.md
|
|
@ -1,111 +1,44 @@
|
|||
# CloudStream
|
||||
|
||||
**⚠️ Warning: By default, this app doesn't provide any video sources; you have to install extensions to add functionality to the app.**
|
||||
|
||||
[](https://discord.gg/5Hus6fM)
|
||||
**⚠️ Warning: By default this app doesn't provide any video sources, you have to install extensions in order to add functionality to the app.**
|
||||
You can find the list of community-maintained extension repositories [here
|
||||
](https://recloudstream.github.io/repos/)
|
||||
|
||||
|
||||
## 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)
|
||||
[](https://discord.gg/5Hus6fM)
|
||||
|
||||
|
||||
<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
|
||||
+ Phone and TV support
|
||||
+ Download and stream movies, tv-shows and anime
|
||||
+ Chromecast
|
||||
+ Extension system for personal customization
|
||||
|
||||
***Screenshots:***
|
||||
|
||||
<a id="install_rules"></a>
|
||||
<img src="./.github/home.jpg" height="400"/><img src="./.github/search.jpg" height="400"/><img src="./.github/downloads.jpg" height="400"/><img src="./.github/results.jpg" height="400"/>
|
||||
<img src="./.github/player.jpg" height="200"/>
|
||||
|
||||
## Installation:
|
||||
***The list of supported languages:***
|
||||
* 🇱🇧 Arabic
|
||||
* 🇨🇿 Czech
|
||||
* 🇳🇱 Dutch
|
||||
* 🇬🇧 English
|
||||
* 🇫🇷 French
|
||||
* 🇩🇪 German
|
||||
* 🇬🇷 Greek
|
||||
* 🇮🇳 Hindi
|
||||
* 🇮🇩 Indonesian
|
||||
* 🇮🇹 Italian
|
||||
* 🇲🇰 Macedonian
|
||||
* 🇮🇳 Malayalam
|
||||
* 🇳🇴 Norsk
|
||||
* 🇵🇱 Polish
|
||||
* 🇧🇷 Portuguese (Brazil)
|
||||
* 🇷🇴 Romanian
|
||||
* 🇪🇸 Spanish
|
||||
* 🇸🇪 Swedish
|
||||
* 🇵🇭 Tagalog
|
||||
* 🇹🇷 Turkish
|
||||
* 🇻🇳 Vietnamese
|
||||
|
||||
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>
|
||||
|
|
|
|||
215
app/build.gradle
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'kotlin-android-extensions'
|
||||
id 'org.jetbrains.dokka'
|
||||
}
|
||||
|
||||
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
|
||||
def allFilesFromDir = new File(tmpFilePath).listFiles()
|
||||
def prereleaseStoreFile = null
|
||||
if (allFilesFromDir != null) {
|
||||
prereleaseStoreFile = allFilesFromDir.first()
|
||||
}
|
||||
|
||||
android {
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
signingConfigs {
|
||||
prerelease {
|
||||
if (prereleaseStoreFile != null) {
|
||||
storeFile = file(prereleaseStoreFile)
|
||||
storePassword System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "30.0.3"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.lagradost.cloudstream3"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
|
||||
versionCode 50
|
||||
versionName "3.1.3"
|
||||
|
||||
resValue "string", "app_version",
|
||||
"${defaultConfig.versionName}${versionNameSuffix ?: ""}"
|
||||
|
||||
resValue "string", "commit_hash",
|
||||
("git rev-parse --short HEAD".execute().text.trim() ?: "")
|
||||
|
||||
resValue "bool", "is_prerelease", "false"
|
||||
|
||||
buildConfigField("String", "BUILDDATE", "new java.text.SimpleDateFormat(\"yyyy-MM-dd HH:mm\").format(new java.util.Date(" + System.currentTimeMillis() + "L));")
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
kapt {
|
||||
includeCompileClasspath = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
// release {
|
||||
// debuggable false
|
||||
// minifyEnabled false
|
||||
// shrinkResources false
|
||||
// proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
// resValue "bool", "is_prerelease", "false"
|
||||
// }
|
||||
prerelease {
|
||||
applicationIdSuffix ".prerelease"
|
||||
buildConfigField("boolean", "BETA", "true")
|
||||
signingConfig signingConfigs.prerelease
|
||||
versionNameSuffix '-PRE'
|
||||
debuggable false
|
||||
minifyEnabled false
|
||||
shrinkResources false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "bool", "is_prerelease", "true"
|
||||
}
|
||||
debug {
|
||||
debuggable true
|
||||
applicationIdSuffix ".debug"
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
resValue "bool", "is_prerelease", "true"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
freeCompilerArgs = ['-Xjvm-default=compatibility']
|
||||
}
|
||||
lintOptions {
|
||||
checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.mediahome:video:1.0.0'
|
||||
implementation 'androidx.test.ext:junit-ktx:1.1.3'
|
||||
testImplementation 'org.json:json:20180813'
|
||||
|
||||
implementation 'androidx.core:core-ktx:1.8.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.2'
|
||||
|
||||
// dont change this to 1.6.0 it looks ugly af
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
//implementation "io.karn:khttp-android:0.1.2" //okhttp instead
|
||||
// implementation 'org.jsoup:jsoup:1.13.1'
|
||||
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1"
|
||||
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
|
||||
implementation 'com.github.bumptech.glide:glide:4.13.1'
|
||||
kapt 'com.github.bumptech.glide:compiler:4.13.1'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.13.0'
|
||||
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
|
||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
||||
|
||||
// implementation "androidx.leanback:leanback-paging:1.1.0-alpha09"
|
||||
|
||||
// Exoplayer
|
||||
implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
|
||||
implementation 'com.google.android.exoplayer:extension-cast:2.16.1'
|
||||
implementation "com.google.android.exoplayer:extension-mediasession:2.16.1"
|
||||
implementation 'com.google.android.exoplayer:extension-okhttp:2.16.1'
|
||||
|
||||
//implementation "com.google.android.exoplayer:extension-leanback:2.14.0"
|
||||
|
||||
// Bug reports
|
||||
implementation "ch.acra:acra-core:5.8.4"
|
||||
implementation "ch.acra:acra-toast:5.8.4"
|
||||
|
||||
compileOnly "com.google.auto.service:auto-service-annotations:1.0"
|
||||
//either for java sources:
|
||||
annotationProcessor "com.google.auto.service:auto-service:1.0"
|
||||
//or for kotlin sources (requires kapt gradle plugin):
|
||||
kapt "com.google.auto.service:auto-service:1.0"
|
||||
|
||||
// subtitle color picker
|
||||
implementation 'com.jaredrummler:colorpicker:1.1.0'
|
||||
|
||||
//run JS
|
||||
implementation 'org.mozilla:rhino:1.7.14'
|
||||
|
||||
// TorrentStream
|
||||
//implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
|
||||
|
||||
// Downloading
|
||||
implementation "androidx.work:work-runtime:2.7.1"
|
||||
implementation "androidx.work:work-runtime-ktx:2.7.1"
|
||||
|
||||
// Networking
|
||||
// implementation "com.squareup.okhttp3:okhttp:4.9.2"
|
||||
// implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1"
|
||||
implementation 'com.github.Blatzar:NiceHttp:0.3.2'
|
||||
|
||||
// Util to skip the URI file fuckery 🙏
|
||||
implementation "com.github.tachiyomiorg:unifile:17bec43"
|
||||
|
||||
// API because cba maintaining it myself
|
||||
implementation "com.uwetrottmann.tmdb2:tmdb-java:2.6.0"
|
||||
|
||||
implementation 'com.github.discord:OverlappingPanels:0.1.3'
|
||||
// debugImplementation because LeakCanary should only run in debug builds.
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
||||
|
||||
// for shimmer when loading
|
||||
implementation 'com.facebook.shimmer:shimmer:0.5.0'
|
||||
|
||||
implementation "androidx.tvprovider:tvprovider:1.0.0"
|
||||
|
||||
// used for subtitle decoding https://github.com/albfernandez/juniversalchardet
|
||||
implementation 'com.github.albfernandez:juniversalchardet:2.4.0'
|
||||
|
||||
// slow af yt
|
||||
//implementation 'com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT'
|
||||
|
||||
// newpipe yt
|
||||
implementation 'com.github.recloudstream:NewPipeExtractor:master-SNAPSHOT'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
// Library/extensions searching with Levenshtein distance
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
|
||||
}
|
||||
|
||||
task androidSourcesJar(type: Jar) {
|
||||
getArchiveClassifier().set('sources')
|
||||
from android.sourceSets.main.java.srcDirs//full sources
|
||||
}
|
||||
|
||||
task makeJar(type: Copy) {
|
||||
// after modifying here, you can export. Jar
|
||||
from('build/intermediates/compile_app_classes_jar/debug')
|
||||
into('build') // output location
|
||||
include('classes.jar') // the classes file of the imported rack package
|
||||
dependsOn build
|
||||
}
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
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
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.dokka)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
val javaTarget = JvmTarget.fromTarget(libs.versions.jvmTarget.get())
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
androidComponents {
|
||||
onVariants { variant ->
|
||||
variant.sources.assets?.addGeneratedSourceDirectory(
|
||||
generateGitHash,
|
||||
GenerateGitHashTask::outputDir
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
storePassword = System.getenv("SIGNING_STORE_PASSWORD")
|
||||
keyAlias = System.getenv("SIGNING_KEY_ALIAS")
|
||||
keyPassword = System.getenv("SIGNING_KEY_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
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()
|
||||
|
||||
manifestPlaceholders["target_sdk_version"] = libs.versions.targetSdk.get()
|
||||
|
||||
// Reads local.properties
|
||||
val localProperties = gradleLocalProperties(rootDir, project.providers)
|
||||
|
||||
buildConfigField(
|
||||
"long",
|
||||
"BUILD_DATE",
|
||||
"${System.currentTimeMillis()}"
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"SIMKL_CLIENT_ID",
|
||||
"\"" + (System.getenv("SIMKL_CLIENT_ID") ?: localProperties["simkl.id"]) + "\""
|
||||
)
|
||||
buildConfigField(
|
||||
"String",
|
||||
"SIMKL_CLIENT_SECRET",
|
||||
"\"" + (System.getenv("SIMKL_CLIENT_SECRET") ?: localProperties["simkl.secret"]) + "\""
|
||||
)
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isDebuggable = false
|
||||
isMinifyEnabled = false
|
||||
isShrinkResources = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".debug"
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions.add("state")
|
||||
productFlavors {
|
||||
create("stable") {
|
||||
dimension = "state"
|
||||
}
|
||||
create("prerelease") {
|
||||
dimension = "state"
|
||||
applicationIdSuffix = ".prerelease"
|
||||
if (signingConfigs.names.contains("prerelease")) {
|
||||
signingConfig = signingConfigs.getByName("prerelease")
|
||||
} else {
|
||||
logger.warn("No prerelease signing config!")
|
||||
}
|
||||
versionNameSuffix = "-PRE"
|
||||
versionCode = (System.currentTimeMillis() / 60000).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
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"
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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
|
||||
|
||||
// Design & UI
|
||||
implementation(libs.preference.ktx)
|
||||
implementation(libs.material)
|
||||
implementation(libs.constraintlayout)
|
||||
|
||||
// Coil Image Loading
|
||||
implementation(libs.bundles.coil)
|
||||
|
||||
// Media 3 (ExoPlayer)
|
||||
implementation(libs.bundles.media3)
|
||||
implementation(libs.video)
|
||||
|
||||
// FFmpeg Decoding
|
||||
implementation(libs.bundles.nextlib)
|
||||
|
||||
// Anime-db for filler
|
||||
implementation(libs.anime.db)
|
||||
|
||||
// PlayBack
|
||||
implementation(libs.colorpicker) // Subtitle Color Picker
|
||||
implementation(libs.newpipeextractor) // For Trailers
|
||||
implementation(libs.juniversalchardet) // Subtitle Decoding
|
||||
|
||||
// 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
|
||||
|
||||
// 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)
|
||||
|
||||
// Deprecated; will be removed once extensions have time to migrate from using it
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
|
||||
// Torrent Support
|
||||
implementation(libs.torrentserver)
|
||||
|
||||
// Downloading & Networking
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.nicehttp) // HTTP Lib
|
||||
|
||||
implementation(project(":library"))
|
||||
}
|
||||
|
||||
tasks.register<Jar>("androidSourcesJar") {
|
||||
archiveClassifier.set("sources")
|
||||
from(android.sourceSets.getByName("main").java.directories) // 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")
|
||||
}
|
||||
|
||||
// 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"
|
||||
dokkaSourceSets {
|
||||
configureEach {
|
||||
suppress = name != "prereleaseDebug"
|
||||
analysisPlatform = KotlinPlatform.JVM
|
||||
displayName = "JVM"
|
||||
documentedVisibilities(
|
||||
VisibilityModifier.Public,
|
||||
VisibilityModifier.Protected
|
||||
)
|
||||
|
||||
sourceLink {
|
||||
localDirectory = file("..")
|
||||
remoteUrl("https://github.com/recloudstream/cloudstream/tree/master")
|
||||
remoteLineSuffix = "#L"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
2
app/proguard-rules.pro
vendored
|
|
@ -1,6 +1,6 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.kts.
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
|
|
|||
|
|
@ -1,58 +1,155 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.test.core.app.ActivityScenario
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.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
|
||||
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentEmulatorBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
|
||||
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
|
||||
import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.TestingUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
class TestApplication : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||
super.onCreate(savedInstanceState, persistentState)
|
||||
}
|
||||
}
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
private fun getAllProviders(): Array<MainAPI> {
|
||||
println("Providers: ${APIHolder.allProviders.size}")
|
||||
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
|
||||
//@Test
|
||||
//fun useAppContext() {
|
||||
// // Context of the app under test.
|
||||
// val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
// assertEquals("com.lagradost.cloudstream3", appContext.packageName)
|
||||
//}
|
||||
|
||||
private fun getAllProviders(): List<MainAPI> {
|
||||
return APIHolder.allProviders //.filter { !it.usesWebView }
|
||||
}
|
||||
|
||||
private suspend fun loadLinks(api: MainAPI, url: String?): Boolean {
|
||||
Assert.assertNotNull("Api ${api.name} has invalid url on episode", url)
|
||||
if (url == null) return true
|
||||
var linksLoaded = 0
|
||||
try {
|
||||
val success = api.loadLinks(url, false, {}) { link ->
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} returns link with invalid Quality",
|
||||
Qualities.values().map { it.value }.contains(link.quality)
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} returns link with invalid url ${link.url}",
|
||||
link.url.length > 4
|
||||
)
|
||||
linksLoaded++
|
||||
}
|
||||
if (success) {
|
||||
return linksLoaded > 0
|
||||
}
|
||||
Assert.assertTrue("Api ${api.name} has returns false on .loadLinks", success)
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented .loadLinks")
|
||||
}
|
||||
logError(e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private suspend fun testSingleProviderApi(api: MainAPI): Boolean {
|
||||
val searchQueries = listOf("over", "iron", "guy")
|
||||
var correctResponses = 0
|
||||
var searchResult: List<SearchResponse>? = null
|
||||
for (query in searchQueries) {
|
||||
val response = try {
|
||||
api.search(query)
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented .search")
|
||||
}
|
||||
logError(e)
|
||||
null
|
||||
}
|
||||
if (!response.isNullOrEmpty()) {
|
||||
correctResponses++
|
||||
if (searchResult == null) {
|
||||
searchResult = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (correctResponses == 0 || searchResult == null) {
|
||||
System.err.println("Api ${api.name} did not return any valid search responses")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
var validResults = false
|
||||
for (result in searchResult) {
|
||||
Assert.assertEquals(
|
||||
"Invalid apiName on response on ${api.name}",
|
||||
result.apiName,
|
||||
api.name
|
||||
)
|
||||
val load = api.load(result.url) ?: continue
|
||||
Assert.assertEquals(
|
||||
"Invalid apiName on load on ${api.name}",
|
||||
load.apiName,
|
||||
result.apiName
|
||||
)
|
||||
Assert.assertTrue(
|
||||
"Api ${api.name} on load does not contain any of the supportedTypes",
|
||||
api.supportedTypes.contains(load.type)
|
||||
)
|
||||
when (load) {
|
||||
is AnimeLoadResponse -> {
|
||||
val gotNoEpisodes =
|
||||
load.episodes.keys.isEmpty() || load.episodes.keys.any { load.episodes[it].isNullOrEmpty() }
|
||||
|
||||
if (gotNoEpisodes) {
|
||||
println("Api ${api.name} got no episodes on ${load.url}")
|
||||
continue
|
||||
}
|
||||
|
||||
val url = (load.episodes[load.episodes.keys.first()])?.first()?.data
|
||||
validResults = loadLinks(api, url)
|
||||
if (!validResults) continue
|
||||
}
|
||||
is MovieLoadResponse -> {
|
||||
val gotNoEpisodes = load.dataUrl.isBlank()
|
||||
if (gotNoEpisodes) {
|
||||
println("Api ${api.name} got no movie on ${load.url}")
|
||||
continue
|
||||
}
|
||||
|
||||
validResults = loadLinks(api, load.dataUrl)
|
||||
if (!validResults) continue
|
||||
}
|
||||
is TvSeriesLoadResponse -> {
|
||||
val gotNoEpisodes = load.episodes.isEmpty()
|
||||
if (gotNoEpisodes) {
|
||||
println("Api ${api.name} got no episodes on ${load.url}")
|
||||
continue
|
||||
}
|
||||
|
||||
validResults = loadLinks(api, load.episodes.first().data)
|
||||
if (!validResults) continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if(!validResults) {
|
||||
System.err.println("Api ${api.name} did not load on any")
|
||||
}
|
||||
|
||||
return validResults
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider has not implemented .load")
|
||||
}
|
||||
logError(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -61,89 +158,16 @@ class ExampleInstrumentedTest {
|
|||
println("Done providersExist")
|
||||
}
|
||||
|
||||
@Throws
|
||||
private inline fun <reified T : ViewBinding> testAllLayouts(
|
||||
activity: Activity,
|
||||
vararg layouts: Int
|
||||
) {
|
||||
|
||||
val bind = T::class.java.methods.first { it.name == "bind" }
|
||||
val inflater = LayoutInflater.from(activity)
|
||||
for (layout in layouts) {
|
||||
val root = inflater.inflate(layout, null, false)
|
||||
bind.invoke(null, root)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws
|
||||
fun layoutTest() {
|
||||
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
||||
scenario.onActivity { activity: MainActivity ->
|
||||
// FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same
|
||||
//testAllLayouts<FragmentHomeHeadBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
|
||||
//testAllLayouts<FragmentHomeHeadTvBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
|
||||
|
||||
// main cant be tested
|
||||
// testAllLayouts<ActivityMainTvBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
||||
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
|
||||
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv)
|
||||
|
||||
testAllLayouts<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)
|
||||
|
||||
// testAllLayouts<FragmentResultBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
|
||||
// testAllLayouts<FragmentResultTvBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
|
||||
|
||||
testAllLayouts<PlayerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
||||
testAllLayouts<PlayerCustomLayoutTvBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
||||
testAllLayouts<TrailerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
|
||||
|
||||
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||
|
||||
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
|
||||
|
||||
testAllLayouts<FragmentHomeBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
|
||||
testAllLayouts<FragmentHomeTvBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
|
||||
|
||||
testAllLayouts<FragmentSearchBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
|
||||
testAllLayouts<FragmentSearchTvBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
|
||||
|
||||
testAllLayouts<HomeResultGridBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
|
||||
//testAllLayouts<HomeResultGridExpandedBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
|
||||
|
||||
testAllLayouts<SearchResultGridExpandedBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
|
||||
testAllLayouts<SearchResultGridBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
|
||||
|
||||
|
||||
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
|
||||
|
||||
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentEmulatorBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent_emulator, R.layout.homepage_parent)
|
||||
|
||||
testAllLayouts<FragmentLibraryTvBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
||||
testAllLayouts<FragmentLibraryBinding>(activity, R.layout.fragment_library_tv, R.layout.fragment_library)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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",
|
||||
|
|
@ -156,20 +180,66 @@ class ExampleInstrumentedTest {
|
|||
@Test
|
||||
fun providerCorrectHomepage() {
|
||||
runBlocking {
|
||||
getAllProviders().toList().amap { api ->
|
||||
TestingUtils.testHomepage(api, TestingUtils.Logger())
|
||||
getAllProviders().apmap { api ->
|
||||
if (api.hasMainPage) {
|
||||
try {
|
||||
val homepage = api.getMainPage()
|
||||
when {
|
||||
homepage == null -> {
|
||||
System.err.println("Homepage provider ${api.name} did not correctly load homepage!")
|
||||
}
|
||||
homepage.items.isEmpty() -> {
|
||||
System.err.println("Homepage provider ${api.name} does not contain any items!")
|
||||
}
|
||||
homepage.items.any { it.list.isEmpty() } -> {
|
||||
System.err.println ("Homepage provider ${api.name} does not have any items on result!")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e.cause is NotImplementedError) {
|
||||
Assert.fail("Provider marked as hasMainPage, while in reality is has not been implemented")
|
||||
}
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
println("Done providerCorrectHomepage")
|
||||
}
|
||||
|
||||
// @Test
|
||||
// fun testSingleProvider() {
|
||||
// testSingleProviderApi(ThenosProvider())
|
||||
// }
|
||||
|
||||
@Test
|
||||
fun testAllProvidersCorrect() {
|
||||
fun providerCorrect() {
|
||||
runBlocking {
|
||||
TestingUtils.getDeferredProviderTests(
|
||||
this,
|
||||
getAllProviders(),
|
||||
) { _, _ -> }
|
||||
val invalidProvider = ArrayList<Pair<MainAPI, Exception?>>()
|
||||
val providers = getAllProviders()
|
||||
providers.apmap { api ->
|
||||
try {
|
||||
println("Trying $api")
|
||||
if (testSingleProviderApi(api)) {
|
||||
println("Success $api")
|
||||
} else {
|
||||
System.err.println("Error $api")
|
||||
invalidProvider.add(Pair(api, null))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
invalidProvider.add(Pair(api, e))
|
||||
}
|
||||
}
|
||||
if(invalidProvider.isEmpty()) {
|
||||
println("No Invalid providers! :D")
|
||||
} else {
|
||||
println("Invalid providers are: ")
|
||||
for (provider in invalidProvider) {
|
||||
println("${provider.first}")
|
||||
}
|
||||
}
|
||||
}
|
||||
println("Done providerCorrect")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 9 KiB |
|
|
@ -1,68 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.lagradost.cloudstream3">
|
||||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- I dont remember, probs has to do with downloads -->
|
||||
<uses-permission android:name="android.permission.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="com.android.providers.tv.permission.WRITE_EPG_DATA" /> not used atm, but code exist that requires it that are not run -->
|
||||
<!-- <permission android:name="android.permission.QUERY_ALL_PACKAGES" /> <!– Used for getting if vlc is installed –> -->
|
||||
<!-- Fixes android tv fuckery -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
|
|
@ -71,24 +21,20 @@
|
|||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<!-- 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 +50,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,65 +68,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>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="cloudstreamplayer" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
|
@ -206,37 +109,6 @@
|
|||
<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" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="cloudstreamsearch" />
|
||||
</intent-filter>
|
||||
|
||||
<!--
|
||||
Allow opening from continue watching with intents: cloudstreamsearch://1234
|
||||
Used on Android TV Watch Next
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="cloudstreamcontinuewatching" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter android:autoVerify="false">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="cs.repo"
|
||||
android:pathPrefix="/"
|
||||
|
|
@ -244,11 +116,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,30 +132,12 @@
|
|||
<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:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
|
|
@ -291,5 +149,5 @@
|
|||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 13 KiB |
|
|
@ -1,78 +1,152 @@
|
|||
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.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.auto.service.AutoService
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
|
||||
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.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.lang.ref.WeakReference
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@Deprecated(
|
||||
message = "AcraApplication is deprecated, use CloudStreamApp instead",
|
||||
replaceWith = ReplaceWith("com.lagradost.cloudstream3.CloudStreamApp.context"),
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
val context get() = CloudStreamApp.context
|
||||
class CustomReportSender : ReportSender {
|
||||
// Sends all your crashes to google forms
|
||||
override fun send(context: Context, errorContent: CrashReportData) {
|
||||
println("Sending report")
|
||||
val url =
|
||||
"https://docs.google.com/forms/u/0/d/e/1FAIpQLSeFmyBChi6HF3IkhTVWPiDXJtxt8W0Hf4Agljm_0-0_QuEYFg/formResponse"
|
||||
val data = mapOf(
|
||||
"entry.134906550" to errorContent.toJSON()
|
||||
)
|
||||
|
||||
@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)
|
||||
thread { // to not run it on main thread
|
||||
runBlocking {
|
||||
suspendSafeApiCall {
|
||||
val post = app.post(url, data = data)
|
||||
println("Report response: $post")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
@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 AcraApplication : Application() {
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
context = base
|
||||
|
||||
initAcra {
|
||||
//core configuration:
|
||||
buildConfigClass = BuildConfig::class.java
|
||||
reportFormat = StringFormat.JSON
|
||||
|
||||
reportContent = arrayOf(
|
||||
ReportField.BUILD_CONFIG, ReportField.USER_CRASH_DATE,
|
||||
ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL,
|
||||
ReportField.STACK_TRACE
|
||||
)
|
||||
|
||||
// removed this due to bug when starting the app, moved it to when it actually crashes
|
||||
//each plugin you chose above can be configured in a block like this:
|
||||
/*toast {
|
||||
text = getString(R.string.acra_report_toast)
|
||||
//opening this block automatically enables the plugin.
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/** Use to get activity from Context */
|
||||
tailrec fun Context.getActivity(): Activity? = this as? Activity
|
||||
?: (this as? ContextWrapper)?.baseContext?.getActivity()
|
||||
|
||||
private var _context: WeakReference<Context>? = null
|
||||
var context
|
||||
get() = _context?.get()
|
||||
private set(value) {
|
||||
_context = WeakReference(value)
|
||||
}
|
||||
|
||||
fun removeKeys(folder: String): Int? {
|
||||
return context?.removeKeys(folder)
|
||||
}
|
||||
|
||||
fun <T> setKey(path: String, value: T) {
|
||||
context?.setKey(path, value)
|
||||
}
|
||||
|
||||
fun <T> setKey(folder: String, path: String, value: T) {
|
||||
context?.setKey(folder, path, value)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> getKey(path: String, defVal: T?): T? {
|
||||
return context?.getKey(path, defVal)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> getKey(path: String): T? {
|
||||
return context?.getKey(path)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> getKey(folder: String, path: String): T? {
|
||||
return context?.getKey(folder, path)
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> getKey(folder: String, path: String, defVal: T?): T? {
|
||||
return context?.getKey(folder, path, defVal)
|
||||
}
|
||||
|
||||
fun getKeys(folder: String): List<String>? {
|
||||
return context?.getKeys(folder)
|
||||
}
|
||||
|
||||
fun removeKey(folder: String, path: String) {
|
||||
context?.removeKey(folder, path)
|
||||
}
|
||||
|
||||
fun removeKey(path: String) {
|
||||
context?.removeKey(path)
|
||||
}
|
||||
|
||||
/**
|
||||
* If fallbackWebview is true and a fragment is supplied then it will open a webview with the url if the browser fails.
|
||||
* */
|
||||
fun openBrowser(url: String, fallbackWebview: Boolean = false, fragment: Fragment? = null) {
|
||||
context?.openBrowser(url, fallbackWebview, fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,151 +1,53 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.View
|
||||
import android.view.View.NO_ID
|
||||
import android.view.ViewGroup
|
||||
import android.view.*
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.StringRes
|
||||
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.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.UiText
|
||||
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 java.lang.ref.WeakReference
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
|
||||
enum class FocusDirection {
|
||||
Start,
|
||||
End,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
import java.util.*
|
||||
|
||||
object CommonActivity {
|
||||
|
||||
private var _activity: WeakReference<Activity>? = null
|
||||
var activity
|
||||
get() = _activity?.get()
|
||||
private set(value) {
|
||||
_activity = WeakReference(value)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun setActivityInstance(newActivity: Activity?) {
|
||||
activity = newActivity
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun Activity?.getCastSession(): CastSession? {
|
||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||
}
|
||||
|
||||
val displayMetrics: DisplayMetrics = Resources.getSystem().displayMetrics
|
||||
|
||||
// screenWidth and screenHeight does always
|
||||
// refer to the screen while in landscape mode
|
||||
val screenWidth: Int
|
||||
get() {
|
||||
return max(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||
}
|
||||
val screenHeight: Int
|
||||
get() {
|
||||
return min(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
||||
}
|
||||
val screenWidthWithOrientation: Int
|
||||
get() {
|
||||
return displayMetrics.widthPixels
|
||||
}
|
||||
val screenHeightWithOrientation: Int
|
||||
get() {
|
||||
return displayMetrics.heightPixels
|
||||
}
|
||||
|
||||
var isPipDesired: Boolean = false
|
||||
var 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
|
||||
|
||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
act.runOnUiThread {
|
||||
showToast(act, act.getString(message), duration)
|
||||
}
|
||||
}
|
||||
|
||||
fun showToast(message: String?, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
act.runOnUiThread {
|
||||
showToast(act, message, duration)
|
||||
}
|
||||
}
|
||||
|
||||
fun showToast(message: UiText?, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
if (message == null) return
|
||||
act.runOnUiThread {
|
||||
showToast(act, message.asString(act), duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainThread
|
||||
var currentToast: Toast? = null
|
||||
|
||||
fun showToast(act: Activity?, text: UiText, duration: Int) {
|
||||
if (act == null) return
|
||||
text.asStringNull(act)?.let {
|
||||
|
|
@ -153,9 +55,7 @@ object CommonActivity {
|
|||
}
|
||||
}
|
||||
|
||||
/** duration is Toast.LENGTH_SHORT if null*/
|
||||
@MainThread
|
||||
fun showToast(act: Activity?, @StringRes message: Int, duration: Int? = null) {
|
||||
fun showToast(act: Activity?, @StringRes message: Int, duration: Int) {
|
||||
if (act == null) return
|
||||
showToast(act, act.getString(message), duration)
|
||||
}
|
||||
|
|
@ -163,7 +63,6 @@ object CommonActivity {
|
|||
const val TAG = "COMPACT"
|
||||
|
||||
/** duration is Toast.LENGTH_SHORT if null*/
|
||||
@MainThread
|
||||
fun showToast(act: Activity?, message: String?, duration: Int? = null) {
|
||||
if (act == null || message == null) {
|
||||
Log.w(TAG, "invalid showToast act = $act message = $message")
|
||||
|
|
@ -176,50 +75,33 @@ 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)
|
||||
fun setLocale(context: Context?, languageCode: String?) {
|
||||
if (context == null || languageCode == null) return
|
||||
val locale = Locale(languageCode)
|
||||
val resources: Resources = context.resources
|
||||
val config = resources.configuration
|
||||
Locale.setDefault(locale)
|
||||
|
|
@ -227,12 +109,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,64 +118,31 @@ object CommonActivity {
|
|||
setLocale(this, localeCode)
|
||||
}
|
||||
|
||||
fun init(act: Activity) {
|
||||
setActivityInstance(act)
|
||||
ioSafe { Torrent.deleteAllFiles() }
|
||||
val componentActivity = activity as? ComponentActivity ?: return
|
||||
fun init(act: Activity?) {
|
||||
if (act == null) return
|
||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||
canShowPipMode =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT
|
||||
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
|
||||
|
||||
act.updateLocale()
|
||||
|
||||
componentActivity.updateLocale()
|
||||
componentActivity.updateTv()
|
||||
AccountManager.initMainAPI()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for notification permissions on Android 13
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(
|
||||
componentActivity,
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
val requestPermissionLauncher = componentActivity.registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { isGranted: Boolean ->
|
||||
Log.d(TAG, "Notification permission: $isGranted")
|
||||
}
|
||||
requestPermissionLauncher.launch(
|
||||
Manifest.permission.POST_NOTIFICATIONS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 +151,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,33 +163,22 @@ 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
|
||||
"AmoledLight" -> R.style.AmoledModeLight
|
||||
"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,230 +187,204 @@ 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
|
||||
|
||||
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
||||
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
|
||||
|
||||
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
|
||||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||
}
|
||||
|
||||
/** because we want closes find, aka when multiple have the same id, we go to parent
|
||||
until the correct one is found */
|
||||
private fun localLook(from: View, id: Int): View? {
|
||||
if (id == NO_ID) return null
|
||||
var currentLook: View = from
|
||||
// limit to 15 look depth
|
||||
for (i in 0..15) {
|
||||
currentLook.findViewById<View?>(id)?.let { return it }
|
||||
currentLook = (currentLook.parent as? View) ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
/*var currentLook: View = view
|
||||
while (true) {
|
||||
val tmpNext = currentLook.findViewById<View?>(nextId)
|
||||
if (tmpNext != null) {
|
||||
next = tmpNext
|
||||
break
|
||||
}
|
||||
currentLook = currentLook.parent as? View ?: break
|
||||
}*/
|
||||
|
||||
private fun View.hasContent(): Boolean {
|
||||
return isShown && when (this) {
|
||||
is ViewGroup -> this.isNotEmpty()
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
/** skips the initial stage of searching for an id using the view, see getNextFocus for specification */
|
||||
fun continueGetNextFocus(
|
||||
root: Any?,
|
||||
view: View,
|
||||
direction: FocusDirection,
|
||||
nextId: Int,
|
||||
depth: Int = 0
|
||||
): View? {
|
||||
if (nextId == NO_ID) return null
|
||||
|
||||
// do an initial search for the view, in case the localLook is too deep we can use this as
|
||||
// an early break and backup view
|
||||
var next =
|
||||
when (root) {
|
||||
is Activity -> root.findViewById(nextId)
|
||||
is View -> root.rootView.findViewById<View?>(nextId)
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
||||
next = localLook(view, nextId) ?: next
|
||||
val shown = next.hasContent()
|
||||
|
||||
// if cant focus but visible then break and let android decide
|
||||
// the exception if is the view is a parent and has children that wants focus
|
||||
val hasChildrenThatWantsFocus = (next as? ViewGroup)?.let { parent ->
|
||||
parent.descendantFocusability == ViewGroup.FOCUS_AFTER_DESCENDANTS && parent.isNotEmpty()
|
||||
} ?: false
|
||||
if (!next.isFocusable && shown && !hasChildrenThatWantsFocus) return null
|
||||
|
||||
// if not shown then continue because we will "skip" over views to get to a replacement
|
||||
if (!shown) {
|
||||
// we don't want a while true loop, so we let android decide if we find a recursive view
|
||||
if (next == view) return null
|
||||
return getNextFocus(root, next, direction, depth + 1)
|
||||
}
|
||||
|
||||
(when (next) {
|
||||
is ChipGroup -> {
|
||||
next.children.firstOrNull { it.isFocusable && it.isShown }
|
||||
}
|
||||
|
||||
is NavigationRailView -> {
|
||||
next.findViewById(next.selectedItemId) ?: next.findViewById(R.id.navigation_home)
|
||||
}
|
||||
|
||||
else -> null
|
||||
})?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
// nothing wrong with the view found, return it
|
||||
return next
|
||||
}
|
||||
|
||||
/** recursively looks for a next focus up to a depth of 10,
|
||||
* this is used to override the normal shit focus system
|
||||
* because this application has a lot of invisible views that messes with some tv devices*/
|
||||
fun getNextFocus(
|
||||
root: Any?,
|
||||
private fun getNextFocus(
|
||||
act: Activity?,
|
||||
view: View?,
|
||||
direction: FocusDirection,
|
||||
depth: Int = 0
|
||||
): View? {
|
||||
// if input is invalid let android decide + depth test to not crash if loop is found
|
||||
if (view == null || depth >= 10 || root == null) {
|
||||
): Int? {
|
||||
if (view == null || depth >= 10 || act == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
var nextId = when (direction) {
|
||||
FocusDirection.Start -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusRightId
|
||||
else
|
||||
view.nextFocusLeftId
|
||||
val nextId = when (direction) {
|
||||
FocusDirection.Left -> {
|
||||
view.nextFocusLeftId
|
||||
}
|
||||
|
||||
FocusDirection.Up -> {
|
||||
view.nextFocusUpId
|
||||
}
|
||||
|
||||
FocusDirection.End -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusLeftId
|
||||
else
|
||||
view.nextFocusRightId
|
||||
FocusDirection.Right -> {
|
||||
view.nextFocusRightId
|
||||
}
|
||||
|
||||
FocusDirection.Down -> {
|
||||
view.nextFocusDownId
|
||||
}
|
||||
}
|
||||
|
||||
if (nextId == NO_ID) {
|
||||
// if not specified then use forward id
|
||||
nextId = view.nextFocusForwardId
|
||||
// if view is still not found to next focus then return and let android decide
|
||||
if (nextId == NO_ID)
|
||||
return null
|
||||
return if (nextId != -1) {
|
||||
val next = act.findViewById<View?>(nextId)
|
||||
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
|
||||
|
||||
if (next?.isShown == false) {
|
||||
getNextFocus(act, next, direction, depth + 1)
|
||||
} else {
|
||||
if (depth == 0) {
|
||||
null
|
||||
} else {
|
||||
nextId
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return continueGetNextFocus(root, view, direction, nextId, depth)
|
||||
}
|
||||
|
||||
|
||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?): Boolean? {
|
||||
return null
|
||||
enum class FocusDirection {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
fun onKeyDown(act: Activity?, keyCode: Int, event: KeyEvent?) {
|
||||
//println("Keycode: $keyCode")
|
||||
//showToast(
|
||||
// this,
|
||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||
// Toast.LENGTH_LONG
|
||||
//)
|
||||
|
||||
// Tested keycodes on remote:
|
||||
// KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||
// KeyEvent.KEYCODE_MEDIA_REWIND
|
||||
// KeyEvent.KEYCODE_MENU
|
||||
// KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
|
||||
// KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
|
||||
// 149 keycode_numpad 5
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||
PlayerEventType.SeekForward
|
||||
}
|
||||
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||
PlayerEventType.SeekBack
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
|
||||
PlayerEventType.NextEpisode
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
|
||||
PlayerEventType.PrevEpisode
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||
PlayerEventType.Pause
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
|
||||
PlayerEventType.Play
|
||||
}
|
||||
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7 -> {
|
||||
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 -> {
|
||||
PlayerEventType.ShowMirrors
|
||||
}
|
||||
// OpenSubtitles shortcut
|
||||
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8 -> {
|
||||
PlayerEventType.SearchSubtitlesOnline
|
||||
}
|
||||
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3 -> {
|
||||
PlayerEventType.ShowSpeed
|
||||
}
|
||||
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0 -> {
|
||||
PlayerEventType.Resize
|
||||
}
|
||||
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4 -> {
|
||||
PlayerEventType.SkipOp
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
|
||||
PlayerEventType.PlayPauseToggle
|
||||
}
|
||||
else -> null
|
||||
}?.let { playerEvent ->
|
||||
playerEventListener?.invoke(playerEvent)
|
||||
}
|
||||
|
||||
//when (keyCode) {
|
||||
// KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
// println("DPAD PRESSED")
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
/** overrides focus and custom key events */
|
||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||
if (act == null) return null
|
||||
val currentFocus = act.currentFocus
|
||||
|
||||
event?.keyCode?.let { keyCode ->
|
||||
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
|
||||
val nextView = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Start
|
||||
)
|
||||
when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
if (act.currentFocus != null) {
|
||||
val next = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Left
|
||||
)
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Right
|
||||
)
|
||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Up
|
||||
)
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Down
|
||||
)
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.End
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Up
|
||||
)
|
||||
if (next != null && next != -1) {
|
||||
val nextView = act.findViewById<View?>(next)
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus()
|
||||
keyEventListener?.invoke(Pair(event, true))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Down
|
||||
)
|
||||
|
||||
else -> null
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
|
||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//println("Keycode: $keyCode")
|
||||
//showToast(
|
||||
// this,
|
||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||
// Toast.LENGTH_LONG
|
||||
//)
|
||||
}
|
||||
}
|
||||
|
||||
// println("NEXT FOCUS : $nextView")
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus()
|
||||
keyEventListener?.invoke(Pair(event, true))
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO: Figure out why removing the check for SearchAutoComplete seems
|
||||
// to break focus on TV as it shouldn't need to be used.
|
||||
// Also handle KEYCODE_ENTER here because some remotes (e.g. LG Magic Remote)
|
||||
// send KEYCODE_ENTER instead of KEYCODE_DPAD_CENTER when clicking the OK button.
|
||||
@SuppressLint("RestrictedApi")
|
||||
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) &&
|
||||
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||
) {
|
||||
showInputMethod(act.currentFocus?.findFocus())
|
||||
}
|
||||
|
||||
//println("Keycode: $keyCode")
|
||||
//showToast(
|
||||
// this,
|
||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||
// Toast.LENGTH_LONG
|
||||
//)
|
||||
}
|
||||
|
||||
// if someone else want to override the focus then don't handle the event as it is already
|
||||
// consumed. used in video player
|
||||
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
||||
return true
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -51,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
|||
|
||||
companion object {
|
||||
private const val USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
||||
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
|
||||
private var instance: DownloaderTestImpl? = null
|
||||
|
||||
/**
|
||||
|
|
@ -74,4 +73,8 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
|
|||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
client = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||
}
|
||||
}
|
||||
1414
app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
//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)
|
||||
}*/
|
||||
|
||||
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() }
|
||||
}
|
||||
|
||||
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() }
|
||||
}
|
||||
|
||||
// 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 com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.base64Decode
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
||||
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,
|
||||
headers = mapOf("range" to "bytes=0-")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
class AsianLoad : ExtractorApi() {
|
||||
override var name = "AsianLoad"
|
||||
override var mainUrl = "https://asianembed.io"
|
||||
override val requiresReferer = true
|
||||
|
||||
private val sourceRegex = Regex("""sources:[\W\w]*?file:\s*?["'](.*?)["']""")
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||
with(app.get(url, referer = referer)) {
|
||||
sourceRegex.findAll(this.text).forEach { sourceMatch ->
|
||||
val extractedUrl = sourceMatch.groupValues[1]
|
||||
// Trusting this isn't mp4, may fuck up stuff
|
||||
if (URI(extractedUrl).path.endsWith(".m3u8")) {
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
extractedUrl,
|
||||
url,
|
||||
headers = mapOf("referer" to this.url)
|
||||
).forEach { link ->
|
||||
extractedLinksList.add(link)
|
||||
}
|
||||
} else if (extractedUrl.endsWith(".mp4")) {
|
||||
extractedLinksList.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
extractedUrl,
|
||||
url.replace(" ", "%20"),
|
||||
getQualityFromName(sourceMatch.groupValues[2]),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return extractedLinksList
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.lagradost.cloudstream3.app
|
|||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
||||
open class Blogger : ExtractorApi() {
|
||||
class Blogger : ExtractorApi() {
|
||||
override val name = "Blogger"
|
||||
override val mainUrl = "https://www.blogger.com"
|
||||
override val requiresReferer = false
|
||||
|
|
@ -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
|
||||
|
||||
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,64 @@
|
|||
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 DoodCxExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.cx"
|
||||
}
|
||||
|
||||
class DoodShExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.sh"
|
||||
}
|
||||
class DoodWatchExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.watch"
|
||||
}
|
||||
|
||||
class DoodPmExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.pm"
|
||||
}
|
||||
|
||||
class DoodToExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.to"
|
||||
}
|
||||
|
||||
class DoodSoExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.so"
|
||||
}
|
||||
|
||||
class DoodWsExtractor : DoodLaExtractor() {
|
||||
override var mainUrl = "https://dood.ws"
|
||||
}
|
||||
|
||||
|
||||
open class DoodLaExtractor : ExtractorApi() {
|
||||
override var name = "DoodStream"
|
||||
override var mainUrl = "https://dood.la"
|
||||
override val requiresReferer = false
|
||||
|
||||
override fun getExtractorUrl(id: String): String {
|
||||
return "$mainUrl/d/$id"
|
||||
}
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val response0 = app.get(url).text // html of DoodStream page to look for /pass_md5/...
|
||||
val md5 =mainUrl+(Regex("/pass_md5/[^']*").find(response0)?.value ?: return null) // get https://dood.ws/pass_md5/...
|
||||
val trueUrl = app.get(md5, referer = url).text + "zUEJeL3mUN?token=" + md5.substringAfterLast("/") //direct link to extract (zUEJeL3mUN is random)
|
||||
val quality = Regex("\\d{3,4}p").find(response0.substringAfter("<title>").substringBefore("</title>"))?.groupValues?.get(0)
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
trueUrl,
|
||||
this.name,
|
||||
trueUrl,
|
||||
mainUrl,
|
||||
getQualityFromName(quality),
|
||||
false
|
||||
)
|
||||
) // links are valid in 8h
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -16,21 +16,39 @@ open class Evoload : ExtractorApi() {
|
|||
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val id = url.replace("https://evoload.io/e/", "") // wanted media id
|
||||
val lang = url.substring(0, 2)
|
||||
val flag =
|
||||
if (lang == "vo") {
|
||||
" \uD83C\uDDEC\uD83C\uDDE7"
|
||||
}
|
||||
else if (lang == "vf"){
|
||||
" \uD83C\uDDE8\uD83C\uDDF5"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
val cleaned_url = if (lang == "ht") { // if url doesn't contain a flag and the url starts with http://
|
||||
url
|
||||
} else {
|
||||
url.substring(2, url.length)
|
||||
}
|
||||
//println(lang)
|
||||
//println(cleaned_url)
|
||||
|
||||
val id = cleaned_url.replace("https://evoload.io/e/", "") // wanted media id
|
||||
val csrv_token = app.get("https://csrv.evosrv.com/captcha?m412548=").text // whatever that is
|
||||
val captchaPass = app.get("https://cd2.evosrv.com/html/jsx/e.jsx").text.take(300).split("captcha_pass = '")[1].split("\'")[0] //extract the captcha pass from the js response (located in the 300 first chars)
|
||||
val payload = mapOf("code" to id, "csrv_token" to csrv_token, "pass" to captchaPass)
|
||||
val r = app.post("https://evoload.io/SecurePlayer", data=(payload)).text
|
||||
val link = Regex("src\":\"(.*?)\"").find(r)?.destructured?.component1() ?: return listOf()
|
||||
return listOf(
|
||||
newExtractorLink(
|
||||
name,
|
||||
ExtractorLink(
|
||||
name,
|
||||
name + flag,
|
||||
link,
|
||||
) {
|
||||
this.referer = url
|
||||
this.quality = Qualities.Unknown.value
|
||||
}
|
||||
cleaned_url,
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.apmap
|
||||
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
|
||||
|
||||
class Fastream: ExtractorApi() {
|
||||
override var mainUrl = "https://fastream.to"
|
||||
override var name = "Fastream"
|
||||
override val requiresReferer = false
|
||||
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val id = Regex("emb\\.html\\?(.*)\\=(enc|)").find(url)?.destructured?.component1() ?: return emptyList()
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
val response = app.post("$mainUrl/dl",
|
||||
data = mapOf(
|
||||
Pair("op","embed"),
|
||||
Pair("file_code",id),
|
||||
Pair("auto","1")
|
||||
)).document
|
||||
response.select("script").apmap { script ->
|
||||
if (script.data().contains("sources")) {
|
||||
val m3u8regex = Regex("((https:|http:)\\/\\/.*\\.m3u8)")
|
||||
val m3u8 = m3u8regex.find(script.data())?.value ?: return@apmap
|
||||
generateM3u8(
|
||||
name,
|
||||
m3u8,
|
||||
mainUrl
|
||||
).forEach { link ->
|
||||
sources.add(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
||||
class Filesim : ExtractorApi() {
|
||||
override val name = "Filesim"
|
||||
override val mainUrl = "https://files.im"
|
||||
override val requiresReferer = false
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
with(app.get(url).document) {
|
||||
this.select("script").map { script ->
|
||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||
val data = getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
|
||||
tryParseJson<List<ResponseSource>>("[$data]")?.map {
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
it.file,
|
||||
"$mainUrl/",
|
||||
).forEach { m3uData -> sources.add(m3uData) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private data class ResponseSource(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("label") val label: String?
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
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
|
||||
|
||||
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 M3u8Helper.generateM3u8(
|
||||
name,
|
||||
m3u8,
|
||||
ref,
|
||||
headers = mapOf("accept" to "*/*")
|
||||
)
|
||||
}
|
||||
|
||||
private data class GmResponse(
|
||||
val videoSource: String? = null
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
|
||||
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>,
|
||||
)
|
||||
|
||||
data class GuardareData (
|
||||
@JsonProperty("file") val file : String,
|
||||
@JsonProperty("label") val label : String,
|
||||
@JsonProperty("type") val type : String
|
||||
)
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val response = app.post(url.replace("/v/","/api/source/"), data = mapOf("d" to mainUrl)).text
|
||||
val jsonvideodata = AppUtils.parseJson<GuardareJsonData>(response)
|
||||
return jsonvideodata.data.map {
|
||||
ExtractorLink(
|
||||
it.file+".${it.type}",
|
||||
this.name,
|
||||
it.file+".${it.type}",
|
||||
mainUrl,
|
||||
it.label.filter{ it.isDigit() }.toInt(),
|
||||
false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.*
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
|
||||
class Neonime7n : Hxfile() {
|
||||
override val name = "Neonime7n"
|
||||
override val mainUrl = "https://7njctn.neonime.watch"
|
||||
override val redirect = false
|
||||
}
|
||||
|
||||
class Neonime8n : Hxfile() {
|
||||
override val name = "Neonime8n"
|
||||
override val mainUrl = "https://8njctn.neonime.net"
|
||||
override val redirect = false
|
||||
}
|
||||
|
||||
class KotakAnimeid : Hxfile() {
|
||||
override val name = "KotakAnimeid"
|
||||
override val mainUrl = "https://kotakanimeid.com"
|
||||
override val requiresReferer = true
|
||||
}
|
||||
|
||||
class Yufiles : Hxfile() {
|
||||
override val name = "Yufiles"
|
||||
override val mainUrl = "https://yufiles.com"
|
||||
}
|
||||
|
||||
class Aico : Hxfile() {
|
||||
override val name = "Aico"
|
||||
override val mainUrl = "https://aico.pw"
|
||||
}
|
||||
|
||||
open class Hxfile : ExtractorApi() {
|
||||
override val name = "Hxfile"
|
||||
override val mainUrl = "https://hxfile.co"
|
||||
override val requiresReferer = false
|
||||
open val redirect = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
val document = app.get(url, allowRedirects = redirect, referer = referer).document
|
||||
with(document) {
|
||||
this.select("script").map { script ->
|
||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||
val data =
|
||||
getAndUnpack(script.data()).substringAfter("sources:[").substringBefore("]")
|
||||
tryParseJson<List<ResponseSource>>("[$data]")?.map {
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
it.file,
|
||||
referer = mainUrl,
|
||||
quality = when {
|
||||
url.contains("hxfile.co") -> getQualityFromName(
|
||||
Regex("\\d\\.(.*?).mp4").find(
|
||||
document.select("title").text()
|
||||
)?.groupValues?.get(1).toString()
|
||||
)
|
||||
else -> getQualityFromName(it.label)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (script.data().contains("\"sources\":[")) {
|
||||
val data = script.data().substringAfter("\"sources\":[").substringBefore("]")
|
||||
tryParseJson<List<ResponseSource>>("[$data]")?.map {
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
it.file,
|
||||
referer = mainUrl,
|
||||
quality = when {
|
||||
it.label?.contains("HD") == true -> Qualities.P720.value
|
||||
it.label?.contains("SD") == true -> Qualities.P480.value
|
||||
else -> getQualityFromName(it.label)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private data class ResponseSource(
|
||||
@JsonProperty("file") val file: String,
|
||||
@JsonProperty("type") val type: String?,
|
||||
@JsonProperty("label") val label: String?
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
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.getQualityFromName
|
||||
|
||||
class Linkbox : ExtractorApi() {
|
||||
override val name = "Linkbox"
|
||||
override val mainUrl = "https://www.linkbox.to"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val id = url.substringAfter("id=")
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
|
||||
app.get("$mainUrl/api/open/get_url?itemId=$id", referer=url).parsedSafe<Responses>()?.data?.rList?.map { link ->
|
||||
sources.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link.url,
|
||||
url,
|
||||
getQualityFromName(link.resolution)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
data class RList(
|
||||
@JsonProperty("url") val url: String,
|
||||
@JsonProperty("resolution") val resolution: String?,
|
||||
)
|
||||
|
||||
data class Data(
|
||||
@JsonProperty("rList") val rList: List<RList>?,
|
||||
)
|
||||
|
||||
data class Responses(
|
||||
@JsonProperty("data") val data: Data?,
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
open class Mcloud : WcoStream() {
|
||||
override var name = "Mcloud"
|
||||
override var mainUrl = "https://mcloud.to"
|
||||
override val requiresReferer = true
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||
|
||||
class Mp4Upload : ExtractorApi() {
|
||||
override var name = "Mp4Upload"
|
||||
override var mainUrl = "https://www.mp4upload.com"
|
||||
private val srcRegex = Regex("""player\.src\("(.*?)"""")
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink>? {
|
||||
with(app.get(url)) {
|
||||
getAndUnpack(this.text).let { unpackedText ->
|
||||
val quality = unpackedText.lowercase().substringAfter(" height=").substringBefore(" ").toIntOrNull()
|
||||
srcRegex.find(unpackedText)?.groupValues?.get(1)?.let { link ->
|
||||
return listOf(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name,
|
||||
link,
|
||||
url,
|
||||
quality ?: Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
class MultiQuality : ExtractorApi() {
|
||||
override var name = "MultiQuality"
|
||||
override var mainUrl = "https://gogo-play.net"
|
||||
private val sourceRegex = Regex("""file:\s*['"](.*?)['"],label:\s*['"](.*?)['"]""")
|
||||
private val m3u8Regex = Regex(""".*?(\d*).m3u8""")
|
||||
private val urlRegex = Regex("""(.*?)([^/]+$)""")
|
||||
override val requiresReferer = false
|
||||
|
||||
override fun getExtractorUrl(id: String): String {
|
||||
return "$mainUrl/loadserver.php?id=$id"
|
||||
}
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val extractedLinksList: MutableList<ExtractorLink> = mutableListOf()
|
||||
with(app.get(url)) {
|
||||
sourceRegex.findAll(this.text).forEach { sourceMatch ->
|
||||
val extractedUrl = sourceMatch.groupValues[1]
|
||||
// Trusting this isn't mp4, may fuck up stuff
|
||||
if (URI(extractedUrl).path.endsWith(".m3u8")) {
|
||||
with(app.get(extractedUrl)) {
|
||||
m3u8Regex.findAll(this.text).forEach { match ->
|
||||
extractedLinksList.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
name = name,
|
||||
urlRegex.find(this.url)!!.groupValues[1] + match.groupValues[0],
|
||||
url,
|
||||
getQualityFromName(match.groupValues[1]),
|
||||
isM3u8 = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
} else if (extractedUrl.endsWith(".mp4")) {
|
||||
extractedLinksList.add(
|
||||
ExtractorLink(
|
||||
name,
|
||||
"$name ${sourceMatch.groupValues[2]}",
|
||||
extractedUrl,
|
||||
url.replace(" ", "%20"),
|
||||
Qualities.Unknown.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
return extractedLinksList
|
||||
}
|
||||
}
|
||||
}
|
||||