Compare commits

..

No commits in common. "9602aa5dd546c1726adc22ffdad67b87a6ff952c" and "c5c6d77479d473b7acd5251d603d224de1d31132" have entirely different histories.

6237 changed files with 4668 additions and 15267 deletions

View File

@ -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:

View 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>

View 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>

View File

@ -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
View File

@ -0,0 +1,2 @@
.gradle/
build/

View File

@ -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)
}

View File

@ -1,7 +0,0 @@
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@ -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)

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 3

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 6

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 9

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 3
dependencies {
api(project(":lib:synchrony"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 7
dependencies {
api(project(":lib:speedbinb"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 5

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 9

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 3

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 5

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 5

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 22

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 5

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 20

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 10

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View File

@ -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)
}
}
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 25

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,10 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 33
dependencies {
api(project(":lib:cryptoaes"))
api(project(":lib:randomua"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 13

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 5

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 13

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 28
dependencies {
api(project(":lib:randomua"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 5

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 28
dependencies {
api(project(":lib:randomua"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 6

View File

@ -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

View File

@ -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

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 11
dependencies {
api(project(":lib:i18n"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 3

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 12

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View File

@ -1,9 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1
dependencies {
api(project(":lib:dataimage"))
}

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 8

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 11

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 2

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 4

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 6

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 8

View File

@ -1,5 +0,0 @@
plugins {
id("lib-multisrc")
}
baseVersionCode = 1

View File

@ -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)
}

View File

@ -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) { }
}
}

View File

@ -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"))
}

View File

@ -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())
}
}

View File

@ -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,
)

View File

@ -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()

View File

@ -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
}

View File

@ -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>)
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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
View 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"))
}
}

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show More