Enable multisrc support (#1352)

* Re-add multisrc support

* Convert SFlix and DopeBox to multisrc

* Re-enable multisrc tasks on workflows
This commit is contained in:
Claudemirovsky 2023-03-02 08:37:11 -03:00 committed by GitHub
parent d42f5c6cc6
commit ec19a31c3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 756 additions and 1209 deletions

View File

@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
outputs:
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
#multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
isIndividualChanged: ${{ steps.parse-changed-files.outputs.isIndividualChanged }}
#isMultisrcChanged: ${{ steps.parse-changed-files.outputs.isMultisrcChanged }}
isMultisrcChanged: ${{ steps.parse-changed-files.outputs.isMultisrcChanged }}
env:
CI_MODULE_GEN: true
steps:
@ -44,34 +44,36 @@ jobs:
name: Parse changed files
run: |
isIndividualChanged=0
#isMultisrcChanged=0
isMultisrcChanged=0
for changedFile in ${{ steps.get-changed-files.outputs.all }}; do
if [[ ${changedFile} == src/* ]]; then
isIndividualChanged=1
# elif [[ ${changedFile} == multisrc/* ]]; then
# isMultisrcChanged=1
elif [[ ${changedFile} == multisrc/* ]]; then
isMultisrcChanged=1
elif [[ ${changedFile} == *.md ]]; then
true
else
isIndividualChanged=1
# isMultisrcChanged=1
isMultisrcChanged=1
break
fi
done
echo "::set-output name=isIndividualChanged::$isIndividualChanged"
#echo "::set-output name=isMultisrcChanged::$isMultisrcChanged"
echo "isIndividualChanged=$isIndividualChanged" >> $GITHUB_OUTPUT
echo "isMultisrcChanged=$isMultisrcChanged" >> $GITHUB_OUTPUT
# - name: Generate multisrc sources
# if: ${{ steps.parse-changed-files.outputs.isMultisrcChanged == '1' }}
# uses: gradle/gradle-command-action@v2
# with:
# arguments: :multisrc:generateExtensions
- name: Generate multisrc sources
if: ${{ steps.parse-changed-files.outputs.isMultisrcChanged == '1' }}
uses: gradle/gradle-command-action@v2
with:
arguments: :multisrc:generateExtensions
- name: Get number of modules
run: |
set -x
./gradlew -q projects | grep '.*extensions\:individual\:.*\:.*' > projects.txt
./gradlew -q projects | grep '.*extensions\:\(individual\|multisrc\)\:.*\:.*' > projects.txt
echo "NUM_INDIVIDUAL_MODULES=$(cat projects.txt | grep '.*\:individual\:.*' | wc -l)" >> $GITHUB_ENV
#echo "NUM_MULTISRC_MODULES=$(cat projects.txt | grep '.*\:multisrc\:.*' | wc -l)" >> $GITHUB_ENV
echo "NUM_MULTISRC_MODULES=$(cat projects.txt | grep '.*\:multisrc\:.*' | wc -l)" >> $GITHUB_ENV
- id: generate-matrices
name: Create output matrices
@ -79,51 +81,51 @@ jobs:
with:
script: |
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
//const numMultisrcModules = process.env.NUM_MULTISRC_MODULES;
const numMultisrcModules = process.env.NUM_MULTISRC_MODULES;
const chunkSize = process.env.CI_CHUNK_SIZE;
const numIndividualChunks = Math.ceil(numIndividualModules / chunkSize);
//const numMultisrcChunks = Math.ceil(numMultisrcModules / chunkSize);
const numMultisrcChunks = Math.ceil(numMultisrcModules / chunkSize);
console.log(`Individual modules: ${numIndividualModules} (${numIndividualChunks} chunks of ${chunkSize})`);
//console.log(`Multi-source modules: ${numMultisrcModules} (${numMultisrcChunks} chunks of ${chunkSize})`);
console.log(`Multi-source modules: ${numMultisrcModules} (${numMultisrcChunks} chunks of ${chunkSize})`);
core.setOutput('individualMatrix', { 'chunk': [...Array(numIndividualChunks).keys()] });
//core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
#build_multisrc:
# name: Build multisrc modules
# needs: prepare
# if: ${{ needs.prepare.outputs.isMultisrcChanged == '1' }}
# runs-on: ubuntu-latest
# strategy:
# matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
# steps:
# - name: Checkout PR
# uses: actions/checkout@v3
build_multisrc:
name: Build multisrc modules
needs: prepare
if: ${{ needs.prepare.outputs.isMultisrcChanged == '1' }}
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
steps:
- name: Checkout PR
uses: actions/checkout@v3
# - name: Set up JDK
# uses: actions/setup-java@v3
# with:
# java-version: 11
# distribution: adopt
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: 11
distribution: adopt
# - name: Generate sources from the multi-source library
# uses: gradle/gradle-command-action@v2
# env:
# CI_MODULE_GEN: "true"
# with:
# arguments: :multisrc:generateExtensions
# cache-read-only: true
- name: Generate sources from the multi-source library
uses: gradle/gradle-command-action@v2
env:
CI_MODULE_GEN: "true"
with:
arguments: :multisrc:generateExtensions
cache-read-only: true
# - name: Build extensions (chunk ${{ matrix.chunk }})
# uses: gradle/gradle-command-action@v2
# env:
# CI_MULTISRC: "true"
# CI_CHUNK_NUM: ${{ matrix.chunk }}
# with:
# arguments: assembleDebug
# cache-read-only: true
- name: Build extensions (chunk ${{ matrix.chunk }})
uses: gradle/gradle-command-action@v2
env:
CI_MULTISRC: "true"
CI_CHUNK_NUM: ${{ matrix.chunk }}
with:
arguments: assembleDebug
cache-read-only: true
build_individual:
name: Build individual modules

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
outputs:
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
#multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
env:
CI_MODULE_GEN: true
steps:
@ -49,18 +49,18 @@ jobs:
java-version: 11
distribution: adopt
# - name: Generate multisrc sources
# uses: gradle/gradle-command-action@v2
# with:
# arguments: :multisrc:generateExtensions
- name: Generate multisrc sources
uses: gradle/gradle-command-action@v2
with:
arguments: :multisrc:generateExtensions
- name: Get number of modules
run: |
set -x
./gradlew -q projects | grep '.*extensions\:individual\:.*\:.*' > projects.txt
./gradlew -q projects | grep '.*extensions\:\(individual\|multisrc\)\:.*\:.*' > projects.txt
echo "NUM_INDIVIDUAL_MODULES=$(cat projects.txt | grep '.*\:individual\:.*' | wc -l)" >> $GITHUB_ENV
#echo "NUM_MULTISRC_MODULES=$(cat projects.txt | grep '.*\:multisrc\:.*' | wc -l)" >> $GITHUB_ENV
echo "NUM_MULTISRC_MODULES=$(cat projects.txt | grep '.*\:multisrc\:.*' | wc -l)" >> $GITHUB_ENV
- id: generate-matrices
name: Create output matrices
@ -68,68 +68,66 @@ jobs:
with:
script: |
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES;
//const numMultisrcModules = process.env.NUM_MULTISRC_MODULES;
const numMultisrcModules = process.env.NUM_MULTISRC_MODULES;
const chunkSize = process.env.CI_CHUNK_SIZE;
const numIndividualChunks = Math.ceil(numIndividualModules / chunkSize);
//const numMultisrcChunks = Math.ceil(numMultisrcModules / chunkSize);
const numMultisrcChunks = Math.ceil(numMultisrcModules / chunkSize);
console.log(`Individual modules: ${numIndividualModules} (${numIndividualChunks} chunks of ${chunkSize})`);
//console.log(`Multi-source modules: ${numMultisrcModules} (${numMultisrcChunks} chunks of ${chunkSize})`);
console.log(`Multi-source modules: ${numMultisrcModules} (${numMultisrcChunks} chunks of ${chunkSize})`);
core.setOutput('individualMatrix', { 'chunk': [...Array(numIndividualChunks).keys()] });
//core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
#build_multisrc:
# name: Build multisrc modules
# needs: prepare
# runs-on: ubuntu-latest
# strategy:
# matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
# steps:
# - name: Checkout master branch
# uses: actions/checkout@v3
# with:
# ref: master
build_multisrc:
name: Build multisrc modules
needs: prepare
runs-on: ubuntu-latest
strategy:
matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
steps:
- name: Checkout master branch
uses: actions/checkout@v3
# - name: Set up JDK
# uses: actions/setup-java@v3
# with:
# java-version: 11
# distribution: adopt
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: 11
distribution: adopt
# - name: Prepare signing key
# run: |
# echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
- name: Prepare signing key
run: |
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
# - name: Generate sources from the multi-source library
# uses: gradle/gradle-command-action@v2
# env:
# CI_MODULE_GEN: "true"
# with:
# arguments: :multisrc:generateExtensions
- name: Generate sources from the multi-source library
uses: gradle/gradle-command-action@v2
env:
CI_MODULE_GEN: "true"
with:
arguments: :multisrc:generateExtensions
# - name: Build extensions (chunk ${{ matrix.chunk }})
# uses: gradle/gradle-command-action@v2
# env:
# CI_MULTISRC: "true"
# CI_CHUNK_NUM: ${{ matrix.chunk }}
# ALIAS: ${{ secrets.ALIAS }}
# KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
# with:
# arguments: assembleRelease
- name: Build extensions (chunk ${{ matrix.chunk }})
uses: gradle/gradle-command-action@v2
env:
CI_MULTISRC: "true"
CI_CHUNK_NUM: ${{ matrix.chunk }}
ALIAS: ${{ secrets.ALIAS }}
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
with:
arguments: assembleRelease
# - name: Upload APKs (chunk ${{ matrix.chunk }})
# uses: actions/upload-artifact@v2
# if: "github.repository == 'jmir1/aniyomi-extensions'"
# with:
# name: "multisrc-apks-${{ matrix.chunk }}"
# path: "**/*.apk"
# retention-days: 1
- name: Upload APKs (chunk ${{ matrix.chunk }})
uses: actions/upload-artifact@v3
if: "github.repository == 'jmir1/aniyomi-extensions'"
with:
name: "multisrc-apks-${{ matrix.chunk }}"
path: "**/*.apk"
retention-days: 1
# - name: Clean up CI files
# run: rm signingkey.jks
- name: Clean up CI files
run: rm signingkey.jks
build_individual:
name: Build individual modules
@ -177,6 +175,7 @@ jobs:
name: Publish repo
needs:
- build_individual
- build_multisrc
if: "github.repository == 'jmir1/aniyomi-extensions'"
runs-on: ubuntu-latest
steps:

93
multisrc/build.gradle.kts Normal file
View File

@ -0,0 +1,93 @@
plugins {
id("com.android.library")
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
}
android {
compileSdk = AndroidConfig.compileSdk
namespace = "eu.kanade.tachiyomi.lib.themesources"
defaultConfig {
minSdk = 29
targetSdk = AndroidConfig.targetSdk
}
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
}
configurations {
compileOnly {
isCanBeResolved = true
}
}
dependencies {
compileOnly(libs.bundles.common)
// Implements all shared-extractors on the extensions generator
// Note that this does not mean that generated sources are going to
// implement them too; this is just to be able to compile and generate sources.
rootProject.subprojects
.filter { it.path.startsWith(":lib") }
.forEach(::implementation)
}
tasks {
val generateExtensions by registering {
doLast {
val isWindows = System.getProperty("os.name").toString().toLowerCase().contains("win")
var classPath = (
configurations.compileOnly.get().asFileTree.toList() +
listOf(
configurations.androidApis.get().asFileTree.first().absolutePath, // android.jar path
"$projectDir/build/intermediates/aar_main_jar/debug/classes.jar", // jar made from this module
)
)
.joinToString(if (isWindows) ";" else ":")
var javaPath = "${System.getProperty("java.home")}/bin/java"
val mainClass = "generator.GeneratorMainKt" // Main class we want to execute
if (isWindows) {
classPath = classPath.replace("/", "\\")
javaPath = javaPath.replace("/", "\\")
}
val javaProcess = ProcessBuilder()
.directory(null).command(javaPath, "-classpath", classPath, mainClass)
.redirectErrorStream(true).start()
javaProcess.inputStream
.bufferedReader()
.forEachLine(logger::info)
val exitCode = javaProcess.waitFor()
if (exitCode != 0) {
throw Exception("Java process failed with exit code: $exitCode")
}
}
dependsOn("ktFormat", "ktLint", "assembleDebug")
}
register<org.jmailen.gradle.kotlinter.tasks.LintTask>("ktLint") {
if (project.hasProperty("theme")) {
val theme = project.property("theme")
source(files("src/main/java/eu/kanade/tachiyomi/multisrc/$theme", "overrides/$theme"))
return@register
}
source(files("src", "overrides"))
}
register<org.jmailen.gradle.kotlinter.tasks.FormatTask>("ktFormat") {
if (project.hasProperty("theme")) {
val theme = project.property("theme")
source(files("src/main/java/eu/kanade/tachiyomi/multisrc/$theme", "overrides/$theme"))
return@register
}
source(files("src", "overrides"))
}
}

View File

@ -0,0 +1,3 @@
dependencies {
implementation(project(":lib-dood-extractor"))
}

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox
import eu.kanade.tachiyomi.multisrc.dopeflix.DopeFlix
class DopeBox : DopeFlix(
"DopeBox",
"en",
arrayOf("dopebox.to", "dopebox.se"), // Domain list
"dopebox.to", // Default domain
) {
override val id: Long = 787491081765201367
}

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.animeextension.en.sflix
import eu.kanade.tachiyomi.multisrc.dopeflix.DopeFlix
class SFlix : DopeFlix(
"SFlix",
"en",
arrayOf("sflix.to", "sflix.se"), // Domain list
"sflix.to", // Default domain
) {
override val id: Long = 8615824918772726940
}

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.animeextension.en.sflix
package eu.kanade.tachiyomi.multisrc.dopeflix
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.sflix.extractors.SFlixExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -14,6 +13,8 @@ import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.multisrc.dopeflix.dto.VideoDto
import eu.kanade.tachiyomi.multisrc.dopeflix.extractors.DopeFlixExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
@ -23,10 +24,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
@ -37,18 +34,18 @@ import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "SFlix"
abstract class DopeFlix(
override val name: String,
override val lang: String,
private val domainArray: Array<String>,
private val defaultDomain: String,
) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val baseUrl by lazy {
"https://" + preferences.getString(PREF_DOMAIN_KEY, "sflix.to")!!
"https://" + preferences.getString(PREF_DOMAIN_KEY, defaultDomain)!!
}
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
@ -57,7 +54,9 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
private val json = Json {
ignoreUnknownKeys = true
}
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
@ -67,7 +66,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request {
val type = preferences.getString(PREF_POPULAR_KEY, "movie")!!
val type = preferences.getString(PREF_POPULAR_KEY, PREF_POPULAR_DEFAULT)!!
return GET("$baseUrl/$type?page=$page")
}
@ -148,7 +147,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val extractor = SFlixExtractor(client)
val extractor = DopeFlixExtractor(client)
val videoList = doc.select("ul.fss-list a.btn-play")
.parallelMap { server ->
val name = server.selectFirst("span")!!.text()
@ -179,18 +178,13 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun getVideosFromServer(source: String, name: String): List<Video>? {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val json = json.decodeFromString<JsonObject>(source)
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content
val subs2 = mutableListOf<Track>()
json["tracks"]?.jsonArray
?.filter { it.jsonObject["kind"]!!.jsonPrimitive.content == "captions" }
?.map { track ->
val trackUrl = track.jsonObject["file"]!!.jsonPrimitive.content
val lang = track.jsonObject["label"]!!.jsonPrimitive.content
try {
subs2.add(Track(trackUrl, lang))
} catch (e: Error) {}
} ?: emptyList()
val response = json.decodeFromString<VideoDto>(source)
val masterUrl = response.sources.first().file
val subs2 = response.tracks
?.filter { it.kind == "captions" }
?.mapNotNull {
runCatching { Track(it.file, it.label) }.getOrNull()
} ?: emptyList<Track>()
val subs = subLangOrder(subs2)
if (masterUrl.contains("playlist.m3u8")) {
val prefix = "#EXT-X-STREAM-INF:"
@ -222,39 +216,17 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
return sortedWith(
compareBy { it.quality.contains(quality) },
).reversed()
}
private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString(PREF_SUB_KEY, null)
if (language != null) {
val newList = mutableListOf<Track>()
var preferred = 0
for (track in tracks) {
if (track.lang.contains(language)) {
newList.add(preferred, track)
preferred++
} else {
newList.add(track)
}
}
return newList
}
return tracks
val language = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
return tracks.sortedWith(
compareBy { it.lang.contains(language) },
).reversed()
}
override fun videoListSelector() = throw Exception("not used")
@ -272,7 +244,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = SFlixFilters.getSearchParameters(filters)
val params = DopeFlixFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
@ -282,7 +254,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: SFlixFilters.FilterSearchParams): Request {
private fun searchAnimeRequest(page: Int, query: String, filters: DopeFlixFilters.FilterSearchParams): Request {
val url = if (query.isNotBlank()) {
val fixedQuery = query.replace(" ", "-")
"$baseUrl/search/$fixedQuery?page=$page"
@ -301,7 +273,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return GET(url, headers)
}
override fun getFilterList(): AnimeFilterList = SFlixFilters.filterList
override fun getFilterList(): AnimeFilterList = DopeFlixFilters.filterList
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
@ -335,7 +307,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/")
override fun latestUpdatesSelector(): String {
val sectionLabel = preferences.getString(PREF_LATEST_KEY, "Movies")!!
val sectionLabel = preferences.getString(PREF_LATEST_KEY, PREF_LATEST_DEFAULT)!!
return "section.block_area:has(h2.cat-heading:contains($sectionLabel)) div.film-poster"
}
@ -345,9 +317,9 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
val domainPref = ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_LIST
entryValues = PREF_DOMAIN_LIST
setDefaultValue("sflix.to")
entries = domainArray
entryValues = domainArray
setDefaultValue(defaultDomain)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -362,7 +334,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
setDefaultValue("1080p")
setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -377,7 +349,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES
entryValues = PREF_SUB_LANGUAGES
setDefaultValue("English")
setDefaultValue(PREF_SUB_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -392,7 +364,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES
entryValues = PREF_LATEST_PAGES
setDefaultValue("Movies")
setDefaultValue(PREF_LATEST_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -407,7 +379,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES
entryValues = PREF_POPULAR_VALUES
setDefaultValue("movie")
setDefaultValue(PREF_POPULAR_DEFAULT)
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
@ -434,14 +406,15 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
companion object {
private const val PREF_DOMAIN_KEY = "preferred_domain_new"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private val PREF_DOMAIN_LIST = arrayOf("sflix.to", "sflix.se")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private const val PREF_QUALITY_DEFAULT = "1080p"
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language"
private const val PREF_SUB_DEFAULT = "English"
private val PREF_SUB_LANGUAGES = arrayOf(
"Arabic", "English", "French", "German", "Hungarian",
"Italian", "Japanese", "Portuguese", "Romanian", "Russian",
@ -450,10 +423,12 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private const val PREF_LATEST_KEY = "preferred_latest_page"
private const val PREF_LATEST_TITLE = "Preferred latest page"
private const val PREF_LATEST_DEFAULT = "Movies"
private val PREF_LATEST_PAGES = arrayOf("Movies", "TV Shows")
private const val PREF_POPULAR_KEY = "preferred_popular_page_new"
private const val PREF_POPULAR_TITLE = "Preferred popular page"
private const val PREF_POPULAR_DEFAULT = "movie"
private val PREF_POPULAR_ENTRIES = PREF_LATEST_PAGES
private val PREF_POPULAR_VALUES = arrayOf("movie", "tv-show")
}

View File

@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox
package eu.kanade.tachiyomi.multisrc.dopeflix
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DopeBoxFilters {
object DopeFlixFilters {
open class QueryPartFilter(
displayName: String,
@ -46,17 +46,17 @@ object DopeBoxFilters {
}
}
class TypeFilter : QueryPartFilter("Type", DopeBoxFiltersData.types)
class QualityFilter : QueryPartFilter("Quality", DopeBoxFiltersData.qualities)
class ReleaseYearFilter : QueryPartFilter("Released at", DopeBoxFiltersData.years)
class TypeFilter : QueryPartFilter("Type", DopeFlixFiltersData.types)
class QualityFilter : QueryPartFilter("Quality", DopeFlixFiltersData.qualities)
class ReleaseYearFilter : QueryPartFilter("Released at", DopeFlixFiltersData.years)
class GenresFilter : CheckBoxFilterList(
"Genres",
DopeBoxFiltersData.genres.map { CheckBoxVal(it.first, false) },
DopeFlixFiltersData.genres.map { CheckBoxVal(it.first, false) },
)
class CountriesFilter : CheckBoxFilterList(
"Countries",
DopeBoxFiltersData.countries.map { CheckBoxVal(it.first, false) },
DopeFlixFiltersData.countries.map { CheckBoxVal(it.first, false) },
)
val filterList = AnimeFilterList(
@ -83,12 +83,12 @@ object DopeBoxFilters {
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<QualityFilter>(),
filters.asQueryPart<ReleaseYearFilter>(),
filters.parseCheckbox<GenresFilter>(DopeBoxFiltersData.genres),
filters.parseCheckbox<CountriesFilter>(DopeBoxFiltersData.countries),
filters.parseCheckbox<GenresFilter>(DopeFlixFiltersData.genres),
filters.parseCheckbox<CountriesFilter>(DopeFlixFiltersData.countries),
)
}
private object DopeBoxFiltersData {
private object DopeFlixFiltersData {
val all = Pair("All", "all")
val types = arrayOf(

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.multisrc.dopeflix
import generator.ThemeSourceData.SingleLang
import generator.ThemeSourceGenerator
class DopeFlixGenerator : ThemeSourceGenerator {
override val themePkg = "dopeflix"
override val themeClass = "DopeFlix"
override val baseVersionCode = 16
override val sources = listOf(
SingleLang("DopeBox", "https://dopebox.to", "en", isNsfw = false, overrideVersionCode = 1),
SingleLang("SFlix", "https://sflix.to", "en", isNsfw = false),
)
companion object {
@JvmStatic
fun main(args: Array<String>) = DopeFlixGenerator().createAll()
}
}

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.dto
import kotlinx.serialization.Serializable
@Serializable
data class VideoDto(
val sources: List<VideoLink>,
val tracks: List<TrackDto>?,
)
@Serializable
data class VideoLink(val file: String = "")
@Serializable
data class TrackDto(val file: String, val kind: String, val label: String = "")

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.multisrc.dopeflix.extractors
import eu.kanade.tachiyomi.multisrc.dopeflix.utils.Decryptor
import eu.kanade.tachiyomi.network.GET
import okhttp3.Headers
import okhttp3.OkHttpClient
class DopeFlixExtractor(private val client: OkHttpClient) {
companion object {
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id="
}
fun getSourcesJson(url: String): String? {
val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null }
val serverUrl = url.substringBefore("/embed")
val srcRes = client.newCall(
GET(
serverUrl + SOURCES_PATH + id,
headers = Headers.headersOf("x-requested-with", "XMLHttpRequest"),
),
)
.execute()
.body.string()
val key = client.newCall(GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"))
.execute()
.body.string()
// encrypted data will start with "U2Fsd..." because they put
// "Salted__" at the start of encrypted data, thanks openssl
// if its not encrypted, then return it
if ("\"sources\":\"U2FsdGVk" !in srcRes) return srcRes
if (!srcRes.contains("{\"sources\":")) return null
val encrypted = srcRes.substringAfter("sources\":\"").substringBefore("\"")
val decrypted = Decryptor.decrypt(encrypted, key) ?: return null
val end = srcRes.replace("\"$encrypted\"", decrypted)
return end
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.utils
package eu.kanade.tachiyomi.multisrc.dopeflix.utils
import android.util.Base64
import java.security.DigestException

View File

@ -0,0 +1,28 @@
package generator
import java.io.File
/**
* Finds and calls all `ThemeSourceGenerator`s
*/
fun main(args: Array<String>) {
val userDir = System.getProperty("user.dir")!!
val sourcesDirPath = "$userDir/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc"
val sourcesDir = File(sourcesDirPath)
// find all theme packages
sourcesDir.list()!!
.filter { File(sourcesDir, it).isDirectory }
.forEach { themeSource ->
// Find all XxxGenerator.kt files and invoke main from them
File("$sourcesDirPath/$themeSource").list()!!
.filter { it.endsWith("Generator.kt") }
.mapNotNull { generatorClass ->
// Find Java class and extract method lists
Class.forName("eu/kanade/tachiyomi/multisrc/$themeSource/$generatorClass".replace("/", ".").substringBefore(".kt"))
.methods
.find { it.name == "main" }
}
.forEach { it.invoke(null, emptyArray<String>()) }
}
}

View File

@ -0,0 +1,49 @@
package generator
import java.io.File
/**
* Finds all themes and creates an Intellij Idea run configuration for their generators
* Should be run after creation/deletion of each theme
*/
fun main(args: Array<String>) {
val userDir = System.getProperty("user.dir")!!
val sourcesDirPath = "$userDir/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc"
val sourcesDir = File(sourcesDirPath)
// cleanup from past runs
File("$userDir/.run").apply {
if (exists()) {
deleteRecursively()
}
mkdirs()
}
// find all theme packages
sourcesDir.list()!!
.filter { File(sourcesDir, it).isDirectory }
.forEach { themeSource ->
// Find all XxxGenerator.kt files
File("$sourcesDirPath/$themeSource").list()!!
.filter { it.endsWith("Generator.kt") }
.map { it.substringBefore(".kt") }
.forEach { generatorClass ->
val file = File("$userDir/.run/$generatorClass.run.xml")
val intellijConfStr = """
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="$generatorClass" type="JetRunConfigurationType" nameIsGenerated="true">
<module name="aniyomi-extensions.multisrc.main" />
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.$themeSource.$generatorClass" />
<method v="2">
<option name="Make" enabled="true" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="${'$'}PROJECT_DIR${'$'}/multisrc" vmOptions="" scriptParameters="-Ptheme=$themeSource" />
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="${'$'}PROJECT_DIR${'$'}/multisrc" vmOptions="" scriptParameters="-Ptheme=$themeSource" />
</method>
</configuration>
</component>
""".trimIndent()
file.writeText(intellijConfStr)
file.appendText("\n")
}
}
}

View File

@ -0,0 +1,304 @@
package generator
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.io.File
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.util.Locale
/**
* This is meant to be used in place of a factory extension, specifically for what would be a multi-source extension.
* A multi-lang (but not multi-source) extension should still be made as a factory extension.
* Use a generator for initial setup of a theme source or when all of the inheritors need a version bump.
* Source list (val sources) should be kept up to date.
*/
interface ThemeSourceGenerator {
/**
* The class that the sources inherit from.
*/
val themeClass: String
/**
* The package that contains themeClass.
*/
val themePkg: String
/**
* Base theme version, starts with 1 and should be increased when based theme class changes
*/
val baseVersionCode: Int
/**
* The list of sources to be created or updated.
*/
val sources: List<ThemeSourceData>
fun createAll() {
val userDir = System.getProperty("user.dir")!!
sources.forEach { createGradleProject(it, themePkg, themeClass, baseVersionCode, userDir) }
}
companion object {
private fun pkgNameSuffix(source: ThemeSourceData, separator: String): String {
return if (source is ThemeSourceData.SingleLang) {
listOf(source.lang.substringBefore("-"), source.pkgName).joinToString(separator)
} else {
listOf("all", source.pkgName).joinToString(separator)
}
}
private fun themeSuffix(themePkg: String, separator: String): String {
return listOf("eu", "kanade", "tachiyomi", "multisrc", themePkg).joinToString(separator)
}
private fun writeGradle(gradle: File, source: ThemeSourceData, themePkg: String, baseVersionCode: Int, defaultAdditionalGradlePath: String, additionalGradleOverridePath: String) {
fun File.readTextOrEmptyString(): String = if (exists()) readText(Charsets.UTF_8) else ""
val defaultAdditionalGradleText = File(defaultAdditionalGradlePath).readTextOrEmptyString()
val additionalGradleOverrideText = File(additionalGradleOverridePath).readTextOrEmptyString()
val placeholders = mapOf(
"SOURCEHOST" to source.baseUrl.toHttpUrlOrNull()?.host,
"SOURCESCHEME" to source.baseUrl.toHttpUrlOrNull()?.scheme,
)
val placeholdersStr = placeholders
.filter { it.value != null }
.map { "${" ".repeat(12)}${it.key}: \"${it.value}\"" }
.joinToString(",\n")
gradle.writeText(
"""
|// THIS FILE IS AUTO-GENERATED; DO NOT EDIT
|apply plugin: 'com.android.application'
|apply plugin: 'kotlin-android'
|apply plugin: 'kotlinx-serialization'
|
|ext {
| extName = '${source.name}'
| pkgNameSuffix = '${pkgNameSuffix(source, ".")}'
| extClass = '.${source.className}'
| extFactory = '$themePkg'
| extVersionCode = ${baseVersionCode + source.overrideVersionCode + multisrcLibraryVersion}
| ${if (source.isNsfw) "containsNsfw = true\n" else ""}
|}
|$defaultAdditionalGradleText
|$additionalGradleOverrideText
|apply from: "${'$'}rootDir/common.gradle"
|
|android {
| defaultConfig {
| manifestPlaceholders += [
|$placeholdersStr
| ]
| }
|}
""".trimMargin(),
)
}
private fun writeAndroidManifest(androidManifestFile: File, manifestOverridesPath: String, defaultAndroidManifestPath: String) {
val androidManifestOverride = File(manifestOverridesPath)
val defaultAndroidManifest = File(defaultAndroidManifestPath)
if (androidManifestOverride.exists()) {
androidManifestOverride.copyTo(androidManifestFile)
} else if (defaultAndroidManifest.exists()) {
defaultAndroidManifest.copyTo(androidManifestFile)
} else {
androidManifestFile.writeText(
"""
|<?xml version="1.0" encoding="utf-8"?>
|<!-- THIS FILE IS AUTO-GENERATED; DO NOT EDIT -->
|<manifest package="eu.kanade.tachiyomi.animeextension" />
""".trimMargin(),
)
}
}
fun createGradleProject(source: ThemeSourceData, themePkg: String, themeClass: String, baseVersionCode: Int, userDir: String) {
// userDir = aniyomi-extensions project root path
val projectRootPath = "$userDir/generated-src/${pkgNameSuffix(source, "/")}"
val projectSrcPath = "$projectRootPath/src/eu/kanade/tachiyomi/animeextension/${pkgNameSuffix(source, "/")}"
val overridesPath = "$userDir/multisrc/overrides/$themePkg/${source.pkgName}"
val defaultResPath = "$userDir/multisrc/overrides/$themePkg/default/res"
val defaultAndroidManifestPath = "$userDir/multisrc/overrides/$themePkg/default/AndroidManifest.xml"
val defaultAdditionalGradlePath = "$userDir/multisrc/overrides/$themePkg/default/additional.gradle"
val resOverridePath = "$overridesPath/res"
val srcOverridePath = "$overridesPath/src"
val manifestOverridePath = "$overridesPath/AndroidManifest.xml"
val additionalGradleOverridePath = "$overridesPath/additional.gradle"
val projectGradleFile = File("$projectRootPath/build.gradle")
val projectAndroidManifestFile = File("$projectRootPath/AndroidManifest.xml")
File(projectRootPath).let { projectRootFile ->
println("Generating $source")
projectRootFile.mkdirs()
// remove everything from past runs
cleanDirectory(projectRootFile)
writeGradle(projectGradleFile, source, themePkg, baseVersionCode, defaultAdditionalGradlePath, additionalGradleOverridePath)
writeAndroidManifest(projectAndroidManifestFile, manifestOverridePath, defaultAndroidManifestPath)
writeSourceClasses(projectSrcPath, srcOverridePath, source, themePkg, themeClass)
copyThemeClasses(userDir, themePkg, projectRootPath)
copyThemeReadmes(userDir, themePkg, overridesPath, projectRootPath)
copyResFiles(resOverridePath, defaultResPath, source, projectRootPath)
}
}
private fun copyThemeReadmes(userDir: String, themePkg: String, overridesPath: String, projectRootPath: String) {
val sourcePath = "$userDir/multisrc/src/main/java/${themeSuffix(themePkg, "/")}"
File(projectRootPath).mkdirs()
listOf(sourcePath, overridesPath).forEach { path ->
File(path).list()
?.filter { it.endsWith("README.md") || it.endsWith("CHANGELOG.md") }
?.forEach {
Files.copy(
File("$path/$it").toPath(),
File("$projectRootPath/$it").toPath(),
StandardCopyOption.REPLACE_EXISTING,
)
}
}
}
private fun copyThemeClasses(userDir: String, themePkg: String, projectRootPath: String) {
val themeSrcPath = "$userDir/multisrc/src/main/java/${themeSuffix(themePkg, "/")}"
val themeDestPath = "$projectRootPath/src/${themeSuffix(themePkg, "/")}"
File(themeDestPath).mkdirs()
File(themeSrcPath).walk()
.map { it.toString().replace(themeSrcPath, "") }
.filter { it.endsWith(".kt") && !it.endsWith("Generator.kt") }
.forEach {
File("$themeSrcPath/$it").copyTo(
File("$themeDestPath/$it"),
true, // REPLACE_EXISTING
)
}
}
private fun copyResFiles(resOverridePath: String, defaultResPath: String, source: ThemeSourceData, projectRootPath: String): Any {
// check if res override exists if not copy default res
val resOverride = File(resOverridePath)
return if (resOverride.exists()) {
resOverride.copyRecursively(File("$projectRootPath/res"))
} else {
File(defaultResPath).let { defaultResFile ->
if (defaultResFile.exists()) defaultResFile.copyRecursively(File("$projectRootPath/res"))
}
}
}
private fun writeSourceClasses(projectSrcPath: String, srcOverridePath: String, source: ThemeSourceData, themePkg: String, themeClass: String) {
val projectSrcFile = File(projectSrcPath)
projectSrcFile.mkdirs()
val srcOverrideFile = File(srcOverridePath)
if (srcOverrideFile.exists()) {
srcOverrideFile.copyRecursively(projectSrcFile)
} else {
writeSourceClass(projectSrcFile, source, themePkg, themeClass)
}
}
private fun writeSourceClass(classPath: File, source: ThemeSourceData, themePkg: String, themeClass: String) {
fun factoryClassText() = when (source) {
is ThemeSourceData.SingleLang -> {
"""class ${source.className} : $themeClass("${source.sourceName}", "${source.baseUrl}", "${source.lang}")"""
}
is ThemeSourceData.MultiLang -> {
val sourceClasses = source.langs.map { lang ->
"""$themeClass("${source.sourceName}", "${source.baseUrl}", "$lang")"""
}
"""
|class ${source.className} : AnimeSourceFactory {
| override fun createSources() = listOf(
| ${sourceClasses.joinToString(",\n")}
| )
|}
""".trimMargin()
}
}
File("$classPath/${source.className}.kt").writeText(
"""
|/* ktlint-disable */
|// THIS FILE IS AUTO-GENERATED; DO NOT EDIT
|package eu.kanade.tachiyomi.animeextension.${pkgNameSuffix(source, ".")}
|
|import eu.kanade.tachiyomi.multisrc.$themePkg.$themeClass
|${if (source is ThemeSourceData.MultiLang) "import eu.kanade.tachiyomi.animesource.AnimeSourceFactory" else ""}
|
|${factoryClassText()}
""".trimMargin(),
)
}
private fun cleanDirectory(dir: File) {
dir.listFiles()?.forEach {
if (it.isDirectory) {
cleanDirectory(it)
}
it.delete()
}
}
}
}
sealed class ThemeSourceData {
abstract val name: String
abstract val baseUrl: String
abstract val isNsfw: Boolean
abstract val className: String
abstract val pkgName: String
/**
* Override it if for some reason the name attribute inside the source class
* should be different from the extension name. Useful in cases where the
* extension name should be romanized and the source name should be the one
* in the source language. Defaults to the extension name if not specified.
*/
abstract val sourceName: String
/**
* overrideVersionCode defaults to 0, if a source changes their source override code or
* a previous existing source suddenly needs source code overrides, overrideVersionCode
* should be increased.
* When a new source is added with overrides, overrideVersionCode should still be set to 0
*
* Note: source code overrides are located in "multisrc/overrides/src/<themeName>/<sourceName>"
*/
abstract val overrideVersionCode: Int
data class SingleLang(
override val name: String,
override val baseUrl: String,
val lang: String,
override val isNsfw: Boolean = false,
override val className: String = name.replace(" ", ""),
override val pkgName: String = className.lowercase(Locale.ENGLISH),
override val sourceName: String = name,
override val overrideVersionCode: Int = 0,
) : ThemeSourceData()
data class MultiLang(
override val name: String,
override val baseUrl: String,
val langs: List<String>,
override val isNsfw: Boolean = false,
override val className: String = name.replace(" ", "") + "Factory",
override val pkgName: String = className.substringBefore("Factory").lowercase(Locale.ENGLISH),
override val sourceName: String = name,
override val overrideVersionCode: Int = 0,
) : ThemeSourceData()
}
/**
* This variable should be increased when the multisrc library changes in a way that prompts global extension upgrade
*/
const val multisrcLibraryVersion = 0

View File

@ -25,8 +25,8 @@ File(rootDir, "lib").eachDir {
if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
// Local development (full project build)
//include(":multisrc")
//project(":multisrc").projectDir = File("multisrc")
include(":multisrc")
project(":multisrc").projectDir = File("multisrc")
// Loads all extensions
File(rootDir, "src").eachDir { dir ->
@ -37,13 +37,13 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
}
}
// Loads generated extensions from multisrc
//File(rootDir, "generated-src").eachDir { dir ->
// dir.eachDir { subdir ->
// val name = ":extensions:multisrc:${dir.name}:${subdir.name}"
// include(name)
// project(name).projectDir = File("generated-src/${dir.name}/${subdir.name}")
// }
//}
File(rootDir, "generated-src").eachDir { dir ->
dir.eachDir { subdir ->
val name = ":extensions:multisrc:${dir.name}:${subdir.name}"
include(name)
project(name).projectDir = File("generated-src/${dir.name}/${subdir.name}")
}
}
/**
* If you're developing locally and only want to work with a single module,
@ -63,7 +63,7 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
val chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
val chunk = System.getenv("CI_CHUNK_NUM").toInt()
/*if (isMultisrc) {
if (isMultisrc) {
include(":multisrc")
project(":multisrc").projectDir = File("multisrc")
@ -74,15 +74,15 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
include(name)
project(name).projectDir = File("generated-src/${it.parentFile.name}/${it.name}")
}
} else {*/
// Loads individual extensions
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
val name = ":extensions:individual:${it.parentFile.name}:${it.name}"
println(name)
include(name)
project(name).projectDir = File("src/${it.parentFile.name}/${it.name}")
} else {
// Loads individual extensions
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
val name = ":extensions:individual:${it.parentFile.name}:${it.name}"
println(name)
include(name)
project(name).projectDir = File("src/${it.parentFile.name}/${it.name}")
}
//}
}
}
fun File.getChunk(chunk: Int, chunkSize: Int): List<File>? {

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -1,16 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'DopeBox'
pkgNameSuffix = 'en.dopebox'
extClass = '.DopeBox'
extVersionCode = 17
libVersion = '13'
}
dependencies {
implementation(project(':lib-dood-extractor'))
}
apply from: "$rootDir/common.gradle"

View File

@ -1,460 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.dopebox.extractors.DopeBoxExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.Track
import eu.kanade.tachiyomi.animesource.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class DopeBox : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val name = "DopeBox"
override val baseUrl by lazy {
"https://" + preferences.getString(PREF_DOMAIN_KEY, "dopebox.to")!!
}
override val lang = "en"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/")
// ============================== Popular ===============================
override fun popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request {
val type = preferences.getString(PREF_POPULAR_KEY, "movie")!!
return GET("$baseUrl/$type?page=$page")
}
override fun popularAnimeFromElement(element: Element): SAnime {
val anime = SAnime.create()
anime.setUrlWithoutDomain(element.selectFirst("a")!!.attr("href"))
anime.thumbnail_url = element.selectFirst("img")!!.attr("data-src")
anime.title = element.selectFirst("a")!!.attr("title")
return anime
}
override fun popularAnimeNextPageSelector(): String = "ul.pagination li.page-item a[title=next]"
// ============================== Episodes ==============================
override fun episodeListSelector() = throw Exception("not used")
override fun episodeListParse(response: Response): List<SEpisode> {
val document = response.asJsoup()
val episodeList = mutableListOf<SEpisode>()
val infoElement = document.select("div.detail_page-watch")
val id = infoElement.attr("data-id")
val dataType = infoElement.attr("data-type") // Tv = 2 or movie = 1
if (dataType == "2") {
val seasonUrl = "$baseUrl/ajax/v2/tv/seasons/$id"
val seasonsHtml = client.newCall(
GET(
seasonUrl,
headers = Headers.headersOf("Referer", document.location()),
),
).execute().asJsoup()
val seasonsElements = seasonsHtml.select("a.dropdown-item.ss-item")
seasonsElements.forEach {
val seasonEpList = parseEpisodesFromSeries(it)
episodeList.addAll(seasonEpList)
}
} else {
val movieUrl = "$baseUrl/ajax/movie/episodes/$id"
val episode = SEpisode.create()
episode.name = document.select("h2.heading-name").text()
episode.episode_number = 1F
episode.setUrlWithoutDomain(movieUrl)
episodeList.add(episode)
}
return episodeList.reversed()
}
override fun episodeFromElement(element: Element): SEpisode = throw Exception("not used")
private fun parseEpisodesFromSeries(element: Element): List<SEpisode> {
val seasonId = element.attr("data-id")
val seasonName = element.text()
val episodesUrl = "$baseUrl/ajax/v2/season/episodes/$seasonId"
val episodesHtml = client.newCall(GET(episodesUrl))
.execute()
.asJsoup()
val episodeElements = episodesHtml.select("div.eps-item")
return episodeElements.map { episodeFromElement(it, seasonName) }
}
private fun episodeFromElement(element: Element, seasonName: String): SEpisode {
val episodeId = element.attr("data-id")
val epNum = element.selectFirst("div.episode-number")!!.text()
val epName = element.selectFirst("h3.film-name a")!!.text()
val episode = SEpisode.create().apply {
name = "$seasonName $epNum $epName"
setUrlWithoutDomain("$baseUrl/ajax/v2/episode/servers/$episodeId")
}
return episode
}
private fun getNumberFromEpsString(epsStr: String): String {
return epsStr.filter { it.isDigit() }
}
// ============================ Video Links =============================
override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!)
val extractor = DopeBoxExtractor(client)
val videoList = doc.select("ul.fss-list a.btn-play")
.parallelMap { server ->
val name = server.selectFirst("span")!!.text()
val id = server.attr("data-id")
val url = "$baseUrl/ajax/sources/$id"
val reqBody = client.newCall(GET(url, episodeReferer)).execute()
.body.string()
val sourceUrl = reqBody.substringAfter("\"link\":\"")
.substringBefore("\"")
runCatching {
when {
"DoodStream" in name ->
DoodExtractor(client).videoFromUrl(sourceUrl)?.let {
listOf(it)
}
"Vidcloud" in name || "UpCloud" in name -> {
val source = extractor.getSourcesJson(sourceUrl)
source?.let { getVideosFromServer(it, name) }
}
else -> null
}
}.getOrNull()
}
.filterNotNull()
.flatten()
return videoList
}
private fun getVideosFromServer(source: String, name: String): List<Video>? {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val json = json.decodeFromString<JsonObject>(source)
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content
val subs2 = mutableListOf<Track>()
json["tracks"]?.jsonArray
?.filter { it.jsonObject["kind"]!!.jsonPrimitive.content == "captions" }
?.map { track ->
val trackUrl = track.jsonObject["file"]!!.jsonPrimitive.content
val lang = track.jsonObject["label"]!!.jsonPrimitive.content
try {
subs2.add(Track(trackUrl, lang))
} catch (e: Error) {}
} ?: emptyList()
val subs = subLangOrder(subs2)
if (masterUrl.contains("playlist.m3u8")) {
val prefix = "#EXT-X-STREAM-INF:"
val playlist = client.newCall(GET(masterUrl)).execute()
.body.string()
val videoList = playlist.substringAfter(prefix).split(prefix).map {
val quality = "$name - " + it.substringAfter("RESOLUTION=")
.substringAfter("x")
.substringBefore("\n")
.substringBefore(",") + "p"
val videoUrl = it.substringAfter("\n").substringBefore("\n")
try {
Video(videoUrl, quality, videoUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(videoUrl, quality, videoUrl)
}
}
return videoList
}
val defaultVideoList = listOf(
try {
Video(masterUrl, "$name - Default", masterUrl, subtitleTracks = subs)
} catch (e: Error) {
Video(masterUrl, "$name - Default", masterUrl)
},
)
return defaultVideoList
}
override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, null)
if (quality != null) {
val newList = mutableListOf<Video>()
var preferred = 0
for (video in this) {
if (video.quality.contains(quality)) {
newList.add(preferred, video)
preferred++
} else {
newList.add(video)
}
}
return newList
}
return this
}
private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString(PREF_SUB_KEY, null)
if (language != null) {
val newList = mutableListOf<Track>()
var preferred = 0
for (track in tracks) {
if (track.lang.contains(language)) {
newList.add(preferred, track)
preferred++
} else {
newList.add(track)
}
}
return newList
}
return tracks
}
override fun videoListSelector() = throw Exception("not used")
override fun videoFromElement(element: Element) = throw Exception("not used")
override fun videoUrlParse(document: Document) = throw Exception("not used")
// =============================== Search ===============================
override fun searchAnimeFromElement(element: Element) = popularAnimeFromElement(element)
override fun searchAnimeNextPageSelector(): String = popularAnimeNextPageSelector()
override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
val params = DopeBoxFilters.getSearchParameters(filters)
return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess()
.map { response ->
searchAnimeParse(response)
}
}
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used")
private fun searchAnimeRequest(page: Int, query: String, filters: DopeBoxFilters.FilterSearchParams): Request {
val url = if (query.isNotBlank()) {
val fixedQuery = query.replace(" ", "-")
"$baseUrl/search/$fixedQuery?page=$page"
} else {
"$baseUrl/filter?".toHttpUrlOrNull()!!.newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("type", filters.type)
.addQueryParameter("quality", filters.quality)
.addQueryParameter("release_year", filters.releaseYear)
.addQueryParameter("genre", filters.genres)
.addQueryParameter("country", filters.countries)
.build()
.toString()
}
return GET(url, headers)
}
override fun getFilterList(): AnimeFilterList = DopeBoxFilters.filterList
// =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime {
val anime = SAnime.create().apply {
thumbnail_url = document.selectFirst("img.film-poster-img")!!.attr("src")
title = document.selectFirst("img.film-poster-img")!!.attr("title")
genre = document.select("div.row-line:contains(Genre) a")
.joinToString(", ") { it.text() }
description = document.selectFirst("div.detail_page-watch div.description")!!
.text().replace("Overview:", "")
author = document.select("div.row-line:contains(Production) a")
.joinToString(", ") { it.text() }
status = parseStatus(document.select("li.status span.value").text())
}
return anime
}
private fun parseStatus(statusString: String): Int {
return when (statusString) {
"Ongoing" -> SAnime.ONGOING
else -> SAnime.COMPLETED
}
}
// =============================== Latest ===============================
override fun latestUpdatesNextPageSelector(): String? = null
override fun latestUpdatesFromElement(element: Element) = popularAnimeFromElement(element)
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/")
override fun latestUpdatesSelector(): String {
val sectionLabel = preferences.getString(PREF_LATEST_KEY, "Movies")!!
return "section.block_area:has(h2.cat-heading:contains($sectionLabel)) div.film-poster"
}
// ============================== Settings ==============================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val domainPref = ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_LIST
entryValues = PREF_DOMAIN_LIST
setDefaultValue("dopebox.to")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val videoQualityPref = ListPreference(screen.context).apply {
key = PREF_QUALITY_KEY
title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST
setDefaultValue("1080p")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val subLangPref = ListPreference(screen.context).apply {
key = PREF_SUB_KEY
title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES
entryValues = PREF_SUB_LANGUAGES
setDefaultValue("English")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val latestType = ListPreference(screen.context).apply {
key = PREF_LATEST_KEY
title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES
entryValues = PREF_LATEST_PAGES
setDefaultValue("Movies")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
val popularType = ListPreference(screen.context).apply {
key = PREF_POPULAR_KEY
title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES
entryValues = PREF_POPULAR_VALUES
setDefaultValue("movie")
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
val selected = newValue as String
val index = findIndexOfValue(selected)
val entry = entryValues[index] as String
preferences.edit().putString(key, entry).commit()
}
}
screen.addPreference(domainPref)
screen.addPreference(videoQualityPref)
screen.addPreference(subLangPref)
screen.addPreference(latestType)
screen.addPreference(popularType)
}
// ============================= Utilities ==============================
private fun <A, B> Iterable<A>.parallelMap(f: suspend (A) -> B): List<B> =
runBlocking {
map { async(Dispatchers.Default) { f(it) } }.awaitAll()
}
companion object {
private const val PREF_DOMAIN_KEY = "preferred_domain_new"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)"
private val PREF_DOMAIN_LIST = arrayOf("dopebox.to", "dopebox.se")
private const val PREF_QUALITY_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "Preferred quality"
private val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language"
private val PREF_SUB_LANGUAGES = arrayOf(
"Arabic", "English", "French", "German", "Hungarian",
"Italian", "Japanese", "Portuguese", "Romanian", "Russian",
"Spanish",
)
private const val PREF_LATEST_KEY = "preferred_latest_page"
private const val PREF_LATEST_TITLE = "Preferred latest page"
private val PREF_LATEST_PAGES = arrayOf("Movies", "TV Shows")
private const val PREF_POPULAR_KEY = "preferred_popular_page_new"
private const val PREF_POPULAR_TITLE = "Preferred popular page"
private val PREF_POPULAR_ENTRIES = PREF_LATEST_PAGES
private val PREF_POPULAR_VALUES = arrayOf("movie", "tv-show")
}
}

View File

@ -1,65 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox.extractors
import eu.kanade.tachiyomi.animeextension.en.dopebox.utils.Decryptor
import eu.kanade.tachiyomi.network.GET
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
class DopeBoxExtractor(private val client: OkHttpClient) {
// Prevent (automatic) caching the .JS file for different episodes, because it
// changes everytime, and a cached old .js will have a invalid AES password,
// invalidating the decryption algorithm.
// We cache it manually when initializing the class.
private val cacheControl = CacheControl.Builder().noStore().build()
private val newClient = client.newBuilder()
.cache(null)
.build()
companion object {
// its the same .js file for any server it may use,
// so we choose rabbitstream arbitrarily
private const val JS_URL = "https://rabbitstream.net/js/player/prod/e4-player.min.js"
// unlike the case of the .js file, here it is not possible to
// simply use the same host.
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id="
}
// This will create a lag of 1~3s at the initialization of the class, but the
// speedup of the manual cache will be worth it.
private val cachedJs by lazy {
newClient.newCall(GET(JS_URL, cache = cacheControl)).execute()
.body.string()
}
init { cachedJs }
fun getSourcesJson(url: String): String? {
val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null }
val serverUrl = url.substringBefore("/embed")
val srcRes = newClient.newCall(
GET(
serverUrl + SOURCES_PATH + id,
headers = Headers.headersOf("x-requested-with", "XMLHttpRequest"),
cache = cacheControl,
),
)
.execute()
.body.string()
val key = newClient.newCall(GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"))
.execute()
.body.string()
// encrypted data will start with "U2Fsd..." because they put
// "Salted__" at the start of encrypted data, thanks openssl
// if its not encrypted, then return it
if ("\"sources\":\"U2FsdGVk" !in srcRes) return srcRes
if (!srcRes.contains("{\"sources\":")) return null
val encrypted = srcRes.substringAfter("sources\":\"").substringBefore("\"")
val decrypted = Decryptor.decrypt(encrypted, key) ?: return null
val end = srcRes.replace("\"$encrypted\"", decrypted)
return end
}
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox.utils
import android.util.Base64
import java.security.DigestException
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
object Decryptor {
fun decrypt(encodedData: String, remoteKey: String): String? {
val saltedData = Base64.decode(encodedData, Base64.DEFAULT)
val salt = saltedData.copyOfRange(8, 16)
val ciphertext = saltedData.copyOfRange(16, saltedData.size)
val password = remoteKey.toByteArray()
val (key, iv) = GenerateKeyAndIv(password, salt) ?: return null
val keySpec = SecretKeySpec(key, "AES")
val ivSpec = IvParameterSpec(iv)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec)
val decryptedData = String(cipher.doFinal(ciphertext))
return decryptedData
}
// https://stackoverflow.com/a/41434590/8166854
private fun GenerateKeyAndIv(
password: ByteArray,
salt: ByteArray,
hashAlgorithm: String = "MD5",
keyLength: Int = 32,
ivLength: Int = 16,
iterations: Int = 1,
): List<ByteArray>? {
val md = MessageDigest.getInstance(hashAlgorithm)
val digestLength = md.getDigestLength()
val targetKeySize = keyLength + ivLength
val requiredLength = (targetKeySize + digestLength - 1) / digestLength * digestLength
var generatedData = ByteArray(requiredLength)
var generatedLength = 0
try {
md.reset()
while (generatedLength < targetKeySize) {
if (generatedLength > 0) {
md.update(
generatedData,
generatedLength - digestLength,
digestLength,
)
}
md.update(password)
md.update(salt, 0, 8)
md.digest(generatedData, generatedLength, digestLength)
for (i in 1 until iterations) {
md.update(generatedData, generatedLength, digestLength)
md.digest(generatedData, generatedLength, digestLength)
}
generatedLength += digestLength
}
val result = listOf(
generatedData.copyOfRange(0, keyLength),
generatedData.copyOfRange(keyLength, targetKeySize),
)
return result
} catch (e: DigestException) {
return null
}
}
}

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.dopebox.utils
import app.cash.quickjs.QuickJs
// For e4.min.js
object FindPassword {
fun getPassword(js: String): String {
val funcName = js.substringBefore("CryptoJS[")
.substringBeforeLast("document")
.substringAfterLast(",")
.substringBefore("=")
val suspiciousPass = js.substringAfter(":" + funcName)
.substringAfter(",")
.substringBefore("||")
.substringBeforeLast(")")
if (suspiciousPass.startsWith("'")) {
return suspiciousPass.trim('\'')
}
return getPasswordFromJS(js, "(" + suspiciousPass.substringAfter("("))
}
private fun getPasswordFromJS(js: String, getKeyArgs: String): String {
var script = "(function" + js.substringBefore(",(!function")
.substringAfter("(function") + ")"
val decoderFunName = script.substringAfter("=").substringBefore(",")
val decoderFunPrefix = "function " + decoderFunName
var decoderFunBody = js.substringAfter(decoderFunPrefix)
val decoderFunSuffix = decoderFunName + decoderFunBody.substringBefore("{") + ";}"
decoderFunBody = (
decoderFunPrefix +
decoderFunBody.substringBefore(decoderFunSuffix) +
decoderFunSuffix
)
if ("=[" !in js.substring(0, 30)) {
val superArrayName = decoderFunBody.substringAfter("=")
.substringBefore(";")
val superArrayPrefix = "function " + superArrayName
val superArraySuffix = "return " + superArrayName + ";}"
val superArrayBody = (
superArrayPrefix +
js.substringAfter(superArrayPrefix)
.substringBefore(superArraySuffix) +
superArraySuffix
)
script += "\n\n" + superArrayBody
}
script += "\n\n" + decoderFunBody
script += "\n\n$decoderFunName$getKeyArgs"
val qjs = QuickJs.create()
// this part can be really slow, like 5s or even more >:(
val result = qjs.evaluate(script).toString()
qjs.close()
return result
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.animeextension" />

View File

@ -1,16 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Sflix'
pkgNameSuffix = 'en.sflix'
extClass = '.SFlix'
extVersionCode = 16
libVersion = '13'
}
dependencies {
implementation(project(':lib-dood-extractor'))
}
apply from: "$rootDir/common.gradle"

View File

@ -1,187 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix
import eu.kanade.tachiyomi.animesource.model.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object SFlixFilters {
open class QueryPartFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : AnimeFilter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
fun toQueryPart() = vals[state].second
}
open class CheckBoxFilterList(name: String, values: List<CheckBox>) : AnimeFilter.Group<AnimeFilter.CheckBox>(name, values)
private class CheckBoxVal(name: String, state: Boolean = false) : AnimeFilter.CheckBox(name, state)
private inline fun <reified R> AnimeFilterList.asQueryPart(): String {
return (this.getFirst<R>() as QueryPartFilter).toQueryPart()
}
private inline fun <reified R> AnimeFilterList.getFirst(): R {
return this.filterIsInstance<R>().first()
}
private inline fun <reified R> AnimeFilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): String {
return (this.getFirst<R>() as CheckBoxFilterList).state
.mapNotNull { checkbox ->
if (checkbox.state) {
options.find { it.first == checkbox.name }!!.second
} else {
null
}
}.joinToString("-").let {
if (it.isBlank()) {
"all"
} else {
it
}
}
}
class TypeFilter : QueryPartFilter("Type", SFlixFiltersData.types)
class QualityFilter : QueryPartFilter("Quality", SFlixFiltersData.qualities)
class ReleaseYearFilter : QueryPartFilter("Released at", SFlixFiltersData.years)
class GenresFilter : CheckBoxFilterList(
"Genres",
SFlixFiltersData.genres.map { CheckBoxVal(it.first, false) },
)
class CountriesFilter : CheckBoxFilterList(
"Countries",
SFlixFiltersData.countries.map { CheckBoxVal(it.first, false) },
)
val filterList = AnimeFilterList(
TypeFilter(),
QualityFilter(),
ReleaseYearFilter(),
AnimeFilter.Separator(),
GenresFilter(),
CountriesFilter(),
)
data class FilterSearchParams(
val type: String = "",
val quality: String = "",
val releaseYear: String = "",
val genres: String = "",
val countries: String = "",
)
internal fun getSearchParameters(filters: AnimeFilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<QualityFilter>(),
filters.asQueryPart<ReleaseYearFilter>(),
filters.parseCheckbox<GenresFilter>(SFlixFiltersData.genres),
filters.parseCheckbox<CountriesFilter>(SFlixFiltersData.countries),
)
}
private object SFlixFiltersData {
val all = Pair("All", "all")
val types = arrayOf(
all,
Pair("Movies", "movies"),
Pair("TV Shows", "tv"),
)
val qualities = arrayOf(
all,
Pair("HD", "HD"),
Pair("SD", "SD"),
Pair("CAM", "CAM"),
)
val years = arrayOf(
all,
Pair("2022", "2022"),
Pair("2021", "2021"),
Pair("2020", "2020"),
Pair("2019", "2019"),
Pair("2018", "2018"),
Pair("Older", "older-2018"),
)
val genres = arrayOf(
Pair("Action", "10"),
Pair("Action & Adventure", "24"),
Pair("Adventure", "18"),
Pair("Animation", "3"),
Pair("Biography", "37"),
Pair("Comedy", "7"),
Pair("Crime", "2"),
Pair("Documentary", "11"),
Pair("Drama", "4"),
Pair("Family", "9"),
Pair("Fantasy", "13"),
Pair("History", "19"),
Pair("Horror", "14"),
Pair("Kids", "27"),
Pair("Music", "15"),
Pair("Mystery", "1"),
Pair("News", "34"),
Pair("Reality", "22"),
Pair("Romance", "12"),
Pair("Sci-Fi & Fantasy", "31"),
Pair("Science Fiction", "5"),
Pair("Soap", "35"),
Pair("Talk", "29"),
Pair("Thriller", "16"),
Pair("TV Movie", "8"),
Pair("War", "17"),
Pair("War & Politics", "28"),
Pair("Western", "6"),
)
val countries = arrayOf(
Pair("Argentina", "11"),
Pair("Australia", "151"),
Pair("Austria", "4"),
Pair("Belgium", "44"),
Pair("Brazil", "190"),
Pair("Canada", "147"),
Pair("China", "101"),
Pair("Czech Republic", "231"),
Pair("Denmark", "222"),
Pair("Finland", "158"),
Pair("France", "3"),
Pair("Germany", "96"),
Pair("Hong Kong", "93"),
Pair("Hungary", "72"),
Pair("India", "105"),
Pair("Ireland", "196"),
Pair("Israel", "24"),
Pair("Italy", "205"),
Pair("Japan", "173"),
Pair("Luxembourg", "91"),
Pair("Mexico", "40"),
Pair("Netherlands", "172"),
Pair("New Zealand", "122"),
Pair("Norway", "219"),
Pair("Poland", "23"),
Pair("Romania", "170"),
Pair("Russia", "109"),
Pair("South Africa", "200"),
Pair("South Korea", "135"),
Pair("Spain", "62"),
Pair("Sweden", "114"),
Pair("Switzerland", "41"),
Pair("Taiwan", "119"),
Pair("Thailand", "57"),
Pair("United Kingdom", "180"),
Pair("United States of America", "129"),
)
}
}

View File

@ -1,66 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.extractors
import eu.kanade.tachiyomi.animeextension.en.sflix.utils.Decryptor
import eu.kanade.tachiyomi.network.GET
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
class SFlixExtractor(private val client: OkHttpClient) {
// Prevent (automatic) caching the .JS file for different episodes, because it
// changes everytime, and a cached old .js will have a invalid AES password,
// invalidating the decryption algorithm.
// We cache it manually when initializing the class.
private val cacheControl = CacheControl.Builder().noStore().build()
private val newClient = client.newBuilder()
.cache(null)
.build()
companion object {
// its the same .js file for any server it may use,
// so we choose rabbitstream arbitrarily
private const val JS_URL = "https://rabbitstream.net/js/player/prod/e4-player.min.js"
// unlike the case of the .js file, here it is not possible to
// simply use the same host.
private const val SOURCES_PATH = "/ajax/embed-4/getSources?id="
}
// This will create a lag of 1~3s at the initialization of the class, but the
// speedup of the manual cache will be worth it.
private val cachedJs by lazy {
newClient.newCall(GET(JS_URL, cache = cacheControl)).execute()
.body.string()
}
init { cachedJs }
fun getSourcesJson(url: String): String? {
val id = url.substringAfter("/embed-4/", "")
.substringBefore("?", "").ifEmpty { return null }
val serverUrl = url.substringBefore("/embed")
val srcRes = newClient.newCall(
GET(
serverUrl + SOURCES_PATH + id,
headers = Headers.headersOf("x-requested-with", "XMLHttpRequest"),
cache = cacheControl,
),
)
.execute()
.body.string()
val key = newClient.newCall(GET("https://raw.githubusercontent.com/enimax-anime/key/e4/key.txt"))
.execute()
.body.string()
// encrypted data will start with "U2Fsd..." because they put
// "Salted__" at the start of encrypted data, thanks openssl
// if its not encrypted, then return it
if ("\"sources\":\"U2FsdGVk" !in srcRes) return srcRes
if (!srcRes.contains("{\"sources\":")) return null
val encrypted = srcRes.substringAfter("sources\":\"").substringBefore("\"")
val decrypted = Decryptor.decrypt(encrypted, key) ?: return null
val end = srcRes.replace("\"$encrypted\"", decrypted)
return end
}
}

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.animeextension.en.sflix.utils
import app.cash.quickjs.QuickJs
// For e4.min.js
object FindPassword {
fun getPassword(js: String): String {
val funcName = js.substringBefore("CryptoJS[")
.substringBeforeLast("document")
.substringAfterLast(",")
.substringBefore("=")
val suspiciousPass = js.substringAfter(":" + funcName)
.substringAfter(",")
.substringBefore("||")
.substringBeforeLast(")")
if (suspiciousPass.startsWith("'")) {
return suspiciousPass.trim('\'')
}
return getPasswordFromJS(js, "(" + suspiciousPass.substringAfter("("))
}
private fun getPasswordFromJS(js: String, getKeyArgs: String): String {
var script = "(function" + js.substringBefore(",(!function")
.substringAfter("(function") + ")"
val decoderFunName = script.substringAfter("=").substringBefore(",")
val decoderFunPrefix = "function " + decoderFunName
var decoderFunBody = js.substringAfter(decoderFunPrefix)
val decoderFunSuffix = decoderFunName + decoderFunBody.substringBefore("{") + ";}"
decoderFunBody = (
decoderFunPrefix +
decoderFunBody.substringBefore(decoderFunSuffix) +
decoderFunSuffix
)
if ("=[" !in js.substring(0, 30)) {
val superArrayName = decoderFunBody.substringAfter("=")
.substringBefore(";")
val superArrayPrefix = "function " + superArrayName
val superArraySuffix = "return " + superArrayName + ";}"
val superArrayBody = (
superArrayPrefix +
js.substringAfter(superArrayPrefix)
.substringBefore(superArraySuffix) +
superArraySuffix
)
script += "\n\n" + superArrayBody
}
script += "\n\n" + decoderFunBody
script += "\n\n$decoderFunName$getKeyArgs"
val qjs = QuickJs.create()
// this part can be really slow, like 5s or even more >:(
val result = qjs.evaluate(script).toString()
qjs.close()
return result
}
}