Compare commits
No commits in common. "9602aa5dd546c1726adc22ffdad67b87a6ff952c" and "c5c6d77479d473b7acd5251d603d224de1d31132" have entirely different histories.
9602aa5dd5
...
c5c6d77479
50
.github/workflows/build_push.yml
vendored
@ -25,6 +25,51 @@ jobs:
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
build_multisrc:
|
||||
name: Build multisrc modules
|
||||
needs: prepare
|
||||
runs-on: arch
|
||||
steps:
|
||||
- name: Checkout master branch
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Prepare signing key
|
||||
run: |
|
||||
echo ${{ secrets.SIGNING_KEY }} | base64 -d > signingkey.jks
|
||||
|
||||
- name: Generate sources from the multi-source library
|
||||
uses: gradle/gradle-build-action@v2
|
||||
env:
|
||||
CI_MODULE_GEN: "true"
|
||||
with:
|
||||
arguments: :multisrc:generateExtensions
|
||||
|
||||
- name: Build extensions
|
||||
uses: gradle/gradle-build-action@v2
|
||||
env:
|
||||
CI_MULTISRC: "true"
|
||||
ALIAS: ${{ secrets.ALIAS }}
|
||||
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Upload APKs
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: "multisrc-apks"
|
||||
path: "**/*.apk"
|
||||
retention-days: 1
|
||||
|
||||
- name: Clean up CI files
|
||||
run: rm signingkey.jks
|
||||
|
||||
build_individual:
|
||||
name: Build individual modules
|
||||
@ -47,10 +92,12 @@ jobs:
|
||||
- name: Build extensions
|
||||
uses: gradle/gradle-build-action@v2
|
||||
env:
|
||||
CI_MULTISRC: "false"
|
||||
ALIAS: ${{ secrets.ALIAS }}
|
||||
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
run: ./gradlew -p src assembleRelease
|
||||
with:
|
||||
arguments: assembleRelease
|
||||
|
||||
- name: Upload APKs
|
||||
uses: actions/upload-artifact@v3
|
||||
@ -65,6 +112,7 @@ jobs:
|
||||
publish_repo:
|
||||
name: Publish repo
|
||||
needs:
|
||||
- build_multisrc
|
||||
- build_individual
|
||||
runs-on: arch
|
||||
steps:
|
||||
|
11
.run/MadaraGenerator.run.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MadaraGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||
<module name="tachiyomi-extensions.multisrc.main" />
|
||||
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.madara.MadaraGenerator" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=madara" />
|
||||
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=madara" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
11
.run/MangaThemesiaGenerator.run.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MangaThemesiaGenerator" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||
<module name="tachiyomi-extensions.multisrc.main" />
|
||||
<option name="MAIN_CLASS_NAME" value="eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaGenerator" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktFormat" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangathemesia" />
|
||||
<option name="Gradle.BeforeRunTask" enabled="true" tasks="ktLint" externalProjectPath="$PROJECT_DIR$/multisrc" vmOptions="" scriptParameters="-Ptheme=mangathemesia" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
@ -423,15 +423,14 @@ will be cached.
|
||||
|
||||
```kotlin
|
||||
private fun parseDate(dateStr: String): Long {
|
||||
return try {
|
||||
dateFormat.parse(dateStr)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
return runCatching { DATE_FORMATTER.parse(dateStr)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
2
buildSrc/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.gradle/
|
||||
build/
|
@ -3,14 +3,5 @@ plugins {
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
google()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.gradle.agp)
|
||||
implementation(libs.gradle.kotlin)
|
||||
implementation(libs.gradle.serialization)
|
||||
implementation(libs.gradle.kotlinter)
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
import org.gradle.api.plugins.ExtensionAware
|
||||
import org.gradle.kotlin.dsl.extra
|
||||
|
||||
var ExtensionAware.baseVersionCode: Int
|
||||
get() = extra.get("baseVersionCode") as Int
|
||||
set(value) = extra.set("baseVersionCode", value)
|
@ -1,64 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("kotlinx-serialization")
|
||||
id("org.jmailen.kotlinter")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
}
|
||||
|
||||
namespace = "eu.kanade.tachiyomi.multisrc.${project.name}"
|
||||
|
||||
sourceSets {
|
||||
named("main") {
|
||||
manifest.srcFile("AndroidManifest.xml")
|
||||
java.setSrcDirs(listOf("src"))
|
||||
res.setSrcDirs(listOf("res"))
|
||||
assets.setSrcDirs(listOf("assets"))
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
resValues = false
|
||||
shaders = false
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
}
|
||||
}
|
||||
|
||||
kotlinter {
|
||||
experimentalRules = true
|
||||
disabledRules = arrayOf(
|
||||
"experimental:argument-list-wrapping", // Doesn't play well with Android Studio
|
||||
"experimental:comment-wrapping",
|
||||
)
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
// TODO: use versionCatalogs.named("libs") in Gradle 8.5
|
||||
val libs = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
dependencies {
|
||||
compileOnly(libs.findBundle("common").get())
|
||||
}
|
||||
|
||||
tasks {
|
||||
preBuild {
|
||||
dependsOn(lintKotlin)
|
||||
}
|
||||
|
||||
if (System.getenv("CI") != "true") {
|
||||
lintKotlin {
|
||||
dependsOn(formatKotlin)
|
||||
}
|
||||
}
|
||||
}
|
@ -8,8 +8,6 @@ assert !ext.has("libVersion")
|
||||
|
||||
assert extName.chars().max().asInt < 0x180 : "Extension name should be romanized"
|
||||
|
||||
Project theme = ext.has("themePkg") ? project(":lib-multisrc:$themePkg") : null
|
||||
|
||||
android {
|
||||
compileSdk AndroidConfig.compileSdk
|
||||
|
||||
@ -27,7 +25,7 @@ android {
|
||||
minSdk AndroidConfig.minSdk
|
||||
targetSdk AndroidConfig.targetSdk
|
||||
applicationIdSuffix project.parent.name + "." + project.name
|
||||
versionCode theme == null ? extVersionCode : theme.baseVersionCode + overrideVersionCode
|
||||
versionCode extVersionCode
|
||||
versionName "1.4.$versionCode"
|
||||
base {
|
||||
archivesName = "tachiyomi-$applicationIdSuffix-v$versionName"
|
||||
@ -36,18 +34,8 @@ android {
|
||||
manifestPlaceholders = [
|
||||
appName : "Tachiyomi: $extName",
|
||||
extClass: extClass,
|
||||
nsfw : project.ext.find("isNsfw") ? 1 : 0,
|
||||
nsfw: project.ext.find("isNsfw") ? 1 : 0,
|
||||
]
|
||||
String baseUrl = project.ext.find("baseUrl") ?: ""
|
||||
if (theme != null && !baseUrl.isEmpty()) {
|
||||
def split = baseUrl.split("://")
|
||||
assert split.length == 2
|
||||
def path = split[1].split("/")
|
||||
manifestPlaceholders += [
|
||||
SOURCEHOST : path[0],
|
||||
SOURCESCHEME: split[0],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@ -100,7 +88,6 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
if (theme != null) implementation(theme) // Overrides core launcher icons
|
||||
implementation(project(":core"))
|
||||
compileOnly(libs.bundles.common)
|
||||
}
|
||||
@ -121,6 +108,4 @@ tasks.register("writeManifestFile") {
|
||||
}
|
||||
|
||||
preBuild.dependsOn(writeManifestFile, lintKotlin)
|
||||
if (System.getenv("CI") != "true") {
|
||||
lintKotlin.dependsOn(formatKotlin)
|
||||
}
|
||||
lintKotlin.dependsOn(formatKotlin)
|
||||
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:synchrony"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 7
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:speedbinb"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 9
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 22
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 20
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 10
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
Before Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 27 KiB |
@ -1,271 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.keyoapp
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
abstract class Keyoapp(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
final override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
|
||||
|
||||
override fun popularMangaSelector(): String = "div.flex-col div.grid > div.group.border"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||
thumbnail_url = element.getImageUrl("*[style*=background-image]")
|
||||
element.selectFirst("a[href]")!!.run {
|
||||
title = attr("title")
|
||||
setUrlWithoutDomain(attr("abs:href"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
runCatching { fetchGenres() }
|
||||
return super.popularMangaParse(response)
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/latest/", headers)
|
||||
|
||||
override fun latestUpdatesSelector(): String = "div.grid > div.group"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
runCatching { fetchGenres() }
|
||||
return super.latestUpdatesParse(response)
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
||||
addPathSegment("series")
|
||||
addPathSegment("")
|
||||
if (query.isNotBlank()) {
|
||||
addQueryParameter("q", query)
|
||||
}
|
||||
filters.firstOrNull { it is GenreList }?.also {
|
||||
val filter = it as GenreList
|
||||
filter.state
|
||||
.filter { it.state }
|
||||
.forEach { genre ->
|
||||
addQueryParameter("genre", genre.id)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "#searched_series_page > button"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
runCatching { fetchGenres() }
|
||||
val document = response.asJsoup()
|
||||
|
||||
val query = response.request.url.queryParameter("q") ?: ""
|
||||
val genres = response.request.url.queryParameterValues("genre")
|
||||
|
||||
val mangaList = document.select(searchMangaSelector())
|
||||
.toTypedArray()
|
||||
.filter { it.attr("title").contains(query, true) }
|
||||
.filter { entry ->
|
||||
val entryGenres = json.decodeFromString<List<String>>(entry.attr("tags"))
|
||||
genres.all { genre -> entryGenres.any { it.equals(genre, true) } }
|
||||
}
|
||||
.map(::searchMangaFromElement)
|
||||
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
|
||||
// Filters
|
||||
|
||||
/**
|
||||
* Automatically fetched genres from the source to be used in the filters.
|
||||
*/
|
||||
private var genresList: List<Genre> = emptyList()
|
||||
|
||||
/**
|
||||
* Inner variable to control the genre fetching failed state.
|
||||
*/
|
||||
private var fetchGenresFailed: Boolean = false
|
||||
|
||||
/**
|
||||
* Inner variable to control how much tries the genres request was called.
|
||||
*/
|
||||
private var fetchGenresAttempts: Int = 0
|
||||
|
||||
class Genre(name: String, val id: String = name) : Filter.CheckBox(name)
|
||||
|
||||
protected class GenreList(title: String, genres: List<Genre>) : Filter.Group<Genre>(title, genres)
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
return if (genresList.isNotEmpty()) {
|
||||
FilterList(
|
||||
GenreList("Genres", genresList),
|
||||
)
|
||||
} else {
|
||||
FilterList(
|
||||
Filter.Header("Press 'Reset' to attempt to show the genres"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the genres from the source to be used in the filters.
|
||||
*/
|
||||
protected open fun fetchGenres() {
|
||||
if (fetchGenresAttempts <= 3 && (genresList.isEmpty() || fetchGenresFailed)) {
|
||||
val genres = runCatching {
|
||||
client.newCall(genresRequest()).execute()
|
||||
.use { parseGenres(it.asJsoup()) }
|
||||
}
|
||||
|
||||
fetchGenresFailed = genres.isFailure
|
||||
genresList = genres.getOrNull().orEmpty()
|
||||
fetchGenresAttempts++
|
||||
}
|
||||
}
|
||||
|
||||
private fun genresRequest(): Request = GET("$baseUrl/series/", headers)
|
||||
|
||||
/**
|
||||
* Get the genres from the search page document.
|
||||
*
|
||||
* @param document The search page document
|
||||
*/
|
||||
protected open fun parseGenres(document: Document): List<Genre> {
|
||||
return document.select("#series_tags_page > button")
|
||||
.map { btn ->
|
||||
Genre(btn.text(), btn.attr("tag"))
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
title = document.selectFirst("div.grid > h1")!!.text()
|
||||
thumbnail_url = document.getImageUrl("div[class*=photoURL]")
|
||||
description = document.selectFirst("div.grid > div.overflow-hidden > p")?.text()
|
||||
status = document.selectFirst("div[alt=Status]").parseStatus()
|
||||
author = document.selectFirst("div[alt=Author]")?.text()
|
||||
artist = document.selectFirst("div[alt=Artist]")?.text()
|
||||
genre = document.select("div.grid:has(>h1) > div > a").joinToString { it.text() }
|
||||
}
|
||||
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"dropped" -> SManga.CANCELLED
|
||||
"paused" -> SManga.ON_HIATUS
|
||||
"completed" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapter list
|
||||
|
||||
override fun chapterListSelector(): String = "#chapters > a"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
||||
name = element.selectFirst(".text-sm")!!.text()
|
||||
element.selectFirst(".text-xs")?.run {
|
||||
date_upload = text().trim().parseDate()
|
||||
}
|
||||
}
|
||||
|
||||
// Image list
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("#pages > img").map {
|
||||
val index = it.attr("count").toInt()
|
||||
Page(index, document.location(), it.imgAttr("150"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
|
||||
// Utilities
|
||||
|
||||
// From mangathemesia
|
||||
private fun Element.imgAttr(width: String): String {
|
||||
val url = when {
|
||||
hasAttr("data-lazy-src") -> attr("abs:data-lazy-src")
|
||||
hasAttr("data-src") -> attr("abs:data-src")
|
||||
else -> attr("abs:src")
|
||||
}
|
||||
return url.toHttpUrl()
|
||||
.newBuilder()
|
||||
.addQueryParameter("w", width)
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun Element.getImageUrl(selector: String): String? {
|
||||
return this.selectFirst(selector)?.let {
|
||||
it.attr("style")
|
||||
.substringAfter(":url(", "")
|
||||
.substringBefore(")", "")
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.toHttpUrlOrNull()?.let {
|
||||
it.newBuilder()
|
||||
.setQueryParameter("w", "480")
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.parseDate(): Long {
|
||||
return runCatching { DATE_FORMATTER.parse(this)?.time }
|
||||
.getOrNull() ?: 0L
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMATTER by lazy {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 25
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,10 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 33
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:cryptoaes"))
|
||||
api(project(":lib:randomua"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 13
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 13
|
@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:randomua"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 5
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:randomua"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
@ -1,10 +0,0 @@
|
||||
filter_warning=Ignored if using text search
|
||||
filter_missing_warning=Press 'Reset' to attempt to show filters
|
||||
category_filter_title=Category
|
||||
status_filter_title=Status
|
||||
type_filter_title=Type
|
||||
year_filter_title=Year of release
|
||||
author_filter_title=Author
|
||||
tag_filter_title=Tag
|
||||
title_begins_with_filter_title=Title begins with
|
||||
sort_by_filter_title=Sort by
|
@ -1,10 +0,0 @@
|
||||
filter_warning=Ignorados si se realiza una búsqueda textual
|
||||
filter_missing_warning=Presione 'Restablecer' para intentar mostrar los filtros
|
||||
category_filter_title=Categoría
|
||||
status_filter_title=Estado
|
||||
type_filter_title=Tipo
|
||||
year_filter_title=Año de lanzamiento
|
||||
author_filter_title=Autor
|
||||
tag_filter_title=Etiqueta
|
||||
title_begins_with_filter_title=El título comienza con
|
||||
sort_by_filter_title=Ordenar por
|
@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 11
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 12
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
@ -1,9 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:dataimage"))
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 8
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 11
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 8
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 1
|
@ -1,23 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
}
|
||||
|
||||
namespace = "eu.kanade.tachiyomi.lib.cookieinterceptor"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.kotlin.stdlib)
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.cookieinterceptor
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class CookieInterceptor(
|
||||
private val domain: String,
|
||||
private val cookies: List<Pair<String, String>>
|
||||
) : Interceptor {
|
||||
constructor(domain: String, cookie: Pair<String, String>) : this(domain, listOf(cookie))
|
||||
|
||||
init {
|
||||
val url = "https://$domain/"
|
||||
cookies.forEach {
|
||||
val cookie = "${it.first}=${it.second}; Domain=$domain; Path=/"
|
||||
setCookie(url, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
if (!request.url.host.endsWith(domain)) return chain.proceed(request)
|
||||
|
||||
val cookieList = request.header("Cookie")?.split("; ") ?: emptyList()
|
||||
|
||||
if (cookies.all { (key, value) -> "$key=$value" in cookieList })
|
||||
return chain.proceed(request)
|
||||
|
||||
cookies.forEach { (key, value) ->
|
||||
setCookie("https://$domain/", "$key=$value; Domain=$domain; Path=/")
|
||||
}
|
||||
|
||||
val newCookie = buildList(cookieList.size + cookies.size) {
|
||||
cookieList.filterNotTo(this) { existing ->
|
||||
cookies.any { (key, _) ->
|
||||
existing.startsWith("$key=")
|
||||
}
|
||||
}
|
||||
cookies.forEach { (key, value) ->
|
||||
add("$key=$value")
|
||||
}
|
||||
}.joinToString("; ")
|
||||
|
||||
val newRequest = request.newBuilder()
|
||||
.header("Cookie", newCookie)
|
||||
.build()
|
||||
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
|
||||
private val cookieManager by lazy { CookieManager.getInstance() }
|
||||
|
||||
private fun setCookie(url: String, value: String) {
|
||||
try {
|
||||
cookieManager.setCookie(url, value)
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("kotlinx-serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AndroidConfig.minSdk
|
||||
}
|
||||
|
||||
namespace = "eu.kanade.tachiyomi.lib.speedbinb"
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.bundles.common)
|
||||
implementation(project(":lib:textinterceptor"))
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb
|
||||
|
||||
private const val URLSAFE_BASE64_LOOKUP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
|
||||
internal fun determineKeyPair(src: String?, ptbl: List<String>, ctbl: List<String>): Pair<String, String> {
|
||||
val i = mutableListOf(0, 0)
|
||||
|
||||
if (src != null) {
|
||||
val filename = src.substringAfterLast("/")
|
||||
|
||||
for (e in filename.indices) {
|
||||
i[e % 2] = i[e % 2] + filename[e].code
|
||||
}
|
||||
|
||||
i[0] = i[0] % 8
|
||||
i[1] = i[1] % 8
|
||||
}
|
||||
|
||||
return Pair(ptbl[i[0]], ctbl[i[1]])
|
||||
}
|
||||
|
||||
internal fun decodeScrambleTable(cid: String, sharedKey: String, table: String): String {
|
||||
val r = "$cid:$sharedKey"
|
||||
var e = r.toCharArray()
|
||||
.map { it.code }
|
||||
.reduceIndexed { index, acc, i -> acc + (i shl index % 16) } and 2147483647
|
||||
|
||||
if (e == 0) {
|
||||
e = 0x12345678
|
||||
}
|
||||
|
||||
return buildString(table.length) {
|
||||
for (s in table.indices) {
|
||||
e = e ushr 1 xor (1210056708 and -(1 and e))
|
||||
append(((table[s].code - 32 + e) % 94 + 32).toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun generateSharedKey(cid: String): String {
|
||||
val randomChars = randomChars(16)
|
||||
val cidRepeatCount = (16 + cid.length - 1) / cid.length
|
||||
val unk1 = buildString(cid.length * cidRepeatCount) {
|
||||
for (i in 0 until cidRepeatCount) {
|
||||
append(cid)
|
||||
}
|
||||
}
|
||||
val unk2 = unk1.substring(0, 16)
|
||||
val unk3 = unk1.substring(unk1.length - 16, unk1.length)
|
||||
var s = 0
|
||||
var h = 0
|
||||
var u = 0
|
||||
|
||||
return buildString(randomChars.length * 2) {
|
||||
for (i in randomChars.indices) {
|
||||
s = s xor randomChars[i].code
|
||||
h = h xor unk2[i].code
|
||||
u = u xor unk3[i].code
|
||||
|
||||
append(randomChars[i])
|
||||
append(URLSAFE_BASE64_LOOKUP[(s + h + u) and 63])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun randomChars(length: Int) = buildString(length) {
|
||||
for (i in 0 until length) {
|
||||
append(URLSAFE_BASE64_LOOKUP.random())
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
|
||||
private val COORD_REGEX = Regex("""^i:(\d+),(\d+)\+(\d+),(\d+)>(\d+),(\d+)$""")
|
||||
|
||||
@Serializable
|
||||
class BibContentInfo(
|
||||
val result: Int,
|
||||
val items: List<BibContentItem>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class BibContentItem(
|
||||
@SerialName("ContentID") val contentId: String,
|
||||
@SerialName("ContentsServer") val contentServer: String,
|
||||
@SerialName("ServerType") val serverType: Int,
|
||||
val stbl: String,
|
||||
val ttbl: String,
|
||||
val ptbl: String,
|
||||
val ctbl: String,
|
||||
@SerialName("p") val requestToken: String? = null,
|
||||
@SerialName("ViewMode") val viewMode: Int,
|
||||
@SerialName("ContentDate") val contentDate: String? = null,
|
||||
@SerialName("ShopURL") val shopUrl: String? = null,
|
||||
) {
|
||||
fun getSbcUrl(readerUrl: HttpUrl, cid: String) =
|
||||
contentServer.toHttpUrl().newBuilder().apply {
|
||||
when (serverType) {
|
||||
ServerType.DIRECT -> addPathSegment("content.js")
|
||||
ServerType.REST -> addPathSegment("content")
|
||||
ServerType.SBC -> {
|
||||
addPathSegment("sbcGetCntnt.php")
|
||||
setQueryParameter("cid", cid)
|
||||
requestToken?.let { setQueryParameter("p", it) }
|
||||
setQueryParameter("q", "1")
|
||||
setQueryParameter("vm", viewMode.toString())
|
||||
setQueryParameter("dmytime", contentDate ?: System.currentTimeMillis().toString())
|
||||
copyKeyParametersFrom(readerUrl)
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Unsupported ServerType value $serverType")
|
||||
}
|
||||
}.toString()
|
||||
}
|
||||
|
||||
object ServerType {
|
||||
const val SBC = 0
|
||||
const val DIRECT = 1
|
||||
const val REST = 2
|
||||
}
|
||||
|
||||
object ViewMode {
|
||||
const val COMMERCIAL = 1
|
||||
const val NON_MEMBER_TRIAL = 2
|
||||
const val MEMBER_TRIAL = 3
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PtImg(
|
||||
@SerialName("ptimg-version") val ptImgVersion: Int,
|
||||
val resources: PtImgResources,
|
||||
val views: List<PtImgViews>,
|
||||
) {
|
||||
val translations by lazy {
|
||||
views[0].coords.map { coord ->
|
||||
val v = COORD_REGEX.matchEntire(coord)!!.groupValues.drop(1).map { it.toInt() }
|
||||
PtImgTranslation(v[0], v[1], v[2], v[3], v[4], v[5])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class PtImgResources(
|
||||
val i: PtImgImage,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PtImgImage(
|
||||
val src: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PtImgViews(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val coords: Array<String>,
|
||||
)
|
||||
|
||||
class PtImgTranslation(val xsrc: Int, val ysrc: Int, val width: Int, val height: Int, val xdest: Int, val ydest: Int)
|
||||
|
||||
@Serializable
|
||||
class SBCContent(
|
||||
@SerialName("SBCVersion") val sbcVersion: String,
|
||||
val result: Int,
|
||||
val ttx: String,
|
||||
@SerialName("ImageClass") val imageClass: String? = null,
|
||||
)
|
@ -1,85 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.descrambler.PtBinbDescramblerA
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.descrambler.PtBinbDescramblerF
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.descrambler.PtImgDescrambler
|
||||
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
|
||||
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.io.IOException
|
||||
|
||||
class SpeedBinbInterceptor(private val json: Json) : Interceptor {
|
||||
|
||||
private val textInterceptor by lazy { TextInterceptor() }
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val host = request.url.host
|
||||
val filename = request.url.pathSegments.last()
|
||||
val fragment = request.url.fragment
|
||||
|
||||
return when {
|
||||
host == TextInterceptorHelper.HOST -> textInterceptor.intercept(chain)
|
||||
filename.endsWith(".ptimg.json") -> interceptPtImg(chain, request)
|
||||
fragment == null -> chain.proceed(request)
|
||||
fragment.startsWith("ptbinb,") -> interceptPtBinB(chain, request)
|
||||
else -> chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
private fun interceptPtImg(chain: Interceptor.Chain, request: Request): Response {
|
||||
val response = chain.proceed(request)
|
||||
val metadata = json.decodeFromString<PtImg>(response.body.string())
|
||||
val imageUrl = request.url.newBuilder()
|
||||
.setPathSegment(request.url.pathSize - 1, metadata.resources.i.src)
|
||||
.build()
|
||||
val imageResponse = chain.proceed(
|
||||
request.newBuilder().url(imageUrl).build(),
|
||||
)
|
||||
|
||||
if (metadata.translations.isEmpty()) {
|
||||
return imageResponse
|
||||
}
|
||||
|
||||
val image = BitmapFactory.decodeStream(imageResponse.body.byteStream())
|
||||
val descrambler = PtImgDescrambler(metadata)
|
||||
return imageResponse.newBuilder()
|
||||
.body(descrambler.descrambleImage(image)!!.toResponseBody(JPEG_MEDIA_TYPE))
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun interceptPtBinB(chain: Interceptor.Chain, request: Request): Response {
|
||||
val response = chain.proceed(request)
|
||||
val fragment = request.url.fragment!!
|
||||
val (s, u) = fragment.removePrefix("ptbinb,").split(",", limit = 2)
|
||||
|
||||
if (s.isEmpty() && u.isEmpty()) {
|
||||
return response
|
||||
}
|
||||
|
||||
val imageData = response.body.bytes()
|
||||
val image = BitmapFactory.decodeByteArray(imageData, 0, imageData.size)
|
||||
val descrambler = if (s[0] == '=' && u[0] == '=') {
|
||||
PtBinbDescramblerF(s, u, image.width, image.height)
|
||||
} else if (NUMERIC_CHARACTERS.contains(s[0]) && NUMERIC_CHARACTERS.contains(u[0])) {
|
||||
PtBinbDescramblerA(s, u, image.width, image.height)
|
||||
} else {
|
||||
throw IOException("Cannot select descrambler for key pair s=$s, u=$u")
|
||||
}
|
||||
val descrambled = descrambler.descrambleImage(image) ?: imageData
|
||||
|
||||
return response.newBuilder()
|
||||
.body(descrambled.toResponseBody(JPEG_MEDIA_TYPE))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
private const val NUMERIC_CHARACTERS = "0123456789"
|
||||
private val JPEG_MEDIA_TYPE = "image/jpeg".toMediaType()
|
@ -1,197 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb
|
||||
|
||||
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
/**
|
||||
* SpeedBinb is a reader for various Japanese manga sites.
|
||||
*
|
||||
* Versions (`SpeedBinb.VERSION` in DevTools console):
|
||||
* - Minimum version tested: `1.6650.0001`
|
||||
* - Maximum version tested: `1.6930.1101`
|
||||
*
|
||||
* These versions are only for reference purposes, and does not reflect the actual range
|
||||
* of versions this class can scrape.
|
||||
*/
|
||||
class SpeedBinbReader(
|
||||
private val client: OkHttpClient,
|
||||
private val headers: Headers,
|
||||
private val json: Json,
|
||||
private val highQualityMode: Boolean = false,
|
||||
) {
|
||||
private val isInterceptorAdded by lazy {
|
||||
client.interceptors.filterIsInstance<SpeedBinbInterceptor>().isNotEmpty()
|
||||
}
|
||||
|
||||
fun pageListParse(response: Response): List<Page> =
|
||||
pageListParse(response.asJsoup())
|
||||
|
||||
fun pageListParse(document: Document): List<Page> {
|
||||
// We throw here instead of in the `init {}` block because extensions that fail
|
||||
// to load just mysteriously disappears from the extension list, no errors no nothing.
|
||||
if (!isInterceptorAdded) {
|
||||
throw Exception("SpeedBinbInterceptor was not added to the client.")
|
||||
}
|
||||
|
||||
val readerUrl = document.location().toHttpUrl()
|
||||
val content = document.selectFirst("#content")!!
|
||||
|
||||
if (!content.hasAttr("data-ptbinb")) {
|
||||
return content.select("[data-ptimg]").mapIndexed { i, it ->
|
||||
Page(i, imageUrl = it.absUrl("data-ptimg"))
|
||||
}
|
||||
}
|
||||
|
||||
val cid = content.attr("data-ptbinb-cid")
|
||||
.ifEmpty { readerUrl.queryParameter("cid") }
|
||||
?: throw Exception("Could not find chapter ID")
|
||||
val sharedKey = generateSharedKey(cid)
|
||||
val contentInfoUrl = content.absUrl("data-ptbinb").toHttpUrl().newBuilder()
|
||||
.copyKeyParametersFrom(readerUrl)
|
||||
.setQueryParameter("cid", cid)
|
||||
.setQueryParameter("k", sharedKey)
|
||||
.setQueryParameter("dmytime", System.currentTimeMillis().toString())
|
||||
.build()
|
||||
val contentInfo = client.newCall(GET(contentInfoUrl, headers)).execute().parseAs<BibContentInfo>()
|
||||
|
||||
if (contentInfo.result != 1) {
|
||||
throw Exception("Failed to execute bibGetCntntInfo API.")
|
||||
}
|
||||
|
||||
if (contentInfo.items.isEmpty()) {
|
||||
throw Exception("There is no item.")
|
||||
}
|
||||
|
||||
val contentItem = contentInfo.items[0]
|
||||
val ctbl = json.decodeFromString<List<String>>(decodeScrambleTable(cid, sharedKey, contentItem.ctbl))
|
||||
val ptbl = json.decodeFromString<List<String>>(decodeScrambleTable(cid, sharedKey, contentItem.ptbl))
|
||||
val sbcUrl = contentItem.getSbcUrl(readerUrl, cid)
|
||||
val sbcData = client.newCall(GET(sbcUrl, headers)).execute().body.string().let {
|
||||
val raw = if (contentItem.serverType == ServerType.DIRECT) {
|
||||
it.substringAfter("DataGet_Content(").substringBeforeLast(")")
|
||||
} else {
|
||||
it
|
||||
}
|
||||
|
||||
json.decodeFromString<SBCContent>(raw)
|
||||
}
|
||||
|
||||
if (sbcData.result != 1) {
|
||||
throw Exception("Failed to fetch content")
|
||||
}
|
||||
|
||||
val isSingleQuality = sbcData.imageClass == "singlequality"
|
||||
val ttx = Jsoup.parseBodyFragment(sbcData.ttx, document.location())
|
||||
val pageBaseUrl = when (contentItem.serverType) {
|
||||
ServerType.DIRECT, ServerType.REST -> contentItem.contentServer
|
||||
ServerType.SBC -> sbcUrl.replaceFirst("/sbcGetCntnt.php", "/sbcGetImg.php")
|
||||
else -> throw UnsupportedOperationException("Unsupported ServerType value ${contentItem.serverType}")
|
||||
}.toHttpUrl()
|
||||
val pages = ttx.select("t-case:first-of-type t-img").mapIndexed { i, it ->
|
||||
val src = it.attr("src")
|
||||
val keyPair = determineKeyPair(src, ptbl, ctbl)
|
||||
val fragment = "ptbinb,${keyPair.first},${keyPair.second}"
|
||||
val imageUrl = pageBaseUrl.newBuilder()
|
||||
.buildImageUrl(
|
||||
readerUrl,
|
||||
src,
|
||||
contentItem,
|
||||
isSingleQuality,
|
||||
highQualityMode,
|
||||
)
|
||||
.fragment(fragment)
|
||||
.toString()
|
||||
|
||||
Page(i, imageUrl = imageUrl)
|
||||
}.toMutableList()
|
||||
|
||||
// This is probably the silliest use of TextInterceptor ever.
|
||||
//
|
||||
// If chapter purchases are enabled, and there's a link to purchase the current chapter,
|
||||
// we add in the purchase URL as the last page.
|
||||
val buyIconPosition = document.selectFirst("script:containsData(Config.LoginBuyIconPosition)")
|
||||
?.data()
|
||||
?.substringAfter("Config.LoginBuyIconPosition=")
|
||||
?.substringBefore(";")
|
||||
?.trim()
|
||||
?: "-1"
|
||||
val enableBuying = buyIconPosition != "-1"
|
||||
|
||||
if (enableBuying && contentItem.viewMode != ViewMode.COMMERCIAL && !contentItem.shopUrl.isNullOrEmpty()) {
|
||||
pages.add(
|
||||
Page(pages.size, imageUrl = TextInterceptorHelper.createUrl("", "購入: ${contentItem.shopUrl}")),
|
||||
)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
private fun HttpUrl.Builder.buildImageUrl(
|
||||
readerUrl: HttpUrl,
|
||||
src: String,
|
||||
contentItem: BibContentItem,
|
||||
isSingleQuality: Boolean,
|
||||
highQualityMode: Boolean,
|
||||
) = apply {
|
||||
when (contentItem.serverType) {
|
||||
ServerType.DIRECT -> {
|
||||
val filename = when {
|
||||
isSingleQuality -> "M.jpg"
|
||||
highQualityMode -> "M_H.jpg"
|
||||
else -> "M_L.jpg"
|
||||
}
|
||||
|
||||
addPathSegments(src)
|
||||
addPathSegment(filename)
|
||||
contentItem.contentDate?.let { setQueryParameter("dmytime", it) }
|
||||
}
|
||||
ServerType.REST -> {
|
||||
addPathSegment("img")
|
||||
addPathSegments(src)
|
||||
if (!isSingleQuality && !highQualityMode) {
|
||||
setQueryParameter("q", "1")
|
||||
}
|
||||
|
||||
contentItem.contentDate?.let { setQueryParameter("dmytime", it) }
|
||||
copyKeyParametersFrom(readerUrl)
|
||||
}
|
||||
ServerType.SBC -> {
|
||||
setQueryParameter("src", src)
|
||||
contentItem.requestToken?.let { setQueryParameter("p", it) }
|
||||
|
||||
if (!isSingleQuality) {
|
||||
setQueryParameter("q", if (highQualityMode) "0" else "1")
|
||||
}
|
||||
|
||||
setQueryParameter("vm", contentItem.viewMode.toString())
|
||||
contentItem.contentDate?.let { setQueryParameter("dmytime", it) }
|
||||
copyKeyParametersFrom(readerUrl)
|
||||
}
|
||||
else -> throw UnsupportedOperationException("Unsupported ServerType value ${contentItem.serverType}")
|
||||
}
|
||||
}
|
||||
|
||||
internal fun HttpUrl.Builder.copyKeyParametersFrom(url: HttpUrl): HttpUrl.Builder {
|
||||
for (i in 0..9) {
|
||||
url.queryParameter("u$i")?.let {
|
||||
setQueryParameter("u$i", it)
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
@ -1,301 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb.descrambler
|
||||
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.PtImgTranslation
|
||||
|
||||
private val PTBINBF_REGEX = Regex("""^=([0-9]+)-([0-9]+)([-+])([0-9]+)-([-_0-9A-Za-z]+)$""")
|
||||
private const val PTBINBF_CHAR_LOOKUP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
private const val PTBINBA_CHAR_LOOKUP = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
|
||||
|
||||
abstract class PtBinbDescrambler(
|
||||
val s: String,
|
||||
val u: String,
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
) : SpeedBinbDescrambler()
|
||||
|
||||
class PtBinbDescramblerF(s: String, u: String, width: Int, height: Int) : PtBinbDescrambler(s, u, width, height) {
|
||||
|
||||
private var widthPieces: Int = 0
|
||||
private var heightPieces: Int = 0
|
||||
private var piecePadding: Int = 0
|
||||
private lateinit var hDstPosLookup: List<Int>
|
||||
private lateinit var wDstPosLookup: List<Int>
|
||||
private lateinit var hPosLookup: List<Int>
|
||||
private lateinit var wPosLookup: List<Int>
|
||||
private var pieceDest: List<Int>? = null
|
||||
|
||||
init {
|
||||
// Kotlin init blocks don't allow early returns...
|
||||
init()
|
||||
}
|
||||
|
||||
private fun init() {
|
||||
val srcData = PTBINBF_REGEX.matchEntire(s)?.groupValues
|
||||
val dstData = PTBINBF_REGEX.matchEntire(u)?.groupValues
|
||||
|
||||
if (
|
||||
dstData == null ||
|
||||
srcData == null ||
|
||||
dstData[1] != srcData[1] ||
|
||||
dstData[2] != srcData[2] ||
|
||||
dstData[4] != srcData[4] ||
|
||||
dstData[3] != "+" ||
|
||||
srcData[3] != "-"
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
widthPieces = dstData[1].toInt()
|
||||
heightPieces = dstData[2].toInt()
|
||||
piecePadding = dstData[4].toInt()
|
||||
|
||||
if (widthPieces < 8 || heightPieces < 8 || widthPieces * heightPieces < 64) {
|
||||
return
|
||||
}
|
||||
|
||||
val e = widthPieces + heightPieces + widthPieces * heightPieces
|
||||
|
||||
if (dstData[5].length != e || srcData[5].length != e) {
|
||||
return
|
||||
}
|
||||
|
||||
val srcTnp = decodePieceData(srcData[5])
|
||||
val dstTnp = decodePieceData(dstData[5])
|
||||
|
||||
hDstPosLookup = dstTnp.hPos
|
||||
wDstPosLookup = dstTnp.wPos
|
||||
hPosLookup = srcTnp.hPos
|
||||
wPosLookup = srcTnp.wPos
|
||||
pieceDest = buildList(widthPieces * heightPieces) {
|
||||
for (i in 0 until widthPieces * heightPieces) {
|
||||
add(dstTnp.pieces[srcTnp.pieces[i]])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun isScrambled() =
|
||||
pieceDest != null
|
||||
|
||||
override fun canDescramble(): Boolean {
|
||||
val i = 2 * widthPieces * piecePadding
|
||||
val n = 2 * heightPieces * piecePadding
|
||||
|
||||
return width >= 64 + i && height >= 64 + n && width * height >= (320 + i) * (320 + n)
|
||||
}
|
||||
|
||||
override fun getCanvasDimensions(): Pair<Int, Int> {
|
||||
return if (canDescramble()) {
|
||||
Pair(
|
||||
width - 2 * widthPieces * piecePadding,
|
||||
height - 2 * heightPieces * piecePadding,
|
||||
)
|
||||
} else {
|
||||
Pair(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDescrambleCoords(): List<PtImgTranslation> {
|
||||
val pieceDest = this.pieceDest
|
||||
|
||||
if (!isScrambled() || pieceDest == null) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
if (!canDescramble()) {
|
||||
return listOf(
|
||||
PtImgTranslation(0, 0, width, height, 0, 0),
|
||||
)
|
||||
}
|
||||
|
||||
val canvasWidth = width - 2 * widthPieces * piecePadding
|
||||
val canvasHeight = height - 2 * heightPieces * piecePadding
|
||||
val pieceWidth = (canvasWidth + widthPieces - 1).div(widthPieces)
|
||||
val remainderWidth = canvasWidth - (widthPieces - 1) * pieceWidth
|
||||
val pieceHeight = (canvasHeight + heightPieces - 1).div(heightPieces)
|
||||
val remainderHeight = canvasHeight - (heightPieces - 1) * pieceHeight
|
||||
|
||||
return buildList(widthPieces * heightPieces) {
|
||||
for (o in 0 until widthPieces * heightPieces) {
|
||||
val hPos = o % widthPieces
|
||||
val wPos = o.div(widthPieces)
|
||||
val hDstPos = pieceDest[o] % widthPieces
|
||||
val wDstPos = pieceDest[o].div(widthPieces)
|
||||
|
||||
add(
|
||||
PtImgTranslation(
|
||||
xsrc = piecePadding + hPos * (pieceWidth + 2 * piecePadding) + if (hPosLookup[wPos] < hPos) remainderWidth - pieceWidth else 0,
|
||||
ysrc = piecePadding + wPos * (pieceHeight + 2 * piecePadding) + if (wPosLookup[hPos] < wPos) remainderHeight - pieceHeight else 0,
|
||||
width = if (hPosLookup[wPos] == hPos) remainderWidth else pieceWidth,
|
||||
height = if (wPosLookup[hPos] == wPos) remainderHeight else pieceHeight,
|
||||
xdest = hDstPos * pieceWidth + if (hDstPosLookup[wDstPos] < hDstPos) remainderWidth - pieceWidth else 0,
|
||||
ydest = wDstPos * pieceHeight + if (wDstPosLookup[hDstPos] < wDstPos) remainderHeight - pieceHeight else 0,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodePieceData(key: String): TNP {
|
||||
val wPos = buildList(widthPieces) {
|
||||
for (i in 0 until widthPieces) {
|
||||
add(PTBINBF_CHAR_LOOKUP.indexOf(key[i]))
|
||||
}
|
||||
}
|
||||
val hPos = buildList(heightPieces) {
|
||||
for (i in 0 until heightPieces) {
|
||||
add(PTBINBF_CHAR_LOOKUP.indexOf(key[widthPieces + i]))
|
||||
}
|
||||
}
|
||||
val pieces = buildList(widthPieces * heightPieces) {
|
||||
for (i in 0 until widthPieces * heightPieces) {
|
||||
add(PTBINBF_CHAR_LOOKUP.indexOf(key[widthPieces + heightPieces + i]))
|
||||
}
|
||||
}
|
||||
|
||||
return TNP(wPos, hPos, pieces)
|
||||
}
|
||||
|
||||
private class TNP(val wPos: List<Int>, val hPos: List<Int>, val pieces: List<Int>)
|
||||
}
|
||||
|
||||
class PtBinbDescramblerA(s: String, u: String, width: Int, height: Int) : PtBinbDescrambler(s, u, width, height) {
|
||||
|
||||
private var srcPieces: PieceCollection? = null
|
||||
|
||||
private var dstPieces: PieceCollection? = null
|
||||
|
||||
init {
|
||||
val srcPieces = calculatePieces(u)
|
||||
val dstPieces = calculatePieces(s)
|
||||
|
||||
if (
|
||||
srcPieces != null &&
|
||||
dstPieces != null &&
|
||||
srcPieces.ndx == dstPieces.ndx &&
|
||||
srcPieces.ndy == dstPieces.ndy
|
||||
) {
|
||||
this.srcPieces = srcPieces
|
||||
this.dstPieces = dstPieces
|
||||
}
|
||||
}
|
||||
|
||||
override fun isScrambled() =
|
||||
srcPieces != null && dstPieces != null
|
||||
|
||||
override fun canDescramble(): Boolean =
|
||||
width >= 64 && height >= 64 && width * height >= 102400
|
||||
|
||||
override fun getCanvasDimensions(): Pair<Int, Int> =
|
||||
Pair(width, height)
|
||||
|
||||
override fun getDescrambleCoords(): List<PtImgTranslation> {
|
||||
if (!isScrambled()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
if (!canDescramble()) {
|
||||
return listOf(
|
||||
PtImgTranslation(0, 0, width, height, 0, 0),
|
||||
)
|
||||
}
|
||||
|
||||
val srcPieces = this.srcPieces!!
|
||||
val dstPieces = this.dstPieces!!
|
||||
|
||||
return buildList(srcPieces.piece.size + 2) {
|
||||
val n = width - width % 8
|
||||
val pieceWidth = (n - 1).div(7) - (n - 1).div(7) % 8
|
||||
val e = n - 7 * pieceWidth
|
||||
val s = height - height % 8
|
||||
val pieceHeight = (s - 1).div(7) - (s - 1).div(7) % 8
|
||||
val u = s - 7 * pieceHeight
|
||||
|
||||
for (i in srcPieces.piece.indices) {
|
||||
val src = srcPieces.piece[i]
|
||||
val dst = dstPieces.piece[i]
|
||||
|
||||
add(
|
||||
PtImgTranslation(
|
||||
xsrc = src.x.div(2) * pieceWidth + src.x % 2 * e,
|
||||
ysrc = src.y.div(2) * pieceHeight + src.y % 2 * u,
|
||||
width = src.w.div(2) * pieceWidth + src.w % 2 * e,
|
||||
height = src.h.div(2) * pieceHeight + src.h % 2 * u,
|
||||
xdest = dst.x.div(2) * pieceWidth + dst.x % 2 * e,
|
||||
ydest = dst.y.div(2) * pieceHeight + dst.y % 2 * u,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val l = pieceWidth * (srcPieces.ndx - 1) + e
|
||||
val v = pieceHeight * (srcPieces.ndy - 1) + u
|
||||
|
||||
if (l < width) {
|
||||
add(
|
||||
PtImgTranslation(l, 0, width - l, v, l, 0),
|
||||
)
|
||||
}
|
||||
|
||||
if (v < height) {
|
||||
add(
|
||||
PtImgTranslation(0, v, width, height - v, 0, v),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePieces(key: String): PieceCollection? {
|
||||
if (key.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val parts = key.split("-")
|
||||
|
||||
if (parts.size != 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
val ndx = parts[0].toInt()
|
||||
val ndy = parts[1].toInt()
|
||||
val e = parts[2]
|
||||
|
||||
if (ndx * ndy * 2 != e.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
val pieces = buildList(ndx * ndy) {
|
||||
val a = (ndx - 1) * (ndy - 1) - 1
|
||||
val f = ndx - 1 + a
|
||||
val c = ndy - 1 + f
|
||||
val l = 1 + c
|
||||
var w = 0
|
||||
var h = 0
|
||||
|
||||
for (d in 0 until ndx * ndy) {
|
||||
val x = PTBINBA_CHAR_LOOKUP.indexOf(e[2 * d])
|
||||
val y = PTBINBA_CHAR_LOOKUP.indexOf(e[2 * d + 1])
|
||||
|
||||
if (d <= a) {
|
||||
h = 2
|
||||
w = 2
|
||||
} else if (d <= f) {
|
||||
h = 1
|
||||
w = 2
|
||||
} else if (d <= c) {
|
||||
h = 2
|
||||
w = 1
|
||||
} else if (d <= l) {
|
||||
h = 1
|
||||
w = 1
|
||||
}
|
||||
|
||||
add(Piece(x, y, w, h))
|
||||
}
|
||||
}
|
||||
|
||||
return PieceCollection(ndx, ndy, pieces)
|
||||
}
|
||||
|
||||
private class Piece(val x: Int, val y: Int, val w: Int, val h: Int)
|
||||
|
||||
private class PieceCollection(val ndx: Int, val ndy: Int, val piece: List<Piece>)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb.descrambler
|
||||
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.PtImg
|
||||
|
||||
class PtImgDescrambler(private val metadata: PtImg) : SpeedBinbDescrambler() {
|
||||
override fun isScrambled() = metadata.translations.isNotEmpty()
|
||||
|
||||
override fun canDescramble() = metadata.translations.isNotEmpty()
|
||||
|
||||
override fun getCanvasDimensions() = Pair(metadata.views[0].width, metadata.views[0].height)
|
||||
|
||||
override fun getDescrambleCoords() = metadata.translations
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
package eu.kanade.tachiyomi.lib.speedbinb.descrambler
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import eu.kanade.tachiyomi.lib.speedbinb.PtImgTranslation
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
abstract class SpeedBinbDescrambler {
|
||||
abstract fun isScrambled(): Boolean
|
||||
abstract fun canDescramble(): Boolean
|
||||
abstract fun getCanvasDimensions(): Pair<Int, Int>
|
||||
abstract fun getDescrambleCoords(): List<PtImgTranslation>
|
||||
|
||||
open fun descrambleImage(image: Bitmap): ByteArray? {
|
||||
if (!isScrambled()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val (width, height) = getCanvasDimensions()
|
||||
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(result)
|
||||
|
||||
getDescrambleCoords().forEach {
|
||||
val src = Rect(it.xsrc, it.ysrc, it.xsrc + it.width, it.ysrc + it.height)
|
||||
val dst = Rect(it.xdest, it.ydest, it.xdest + it.width, it.ydest + it.height)
|
||||
|
||||
canvas.drawBitmap(image, src, dst, null)
|
||||
}
|
||||
|
||||
return ByteArrayOutputStream()
|
||||
.also {
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 90, it)
|
||||
}
|
||||
.toByteArray()
|
||||
}
|
||||
}
|
@ -18,73 +18,69 @@ import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
// Designer values:
|
||||
private const val WIDTH: Int = 1000
|
||||
private const val X_PADDING: Float = 50f
|
||||
private const val Y_PADDING: Float = 25f
|
||||
private const val HEADING_FONT_SIZE: Float = 36f
|
||||
private const val BODY_FONT_SIZE: Float = 30f
|
||||
private const val SPACING_MULT: Float = 1.1f
|
||||
private const val SPACING_ADD: Float = 2f
|
||||
|
||||
// No need to touch this one:
|
||||
private const val HOST = TextInterceptorHelper.HOST
|
||||
|
||||
class TextInterceptor : Interceptor {
|
||||
// With help from:
|
||||
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
||||
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
||||
|
||||
companion object {
|
||||
// Designer values:
|
||||
private const val WIDTH: Int = 1000
|
||||
private const val X_PADDING: Float = 50f
|
||||
private const val Y_PADDING: Float = 25f
|
||||
private const val HEADING_FONT_SIZE: Float = 36f
|
||||
private const val BODY_FONT_SIZE: Float = 30f
|
||||
private const val SPACING_MULT: Float = 1.1f
|
||||
private const val SPACING_ADD: Float = 2f
|
||||
|
||||
// No need to touch this one:
|
||||
private const val HOST = TextInterceptorHelper.HOST
|
||||
}
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val url = request.url
|
||||
if (url.host != HOST) return chain.proceed(request)
|
||||
|
||||
val heading = url.pathSegments[0].takeIf { it.isNotEmpty() }?.let {
|
||||
val title = textFixer(url.pathSegments[0])
|
||||
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
||||
val story = textFixer(url.pathSegments[1])
|
||||
|
||||
// Heading
|
||||
val paintHeading = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = HEADING_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
StaticLayout(
|
||||
title, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
// Heading
|
||||
val paintHeading = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = HEADING_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
val body = url.pathSegments[1].takeIf { it.isNotEmpty() }?.let {
|
||||
val story = textFixer(it)
|
||||
@Suppress("DEPRECATION")
|
||||
val heading = StaticLayout(
|
||||
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
|
||||
// Body
|
||||
val paintBody = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = BODY_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
StaticLayout(
|
||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
// Body
|
||||
val paintBody = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = BODY_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val body = StaticLayout(
|
||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
|
||||
// Image building
|
||||
val headingHeight = heading?.height ?: 0
|
||||
val bodyHeight = body?.height ?: 0
|
||||
val imgHeight: Int = (headingHeight + bodyHeight + 2 * Y_PADDING).toInt()
|
||||
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
||||
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
||||
|
||||
Canvas(bitmap).apply {
|
||||
drawColor(Color.WHITE)
|
||||
heading?.draw(this, X_PADDING, Y_PADDING)
|
||||
body?.draw(this, X_PADDING, Y_PADDING + headingHeight.toFloat())
|
||||
heading.draw(this, X_PADDING, Y_PADDING)
|
||||
body.draw(this, X_PADDING, Y_PADDING + heading.height.toFloat())
|
||||
}
|
||||
|
||||
// Image converting & returning
|
||||
@ -123,7 +119,7 @@ object TextInterceptorHelper {
|
||||
|
||||
const val HOST = "tachiyomi-lib-textinterceptor"
|
||||
|
||||
fun createUrl(title: String, text: String): String {
|
||||
return "http://$HOST/" + Uri.encode(title) + "/" + Uri.encode(text)
|
||||
fun createUrl(creator: String, text: String): String {
|
||||
return "http://$HOST/" + Uri.encode(creator) + "/" + Uri.encode(text)
|
||||
}
|
||||
}
|
||||
|
82
multisrc/build.gradle.kts
Normal file
@ -0,0 +1,82 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
kotlin("android")
|
||||
id("kotlinx-serialization")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = AndroidConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 29
|
||||
}
|
||||
|
||||
namespace = "eu.kanade.tachiyomi.lib.themesources"
|
||||
|
||||
kotlinOptions {
|
||||
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
isCanBeResolved = true
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.bundles.common)
|
||||
|
||||
// Only PeachScan sources uses the image-decoder dependency.
|
||||
//noinspection UseTomlInstead
|
||||
compileOnly("com.github.tachiyomiorg:image-decoder:fbd6601290")
|
||||
|
||||
// Implements all :lib libraries on the multisrc 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 {
|
||||
register<JavaExec>("generateExtensions") {
|
||||
val buildDir = layout.buildDirectory.asFile.get()
|
||||
classpath = configurations.compileOnly.get() +
|
||||
configurations.androidApis.get() + // android.jar path
|
||||
files("$buildDir/intermediates/aar_main_jar/debug/classes.jar") // jar made from this module
|
||||
mainClass.set("generator.GeneratorMainKt")
|
||||
|
||||
workingDir = workingDir.parentFile // project root
|
||||
|
||||
errorOutput = System.out // for GitHub workflow commands
|
||||
|
||||
if (!logger.isInfoEnabled) {
|
||||
standardOutput = org.gradle.internal.io.NullOutputStream.INSTANCE
|
||||
}
|
||||
|
||||
dependsOn("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"))
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |