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

View File

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }} individualMatrix: ${{ steps.generate-matrices.outputs.individualMatrix }}
#multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }} multisrcMatrix: ${{ steps.generate-matrices.outputs.multisrcMatrix }}
env: env:
CI_MODULE_GEN: true CI_MODULE_GEN: true
steps: steps:
@ -49,18 +49,18 @@ jobs:
java-version: 11 java-version: 11
distribution: adopt distribution: adopt
# - name: Generate multisrc sources - name: Generate multisrc sources
# uses: gradle/gradle-command-action@v2 uses: gradle/gradle-command-action@v2
# with: with:
# arguments: :multisrc:generateExtensions arguments: :multisrc:generateExtensions
- name: Get number of modules - name: Get number of modules
run: | run: |
set -x 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_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 - id: generate-matrices
name: Create output matrices name: Create output matrices
@ -68,68 +68,66 @@ jobs:
with: with:
script: | script: |
const numIndividualModules = process.env.NUM_INDIVIDUAL_MODULES; 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 chunkSize = process.env.CI_CHUNK_SIZE;
const numIndividualChunks = Math.ceil(numIndividualModules / chunkSize); 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(`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('individualMatrix', { 'chunk': [...Array(numIndividualChunks).keys()] });
//core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] }); core.setOutput('multisrcMatrix', { 'chunk': [...Array(numMultisrcChunks).keys()] });
#build_multisrc: build_multisrc:
# name: Build multisrc modules name: Build multisrc modules
# needs: prepare needs: prepare
# runs-on: ubuntu-latest runs-on: ubuntu-latest
# strategy: strategy:
# matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }} matrix: ${{ fromJSON(needs.prepare.outputs.multisrcMatrix) }}
# steps: steps:
# - name: Checkout master branch - name: Checkout master branch
# uses: actions/checkout@v3 uses: actions/checkout@v3
# with:
# ref: master
# - name: Set up JDK - name: Set up JDK
# uses: actions/setup-java@v3 uses: actions/setup-java@v3
# with: with:
# java-version: 11 java-version: 11
# distribution: adopt distribution: adopt
# - name: Prepare signing key - name: Prepare signing key
# run: | run: |
# echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
# - name: Generate sources from the multi-source library - name: Generate sources from the multi-source library
# uses: gradle/gradle-command-action@v2 uses: gradle/gradle-command-action@v2
# env: env:
# CI_MODULE_GEN: "true" CI_MODULE_GEN: "true"
# with: with:
# arguments: :multisrc:generateExtensions arguments: :multisrc:generateExtensions
# - name: Build extensions (chunk ${{ matrix.chunk }}) - name: Build extensions (chunk ${{ matrix.chunk }})
# uses: gradle/gradle-command-action@v2 uses: gradle/gradle-command-action@v2
# env: env:
# CI_MULTISRC: "true" CI_MULTISRC: "true"
# CI_CHUNK_NUM: ${{ matrix.chunk }} CI_CHUNK_NUM: ${{ matrix.chunk }}
# ALIAS: ${{ secrets.ALIAS }} ALIAS: ${{ secrets.ALIAS }}
# KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }} KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
# KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
# with: with:
# arguments: assembleRelease arguments: assembleRelease
# - name: Upload APKs (chunk ${{ matrix.chunk }}) - name: Upload APKs (chunk ${{ matrix.chunk }})
# uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
# if: "github.repository == 'jmir1/aniyomi-extensions'" if: "github.repository == 'jmir1/aniyomi-extensions'"
# with: with:
# name: "multisrc-apks-${{ matrix.chunk }}" name: "multisrc-apks-${{ matrix.chunk }}"
# path: "**/*.apk" path: "**/*.apk"
# retention-days: 1 retention-days: 1
# - name: Clean up CI files - name: Clean up CI files
# run: rm signingkey.jks run: rm signingkey.jks
build_individual: build_individual:
name: Build individual modules name: Build individual modules
@ -177,6 +175,7 @@ jobs:
name: Publish repo name: Publish repo
needs: needs:
- build_individual - build_individual
- build_multisrc
if: "github.repository == 'jmir1/aniyomi-extensions'" if: "github.repository == 'jmir1/aniyomi-extensions'"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: 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.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.animeextension.en.sflix.extractors.SFlixExtractor
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage 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.model.Video
import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource import eu.kanade.tachiyomi.animesource.online.ParsedAnimeHttpSource
import eu.kanade.tachiyomi.lib.doodextractor.DoodExtractor 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.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
@ -23,10 +24,6 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json 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.Headers
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -37,18 +34,18 @@ import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() { abstract class DopeFlix(
override val name: String,
override val name = "SFlix" override val lang: String,
private val domainArray: Array<String>,
private val defaultDomain: String,
) : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override val baseUrl by lazy { 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 supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient override val client: OkHttpClient = network.cloudflareClient
@ -57,7 +54,9 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000) 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() override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Referer", "$baseUrl/") .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 popularAnimeSelector(): String = "div.film_list-wrap div.flw-item div.film-poster"
override fun popularAnimeRequest(page: Int): Request { 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") return GET("$baseUrl/$type?page=$page")
} }
@ -148,7 +147,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun videoListParse(response: Response): List<Video> { override fun videoListParse(response: Response): List<Video> {
val doc = response.asJsoup() val doc = response.asJsoup()
val episodeReferer = Headers.headersOf("Referer", response.request.header("referer")!!) 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") val videoList = doc.select("ul.fss-list a.btn-play")
.parallelMap { server -> .parallelMap { server ->
val name = server.selectFirst("span")!!.text() val name = server.selectFirst("span")!!.text()
@ -179,18 +178,13 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
private fun getVideosFromServer(source: String, name: String): List<Video>? { private fun getVideosFromServer(source: String, name: String): List<Video>? {
if (!source.contains("{\"sources\":[{\"file\":\"")) return null if (!source.contains("{\"sources\":[{\"file\":\"")) return null
val json = json.decodeFromString<JsonObject>(source) val response = json.decodeFromString<VideoDto>(source)
val masterUrl = json["sources"]!!.jsonArray[0].jsonObject["file"]!!.jsonPrimitive.content val masterUrl = response.sources.first().file
val subs2 = mutableListOf<Track>() val subs2 = response.tracks
json["tracks"]?.jsonArray ?.filter { it.kind == "captions" }
?.filter { it.jsonObject["kind"]!!.jsonPrimitive.content == "captions" } ?.mapNotNull {
?.map { track -> runCatching { Track(it.file, it.label) }.getOrNull()
val trackUrl = track.jsonObject["file"]!!.jsonPrimitive.content } ?: emptyList<Track>()
val lang = track.jsonObject["label"]!!.jsonPrimitive.content
try {
subs2.add(Track(trackUrl, lang))
} catch (e: Error) {}
} ?: emptyList()
val subs = subLangOrder(subs2) val subs = subLangOrder(subs2)
if (masterUrl.contains("playlist.m3u8")) { if (masterUrl.contains("playlist.m3u8")) {
val prefix = "#EXT-X-STREAM-INF:" val prefix = "#EXT-X-STREAM-INF:"
@ -222,39 +216,17 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
} }
override fun List<Video>.sort(): List<Video> { override fun List<Video>.sort(): List<Video> {
val quality = preferences.getString(PREF_QUALITY_KEY, null) val quality = preferences.getString(PREF_QUALITY_KEY, PREF_QUALITY_DEFAULT)!!
if (quality != null) { return sortedWith(
val newList = mutableListOf<Video>() compareBy { it.quality.contains(quality) },
var preferred = 0 ).reversed()
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> { private fun subLangOrder(tracks: List<Track>): List<Track> {
val language = preferences.getString(PREF_SUB_KEY, null) val language = preferences.getString(PREF_SUB_KEY, PREF_SUB_DEFAULT)!!
if (language != null) { return tracks.sortedWith(
val newList = mutableListOf<Track>() compareBy { it.lang.contains(language) },
var preferred = 0 ).reversed()
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 videoListSelector() = throw Exception("not used")
@ -272,7 +244,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeSelector(): String = popularAnimeSelector() override fun searchAnimeSelector(): String = popularAnimeSelector()
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> { 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)) return client.newCall(searchAnimeRequest(page, query, params))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
@ -282,7 +254,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
override fun searchAnimeRequest(page: Int, query: String, filters: AnimeFilterList): Request = throw Exception("not used") 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 url = if (query.isNotBlank()) {
val fixedQuery = query.replace(" ", "-") val fixedQuery = query.replace(" ", "-")
"$baseUrl/search/$fixedQuery?page=$page" "$baseUrl/search/$fixedQuery?page=$page"
@ -301,7 +273,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
return GET(url, headers) return GET(url, headers)
} }
override fun getFilterList(): AnimeFilterList = SFlixFilters.filterList override fun getFilterList(): AnimeFilterList = DopeFlixFilters.filterList
// =========================== Anime Details ============================ // =========================== Anime Details ============================
override fun animeDetailsParse(document: Document): SAnime { 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 latestUpdatesRequest(page: Int): Request = GET("$baseUrl/home/")
override fun latestUpdatesSelector(): String { 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" 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 { val domainPref = ListPreference(screen.context).apply {
key = PREF_DOMAIN_KEY key = PREF_DOMAIN_KEY
title = PREF_DOMAIN_TITLE title = PREF_DOMAIN_TITLE
entries = PREF_DOMAIN_LIST entries = domainArray
entryValues = PREF_DOMAIN_LIST entryValues = domainArray
setDefaultValue("sflix.to") setDefaultValue(defaultDomain)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -362,7 +334,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_QUALITY_TITLE title = PREF_QUALITY_TITLE
entries = PREF_QUALITY_LIST entries = PREF_QUALITY_LIST
entryValues = PREF_QUALITY_LIST entryValues = PREF_QUALITY_LIST
setDefaultValue("1080p") setDefaultValue(PREF_QUALITY_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -377,7 +349,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_SUB_TITLE title = PREF_SUB_TITLE
entries = PREF_SUB_LANGUAGES entries = PREF_SUB_LANGUAGES
entryValues = PREF_SUB_LANGUAGES entryValues = PREF_SUB_LANGUAGES
setDefaultValue("English") setDefaultValue(PREF_SUB_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -392,7 +364,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_LATEST_TITLE title = PREF_LATEST_TITLE
entries = PREF_LATEST_PAGES entries = PREF_LATEST_PAGES
entryValues = PREF_LATEST_PAGES entryValues = PREF_LATEST_PAGES
setDefaultValue("Movies") setDefaultValue(PREF_LATEST_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -407,7 +379,7 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
title = PREF_POPULAR_TITLE title = PREF_POPULAR_TITLE
entries = PREF_POPULAR_ENTRIES entries = PREF_POPULAR_ENTRIES
entryValues = PREF_POPULAR_VALUES entryValues = PREF_POPULAR_VALUES
setDefaultValue("movie") setDefaultValue(PREF_POPULAR_DEFAULT)
summary = "%s" summary = "%s"
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
@ -434,14 +406,15 @@ class SFlix : ConfigurableAnimeSource, ParsedAnimeHttpSource() {
companion object { companion object {
private const val PREF_DOMAIN_KEY = "preferred_domain_new" private const val PREF_DOMAIN_KEY = "preferred_domain_new"
private const val PREF_DOMAIN_TITLE = "Preferred domain (requires app restart)" 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_KEY = "preferred_quality"
private const val PREF_QUALITY_TITLE = "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 val PREF_QUALITY_LIST = arrayOf("1080p", "720p", "480p", "360p")
private const val PREF_SUB_KEY = "preferred_subLang" private const val PREF_SUB_KEY = "preferred_subLang"
private const val PREF_SUB_TITLE = "Preferred sub language" private const val PREF_SUB_TITLE = "Preferred sub language"
private const val PREF_SUB_DEFAULT = "English"
private val PREF_SUB_LANGUAGES = arrayOf( private val PREF_SUB_LANGUAGES = arrayOf(
"Arabic", "English", "French", "German", "Hungarian", "Arabic", "English", "French", "German", "Hungarian",
"Italian", "Japanese", "Portuguese", "Romanian", "Russian", "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_KEY = "preferred_latest_page"
private const val PREF_LATEST_TITLE = "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 val PREF_LATEST_PAGES = arrayOf("Movies", "TV Shows")
private const val PREF_POPULAR_KEY = "preferred_popular_page_new" private const val PREF_POPULAR_KEY = "preferred_popular_page_new"
private const val PREF_POPULAR_TITLE = "Preferred popular page" 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_ENTRIES = PREF_LATEST_PAGES
private val PREF_POPULAR_VALUES = arrayOf("movie", "tv-show") 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.AnimeFilter
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
object DopeBoxFilters { object DopeFlixFilters {
open class QueryPartFilter( open class QueryPartFilter(
displayName: String, displayName: String,
@ -46,17 +46,17 @@ object DopeBoxFilters {
} }
} }
class TypeFilter : QueryPartFilter("Type", DopeBoxFiltersData.types) class TypeFilter : QueryPartFilter("Type", DopeFlixFiltersData.types)
class QualityFilter : QueryPartFilter("Quality", DopeBoxFiltersData.qualities) class QualityFilter : QueryPartFilter("Quality", DopeFlixFiltersData.qualities)
class ReleaseYearFilter : QueryPartFilter("Released at", DopeBoxFiltersData.years) class ReleaseYearFilter : QueryPartFilter("Released at", DopeFlixFiltersData.years)
class GenresFilter : CheckBoxFilterList( class GenresFilter : CheckBoxFilterList(
"Genres", "Genres",
DopeBoxFiltersData.genres.map { CheckBoxVal(it.first, false) }, DopeFlixFiltersData.genres.map { CheckBoxVal(it.first, false) },
) )
class CountriesFilter : CheckBoxFilterList( class CountriesFilter : CheckBoxFilterList(
"Countries", "Countries",
DopeBoxFiltersData.countries.map { CheckBoxVal(it.first, false) }, DopeFlixFiltersData.countries.map { CheckBoxVal(it.first, false) },
) )
val filterList = AnimeFilterList( val filterList = AnimeFilterList(
@ -83,12 +83,12 @@ object DopeBoxFilters {
filters.asQueryPart<TypeFilter>(), filters.asQueryPart<TypeFilter>(),
filters.asQueryPart<QualityFilter>(), filters.asQueryPart<QualityFilter>(),
filters.asQueryPart<ReleaseYearFilter>(), filters.asQueryPart<ReleaseYearFilter>(),
filters.parseCheckbox<GenresFilter>(DopeBoxFiltersData.genres), filters.parseCheckbox<GenresFilter>(DopeFlixFiltersData.genres),
filters.parseCheckbox<CountriesFilter>(DopeBoxFiltersData.countries), filters.parseCheckbox<CountriesFilter>(DopeFlixFiltersData.countries),
) )
} }
private object DopeBoxFiltersData { private object DopeFlixFiltersData {
val all = Pair("All", "all") val all = Pair("All", "all")
val types = arrayOf( 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 android.util.Base64
import java.security.DigestException 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") { if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
// Local development (full project build) // Local development (full project build)
//include(":multisrc") include(":multisrc")
//project(":multisrc").projectDir = File("multisrc") project(":multisrc").projectDir = File("multisrc")
// Loads all extensions // Loads all extensions
File(rootDir, "src").eachDir { dir -> 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 // Loads generated extensions from multisrc
//File(rootDir, "generated-src").eachDir { dir -> File(rootDir, "generated-src").eachDir { dir ->
// dir.eachDir { subdir -> dir.eachDir { subdir ->
// val name = ":extensions:multisrc:${dir.name}:${subdir.name}" val name = ":extensions:multisrc:${dir.name}:${subdir.name}"
// include(name) include(name)
// project(name).projectDir = File("generated-src/${dir.name}/${subdir.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, * 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 chunkSize = System.getenv("CI_CHUNK_SIZE").toInt()
val chunk = System.getenv("CI_CHUNK_NUM").toInt() val chunk = System.getenv("CI_CHUNK_NUM").toInt()
/*if (isMultisrc) { if (isMultisrc) {
include(":multisrc") include(":multisrc")
project(":multisrc").projectDir = File("multisrc") project(":multisrc").projectDir = File("multisrc")
@ -74,15 +74,15 @@ if (System.getenv("CI") == null || System.getenv("CI_MODULE_GEN") == "true") {
include(name) include(name)
project(name).projectDir = File("generated-src/${it.parentFile.name}/${it.name}") project(name).projectDir = File("generated-src/${it.parentFile.name}/${it.name}")
} }
} else {*/ } else {
// Loads individual extensions // Loads individual extensions
File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach { File(rootDir, "src").getChunk(chunk, chunkSize)?.forEach {
val name = ":extensions:individual:${it.parentFile.name}:${it.name}" val name = ":extensions:individual:${it.parentFile.name}:${it.name}"
println(name) println(name)
include(name) include(name)
project(name).projectDir = File("src/${it.parentFile.name}/${it.name}") project(name).projectDir = File("src/${it.parentFile.name}/${it.name}")
} }
//} }
} }
fun File.getChunk(chunk: Int, chunkSize: Int): List<File>? { 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
}
}