Compare commits

..

No commits in common. "fc828972d840c285e0796ef425b64733af51e111" and "c66abf25b95e984663af0fcc0ed5cf6586fed3d4" have entirely different histories.

852 changed files with 6310 additions and 13491 deletions

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
/src/zh/ @stevenyomi

View File

@ -1,5 +1,5 @@
name: 🐞 Issue report
description: Report a source issue in Keiyoushi
description: Report a source issue in Tachiyomi
labels: [Bug]
body:
@ -63,7 +63,7 @@ body:
description: |
You can find your Mihon/Tachiyomi version in **More → About**.
placeholder: |
Example: "0.18.0"
Example: "0.16.3"
validations:
required: true
@ -101,7 +101,7 @@ body:
required: true
- label: I have tried the [troubleshooting guide](https://mihon.app/docs/guides/troubleshooting/).
required: true
- label: If this is an issue with the app itself, I should be opening an issue in the app repository.
- label: If this is an issue with the app itself, I should be opening an issue in the [app repository](https://github.com/tachiyomiorg/tachiyomi/issues/new/choose).
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -1,5 +1,5 @@
name: 🌐 Source request
description: Suggest a new source for Keiyoushi
description: Suggest a new source for Tachiyomi
labels: [Source request]
body:

1
.gitignore vendored
View File

@ -11,4 +11,3 @@ apk/
gen
generated-src/
.kotlin
*.jks

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -38,7 +38,7 @@ kotlinter {
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
implementation(project(":core"))
implementation(project(":utils"))
}
tasks {

View File

@ -103,6 +103,7 @@ dependencies {
if (theme != null) implementation(theme) // Overrides core launcher icons
implementation(project(":core"))
compileOnly(libs.bundles.common)
implementation(project(":utils"))
}
tasks.register("writeManifestFile") {

View File

@ -1,6 +1,5 @@
plugins {
id("com.android.library")
kotlin("android")
}
android {
@ -10,18 +9,17 @@ android {
minSdk = AndroidConfig.minSdk
}
namespace = "keiyoushi.core"
namespace = "eu.kanade.tachiyomi.extension.core"
sourceSets {
named("main") {
manifest.srcFile("AndroidManifest.xml")
res.setSrcDirs(listOf("res"))
}
}
buildFeatures {
resValues = false
shaders = false
}
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.serialization.ExperimentalSerializationApi"
}
}
dependencies {
compileOnly(versionCatalogs.named("libs").findBundle("common").get())
}

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

4
gradlew generated vendored
View File

@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.

188
gradlew.bat generated vendored
View File

@ -1,94 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -2,7 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.extension.zh.onemanhua.ColaMangaUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.colamanga.ColaMangaUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -12,8 +12,8 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="www.colamanga.com"
android:scheme="https"
android:host="${SOURCEHOST}"
android:scheme="${SOURCESCHEME}"
android:pathPattern="/manga-..*/" />
</intent-filter>
</activity>

View File

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

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.zh.onemanhua
package eu.kanade.tachiyomi.multisrc.colamanga
import android.annotation.SuppressLint
import android.app.Application

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.zh.onemanhua
package eu.kanade.tachiyomi.multisrc.colamanga
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.zh.onemanhua
package eu.kanade.tachiyomi.multisrc.colamanga
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.zh.onemanhua
package eu.kanade.tachiyomi.multisrc.colamanga
class ColaMangaIntl(private val lang: String) {

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.zh.onemanhua
package eu.kanade.tachiyomi.multisrc.colamanga
import android.app.Activity
import android.content.ActivityNotFoundException
@ -13,7 +13,7 @@ class ColaMangaUrlActivity : Activity() {
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.isNotEmpty()) {
if (pathSegments != null && pathSegments.size > 0) {
val intent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${ColaManga.PREFIX_SLUG_SEARCH}${pathSegments[0]}")

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="eu.kanade.tachiyomi.multisrc.etoshore.EtoshoreUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/..*/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,247 +0,0 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import eu.kanade.tachiyomi.network.GET
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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
abstract class Etoshore(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
// ============================== Popular ==============================
open val popularFilter = FilterList(
SelectionList("", listOf(Tag(value = "views", query = "sort"))),
)
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", popularFilter)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun popularMangaSelector() = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Latest ===============================
open val latestFilter = FilterList(
SelectionList("", listOf(Tag(value = "date", query = "sort"))),
)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", latestFilter)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesSelector() = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException()
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/page/$page".toHttpUrl().newBuilder()
.addQueryParameter("s", query)
filters.forEach { filter ->
when (filter) {
is SelectionList -> {
val selected = filter.selected().takeIf { it.value.isNotBlank() }
?: return@forEach
url.addQueryParameter(selected.query, selected.value)
}
else -> {}
}
}
return GET(url.build(), headers)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (query.startsWith(PREFIX_SEARCH)) {
val slug = query.substringAfter(PREFIX_SEARCH)
return fetchMangaDetails(SManga.create().apply { url = "/manga/$slug/" })
.map { manga -> MangasPage(listOf(manga), false) }
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-posts .chapter-box .poster a"
override fun searchMangaNextPageSelector() = ".navigation .naviright:has(a)"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.attr("title")
thumbnail_url = element.selectFirst("img")?.let(::imageFromElement)
setUrlWithoutDomain(element.absUrl("href"))
}
override fun searchMangaParse(response: Response): MangasPage {
if (filterList.isEmpty()) {
filterParse(response)
}
return super.searchMangaParse(response)
}
// ============================== Details ===============================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1")!!.text()
description = document.selectFirst(".excerpt p")?.text()
document.selectFirst(".details-right-con img")?.let { thumbnail_url = imageFromElement(it) }
genre = document.select("div.meta-item span.meta-title:contains(Genres) + span a")
.joinToString { it.text() }
author = document.selectFirst("div.meta-item span.meta-title:contains(Author) + span a")
?.text()
with(document) {
status = when {
containsClass(".finished") -> SManga.COMPLETED
containsClass(".publishing") -> SManga.ONGOING
containsClass(".on-hiatus") -> SManga.ON_HIATUS
containsClass(".discontinued") -> SManga.CANCELLED
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location())
}
private fun Element.containsClass(cssSelector: String) = select(cssSelector).isNotEmpty()
protected open fun imageFromElement(element: Element): String? {
val attributes = listOf(
"data-src",
"data-lazy-src",
"data-cfsrc",
"src",
)
return attributes
.mapNotNull { attr -> element.takeIf { it.hasAttr(attr) }?.attr("abs:$attr") }
.maxOrNull()
?: element.takeIf { it.hasAttr("srcset") }?.attr("abs:srcset")?.getSrcSetImage()
}
protected open fun String.getSrcSetImage(): String? {
return this.split(" ")
.filter(URL_REGEX::matches)
.maxOfOrNull(String::toString)
}
// ============================== Chapters ============================
override fun chapterListSelector() = ".chapter-list li a"
override fun chapterListParse(response: Response): List<SChapter> {
return super.chapterListParse(response)
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".title")!!.text()
setUrlWithoutDomain(element.absUrl("href"))
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select(".chapter-images .chapter-item > img").mapIndexed { index, element ->
Page(index, document.location(), imageFromElement(element))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================= Filters ==============================
private var filterList = emptyList<Pair<String, List<Tag>>>()
override fun getFilterList(): FilterList {
val filters = mutableListOf<Filter<*>>()
filters += if (filterList.isNotEmpty()) {
filterList.map { SelectionList(it.first, it.second) }
} else {
listOf(Filter.Header("Aperte 'Redefinir' para tentar mostrar os filtros"))
}
return FilterList(filters)
}
protected open fun parseSelection(document: Document, selector: String): Pair<String, List<Tag>>? {
val selectorFilter = "#filter-form $selector .select-item-head .text"
return document.selectFirst(selectorFilter)?.text()?.let { displayName ->
val tags = document.select("#filter-form $selector li").map { element ->
element.selectFirst("input")!!.let { input ->
Tag(
name = element.selectFirst(".text")!!.text(),
value = input.attr("value"),
query = input.attr("name"),
)
}
}
displayName to mutableListOf<Tag>().apply {
this += Tag("Default")
this += tags
}
}
}
open val filterListSelector: List<String> = listOf(
".filter-genre",
".filter-status",
".filter-type",
".filter-year",
".filter-sort",
)
open fun filterParse(response: Response) {
val document = Jsoup.parseBodyFragment(response.peekBody(Long.MAX_VALUE).string())
filterList = filterListSelector.mapNotNull { selector -> parseSelection(document, selector) }
}
protected data class Tag(val name: String = "", val value: String = "", val query: String = "")
private open class SelectionList(displayName: String, private val vals: List<Tag>, state: Int = 0) :
Filter.Select<String>(displayName, vals.map { it.name }.toTypedArray(), state) {
fun selected() = vals[state]
}
// ============================= Utils ==============================
private fun String.containsIn(array: Array<String>): Boolean {
return this.lowercase() in array.map { it.lowercase() }
}
companion object {
const val PREFIX_SEARCH = "id:"
val URL_REGEX = """^(https?://[^\s/$.?#].[^\s]*)$""".toRegex()
}
}

View File

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.multisrc.etoshore
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class EtoshoreUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size >= 2) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${getSLUG(pathSegments)}")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("EtoshoreUrl", e.toString())
}
} else {
Log.e("EtoshoreUrl", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
private fun getSLUG(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 2) {
val slug = pathSegments[1]
"${Etoshore.PREFIX_SEARCH}$slug"
} else {
null
}
}
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 3
baseVersionCode = 2

View File

@ -84,14 +84,12 @@ open class GoDa(
val document = response.asJsoup().selectFirst("main")!!
val titleElement = document.selectFirst("h1")!!
val elements = titleElement.parent()!!.parent()!!.children()
check(elements[4].tagName() == "p")
check(elements.size == 6)
title = titleElement.ownText()
status = when (titleElement.child(0).text()) {
"連載中", "Ongoing" -> SManga.ONGOING
"完結" -> SManga.COMPLETED
"停止更新" -> SManga.CANCELLED
"休刊" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
author = Entities.unescape(elements[1].children().drop(1).joinToString { it.text().removeSuffix(" ,") })

View File

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

View File

@ -1,13 +1,12 @@
package eu.kanade.tachiyomi.multisrc.greenshit
import android.content.SharedPreferences
import android.util.Base64
import android.widget.Toast
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -16,68 +15,83 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.getPreferences
import keiyoushi.utils.parseAs
import keiyoushi.utils.toJsonString
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.io.IOException
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
abstract class GreenShit(
override val name: String,
override val baseUrl: String,
val url: String,
override val lang: String,
val scanId: Long = 1,
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
private val preferences: SharedPreferences by getPreferencesLazy()
private val isCi = System.getenv("CI") == "true"
protected open val apiUrl = "https://api.sussytoons.wtf"
private val preferences: SharedPreferences = getPreferences()
protected var apiUrl: String
get() = preferences.getString(API_BASE_URL_PREF, defaultApiUrl)!!
private set(value) = preferences.edit().putString(API_BASE_URL_PREF, value).apply()
private var restoreDefaultEnable: Boolean
get() = preferences.getBoolean(DEFAULT_PREF, false)
set(value) = preferences.edit().putBoolean(DEFAULT_PREF, value).apply()
override val baseUrl: String get() = when {
isCi -> defaultBaseUrl
else -> preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
}
private val defaultBaseUrl: String = url
private val defaultApiUrl: String = "https://api.sussytoons.wtf"
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::imageLocation)
.build()
open val targetAudience: TargetAudience = TargetAudience.All
init {
if (restoreDefaultEnable) {
restoreDefaultEnable = false
preferences.edit().putString(DEFAULT_BASE_URL_PREF, null).apply()
preferences.edit().putString(API_DEFAULT_BASE_URL_PREF, null).apply()
}
open val contentOrigin: ContentOrigin = ContentOrigin.Web
preferences.getString(DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultBaseUrl) {
preferences.edit()
.putString(BASE_URL_PREF, defaultBaseUrl)
.putString(DEFAULT_BASE_URL_PREF, defaultBaseUrl)
.apply()
}
}
preferences.getString(API_DEFAULT_BASE_URL_PREF, null).let { domain ->
if (domain != defaultApiUrl) {
preferences.edit()
.putString(API_BASE_URL_PREF, defaultApiUrl)
.putString(API_DEFAULT_BASE_URL_PREF, defaultApiUrl)
.apply()
}
}
}
override fun headersBuilder() = super.headersBuilder()
.set("scan-id", scanId.toString())
// ============================= Popular ==================================
override fun popularMangaRequest(page: Int) =
when (contentOrigin) {
ContentOrigin.Mobile -> GET("$apiUrl/obras/top5", headers)
else -> GET(baseUrl, headers)
}
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaParse(response: Response): MangasPage =
when (contentOrigin) {
ContentOrigin.Mobile -> popularMangaParseMobile(response)
else -> popularMangaParseWeb(response)
}
private fun popularMangaParseMobile(response: Response): MangasPage {
val mangas = response.parseAs<ResultDto<List<MangaDto>>>().toSMangaList()
return MangasPage(mangas, hasNextPage = false)
}
private fun popularMangaParseWeb(response: Response): MangasPage {
override fun popularMangaParse(response: Response): MangasPage {
val json = response.parseScriptToJson().let(POPULAR_JSON_REGEX::find)
?.groups?.get(1)?.value
?: return MangasPage(emptyList(), false)
@ -91,16 +105,11 @@ abstract class GreenShit(
val url = "$apiUrl/obras/novos-capitulos".toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.addQueryParameter("limite", "24")
.addQueryParameterIf(targetAudience != TargetAudience.All, "gen_id", targetAudience.toString())
.addQueryParameter("gen_id", "4")
.build()
return GET(url, headers)
}
private fun HttpUrl.Builder.addQueryParameterIf(predicate: Boolean, name: String, value: String): HttpUrl.Builder {
if (predicate) addQueryParameter(name, value)
return this
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<ResultDto<List<MangaDto>>>()
val mangas = dto.toSMangaList()
@ -125,60 +134,19 @@ abstract class GreenShit(
}
// ============================= Details ==================================
override fun getMangaUrl(manga: SManga) = when (contentOrigin) {
ContentOrigin.Mobile -> "$baseUrl${manga.url}"
else -> super.getMangaUrl(manga)
}
override fun mangaDetailsRequest(manga: SManga): Request =
when (contentOrigin) {
ContentOrigin.Mobile -> mangaDetailsRequestMobile(manga)
else -> super.mangaDetailsRequest(manga)
}
private fun mangaDetailsRequestMobile(manga: SManga): Request {
val pathSegment = manga.url.substringBeforeLast("/").replace("obra", "obras")
return GET("$apiUrl$pathSegment", headers)
}
override fun mangaDetailsParse(response: Response) =
when (contentOrigin) {
ContentOrigin.Mobile -> response.parseAs<ResultDto<MangaDto>>().results.toSManga()
else -> mangaDetailsParseWeb(response)
}
private fun mangaDetailsParseWeb(response: Response): SManga {
override fun mangaDetailsParse(response: Response): SManga {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(1)?.value
?.groups?.get(0)?.value
?: throw IOException("Details do mangá não foi encontrado")
return json.parseAs<ResultDto<MangaDto>>().results.toSManga()
}
// ============================= Chapters =================================
override fun getChapterUrl(chapter: SChapter) = when (contentOrigin) {
ContentOrigin.Mobile -> "$baseUrl${chapter.url}"
else -> super.getChapterUrl(chapter)
}
override fun chapterListRequest(manga: SManga) =
when (contentOrigin) {
ContentOrigin.Mobile -> mangaDetailsRequest(manga)
else -> super.chapterListRequest(manga)
}
override fun chapterListParse(response: Response): List<SChapter> =
when (contentOrigin) {
ContentOrigin.Mobile -> chapterListParseMobile(response)
else -> chapterListParseWeb(response)
}.distinctBy(SChapter::url)
private fun chapterListParseMobile(response: Response): List<SChapter> =
response.parseAs<ResultDto<WrapperChapterDto>>().toSChapterList()
private fun chapterListParseWeb(response: Response): List<SChapter> {
override fun chapterListParse(response: Response): List<SChapter> {
val json = response.parseScriptToJson().let(DETAILS_CHAPTER_REGEX::find)
?.groups?.get(1)?.value
?.groups?.get(0)?.value
?: return emptyList()
return json.parseAs<ResultDto<WrapperChapterDto>>().toSChapterList()
}
@ -187,50 +155,7 @@ abstract class GreenShit(
private val pageUrlSelector = "img.chakra-image"
override fun pageListRequest(chapter: SChapter): Request =
when (contentOrigin) {
ContentOrigin.Mobile -> pageListRequestMobile(chapter)
else -> super.pageListRequest(chapter)
}
private fun pageListRequestMobile(chapter: SChapter): Request {
val pathSegment = chapter.url.replace("capitulo", "capitulo-app-token")
val newHeaders = headers.newBuilder()
.set("x-client-hash", generateToken(scanId, SECRET_KEY))
.set("authorization", "Bearer $token")
.build()
return GET("$apiUrl$pathSegment", newHeaders)
}
private fun generateToken(scanId: Long, secretKey: String): String {
val timestamp = System.currentTimeMillis() / 1000
val expiration = timestamp + 3600
val payload = buildJsonObject {
put("scan_id", scanId)
put("timestamp", timestamp)
put("exp", expiration)
}.toJsonString()
val hmac = Mac.getInstance("HmacSHA256")
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256")
hmac.init(secretKeySpec)
val signatureBytes = hmac.doFinal(payload.toByteArray())
val signature = signatureBytes.joinToString("") { "%02x".format(it) }
return Base64.encodeToString("$payload.$signature".toByteArray(), Base64.NO_WRAP)
}
override fun pageListParse(response: Response): List<Page> =
when (contentOrigin) {
ContentOrigin.Mobile -> pageListParseMobile(response)
else -> pageListParseWeb(response)
}
private fun pageListParseMobile(response: Response): List<Page> =
response.parseAs<ResultDto<ChapterPageDto>>().toPageList()
private fun pageListParseWeb(response: Response): List<Page> {
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
pageListParse(document).takeIf(List<Page>::isNotEmpty)?.let { return it }
@ -275,57 +200,6 @@ abstract class GreenShit(
return GET(page.url, imageHeaders)
}
// ============================= Login ========================================
private val credential: Credential by lazy {
Credential(
email = preferences.getString(USERNAME_PREF, "") as String,
password = preferences.getString(PASSWORD_PREF, "") as String,
)
}
private fun Token.save(): Token {
return this.also {
preferences.edit()
.putString(TOKEN_PREF, it.toJsonString())
.apply()
}
}
private var _cache: Token? = null
private val token: Token
get() {
if (_cache != null && _cache!!.isValid()) {
return _cache!!
}
val tokenValue = preferences.getString(TOKEN_PREF, Token().toJsonString())?.parseAs<Token>()
if (tokenValue != null && tokenValue.isValid()) {
return tokenValue.also { _cache = it }
}
return credential.takeIf(Credential::isNotEmpty)?.let(::doLogin)?.let { response ->
if (response.isSuccessful.not()) {
Token.empty().save()
throw IOException("Falha ao realizar o login")
}
val tokenDto = response.parseAs<ResultDto<TokenDto>>().results
Token(tokenDto.value).also {
_cache = it.save()
}
} ?: throw IOException("Adicione suas credenciais em Extensões > $name > Configurações")
}
val loginClient = network.cloudflareClient
fun doLogin(credential: Credential): Response {
val payload = buildJsonObject {
put("usr_email", credential.email)
put("usr_senha", credential.password)
}.toJsonString().toRequestBody("application/json".toMediaType())
return loginClient.newCall(POST("$apiUrl/me/login", headers, payload)).execute()
}
// ============================= Interceptors =================================
private fun imageLocation(chain: Interceptor.Chain): Response {
@ -350,45 +224,52 @@ abstract class GreenShit(
// ============================= Settings ====================================
override fun setupPreferenceScreen(screen: PreferenceScreen) {
if (contentOrigin != ContentOrigin.Mobile) {
return
}
val fields = listOf(
EditTextPreference(screen.context).apply {
key = BASE_URL_PREF
title = BASE_URL_PREF_TITLE
summary = URL_PREF_SUMMARY
val warning = "⚠️ Os dados inseridos nessa seção serão usados somente para realizar o login na fonte"
val message = "Insira %s para prosseguir com o acesso aos recursos disponíveis na fonte"
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL padrão:\n$defaultBaseUrl"
EditTextPreference(screen.context).apply {
key = USERNAME_PREF
title = "📧 Email"
summary = "Email de acesso"
dialogMessage = buildString {
appendLine(message.format("seu email"))
append("\n$warning")
}
setDefaultValue(defaultBaseUrl)
},
EditTextPreference(screen.context).apply {
key = API_BASE_URL_PREF
title = API_BASE_URL_PREF_TITLE
summary = buildString {
append("Se não souber como verificar a URL da API, ")
append("busque suporte no Discord do repositório de extensões.")
appendLine(URL_PREF_SUMMARY)
append("\n⚠ A fonte não oferece suporte para essa extensão.")
}
setDefaultValue("")
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "URL da API padrão:\n$defaultApiUrl"
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
setDefaultValue(defaultApiUrl)
},
EditTextPreference(screen.context).apply {
key = PASSWORD_PREF
title = "🔑 Senha"
summary = "Senha de acesso"
dialogMessage = buildString {
appendLine(message.format("sua senha"))
append("\n$warning")
}
setDefaultValue("")
SwitchPreferenceCompat(screen.context).apply {
key = DEFAULT_PREF
title = "Redefinir configurações"
summary = buildString {
append("Habilite para redefinir as configurações padrões no próximo reinicialização da aplicação.")
appendLine("Você pode limpar os dados da extensão em Configurações > Avançado:")
appendLine("\t - Limpar os cookies")
appendLine("\t - Limpar os dados da WebView")
appendLine("\t - Limpar o banco de dados (Procure a '$name' e remova os dados)")
}
setDefaultValue(false)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
},
)
setOnPreferenceChangeListener { _, _ ->
Toast.makeText(screen.context, RESTART_APP_MESSAGE, Toast.LENGTH_LONG).show()
true
}
}.also(screen::addPreference)
fields.forEach(screen::addPreference)
}
// ============================= Utilities ====================================
@ -418,33 +299,24 @@ abstract class GreenShit(
return this
}
enum class TargetAudience(val value: Int) {
All(1),
Shoujo(4),
Yaoi(7),
;
override fun toString() = value.toString()
}
enum class ContentOrigin {
Mobile,
Web,
}
companion object {
const val CDN_URL = "https://cdn.sussytoons.site"
val pageRegex = """capituloInicial.{3}(.*?)(\}\]\})""".toRegex()
val POPULAR_JSON_REGEX = """(?:"dataTop":)(\{.+totalPaginas":\d+\})(?:.+"dataF)""".toRegex()
val DETAILS_CHAPTER_REGEX = """\{"obra":(\{.+"\}{3})""".toRegex()
val DETAILS_CHAPTER_REGEX = """(\{\"resultado.+"\}{3})""".toRegex()
private const val URL_PREF_SUMMARY = "Para uso temporário, se a extensão for atualizada, a alteração será perdida."
private const val BASE_URL_PREF = "overrideBaseUrl"
private const val BASE_URL_PREF_TITLE = "Editar URL da fonte"
private const val DEFAULT_BASE_URL_PREF = "defaultBaseUrl"
private const val RESTART_APP_MESSAGE = "Reinicie o aplicativo para aplicar as alterações"
private const val TOKEN_PREF = "greenShitToken"
private const val USERNAME_PREF = "usernamePref"
private const val PASSWORD_PREF = "passwordPref"
private const val API_BASE_URL_PREF = "overrideApiUrl"
private const val API_BASE_URL_PREF_TITLE = "Editar URL da API da fonte"
private const val API_DEFAULT_BASE_URL_PREF = "defaultApiUrl"
private const val SECRET_KEY = "sua_chave_secreta_aqui_32_caracteres"
private const val DEFAULT_PREF = "defaultPref"
}
}

View File

@ -13,41 +13,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import org.jsoup.Jsoup
import java.text.Normalizer
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
@Serializable
class Token(
val value: String = "",
val updateAt: Long = Date().time,
) {
fun isValid() = value.isNotEmpty() && isExpired().not()
fun isExpired(): Boolean {
val updateAtDate = Date(updateAt)
val expiration = Calendar.getInstance().apply {
time = updateAtDate
add(Calendar.HOUR, 1)
}
return Date().after(expiration.time)
}
override fun toString() = value
companion object {
fun empty() = Token()
}
}
class Credential(
val email: String = "",
val password: String = "",
) {
fun isEmpty() = listOf(email, password).any(String::isBlank)
fun isNotEmpty() = isEmpty().not()
}
@Serializable
class ResultDto<T>(
@SerialName("pagina")
@ -117,12 +84,6 @@ class ResultDto<T>(
}
}
@Serializable
class TokenDto(
@SerialName("token")
val value: String,
)
@Serializable
class MangaDto(
@SerialName("obr_id")

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 10
baseVersionCode = 9

View File

@ -67,19 +67,7 @@ abstract class Iken(
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/api/posts".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("perPage", perPage.toString())
if (apiUrl.startsWith("https://api.", true)) {
addQueryParameter("tag", "latestUpdate")
addQueryParameter("isNovel", "false")
}
}.build()
return GET(url, headers)
}
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", getFilterList())
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 22
baseVersionCode = 21

View File

@ -43,8 +43,6 @@ open class Kemono(
private val apiPath = "api/v1"
private val dataPath = "data"
private val imgCdnUrl = baseUrl.replace("//", "//img.")
private var mangasCache: List<KemonoCreatorDto> = emptyList()
@ -233,7 +231,7 @@ open class Kemono(
override fun pageListParse(response: Response): List<Page> {
val postData: KemonoPostDtoWrapped = response.parseAs()
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = "$baseUrl/$dataPath$path") }
return postData.post.images.mapIndexed { i, path -> Page(i, imageUrl = baseUrl + path) }
}
override fun imageRequest(page: Page): Request {
@ -244,7 +242,7 @@ open class Kemono(
val index = imageUrl.indexOf('/', 8)
val url = buildString {
append(imageUrl, 0, index)
append("/thumbnail")
append("/thumbnail/data")
append(imageUrl.substring(index))
}
return GET(url, headers)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -1,193 +0,0 @@
package eu.kanade.tachiyomi.multisrc.lectormonline
import eu.kanade.tachiyomi.network.GET
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import kotlin.concurrent.thread
open class LectorMOnline(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/comics?sort=views&page=$page", headers)
}
override fun popularMangaParse(response: Response): MangasPage = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/comics?page=$page", headers)
}
override fun latestUpdatesParse(response: Response): MangasPage = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addPathSegment("comics")
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is SortByFilter -> {
if (filter.selected == "views") {
url.addQueryParameter("sort", "views")
}
if (filter.state!!.ascending) {
url.addQueryParameter("isDesc", "false")
}
}
is GenreFilter -> {
val selectedGenre = filter.toUriPart()
if (selectedGenre.isNotEmpty()) {
return GET("$baseUrl/genres/$selectedGenre?page=$page", headers)
}
}
else -> { }
}
}
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
if (response.request.url.pathSegments[0] == "genres") {
return searchMangaGenreParse(document)
}
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
val jsonData = COMICS_LIST_REGEX.find(script)?.groupValues?.get(1)?.unescape()
?: throw Exception("No se pudo encontrar la lista de cómics")
val data = jsonData.parseAs<ComicListDataDto>()
return MangasPage(data.comics.map { it.toSManga() }, data.hasNextPage())
}
private fun searchMangaGenreParse(document: Document): MangasPage {
val mangas = document.select("div.grid.relative > a.group.relative").map { element ->
SManga.create().apply {
setUrlWithoutDomain(element.attr("href").substringAfter("/comics/").substringBefore("?"))
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.attr("abs:src")
}
}
val hasNextPage = document.selectFirst("div.flex.items-center > a:has(> svg):last-child:not(.pointer-events-none)") != null
return MangasPage(mangas, hasNextPage)
}
override fun getMangaUrl(manga: SManga) = "$baseUrl/comics/${manga.url}"
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl/api/app/comic/${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<ComicDto>().toSMangaDetails()
}
override fun getChapterUrl(chapter: SChapter): String {
val mangaSlug = chapter.url.substringBefore("/")
val chapterNumber = chapter.url.substringAfter("/")
return "$baseUrl/comics/$mangaSlug/chapters/$chapterNumber"
}
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
return response.parseAs<ComicDto>().getChapters()
}
override fun pageListRequest(chapter: SChapter): Request {
val mangaSlug = chapter.url.substringBefore("/")
val chapterNumber = chapter.url.substringAfter("/")
return GET("$baseUrl/api/app/comic/$mangaSlug/chapter/$chapterNumber", headers)
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<ChapterPagesDataDto>()
return data.chapter.urlImagesChapter.mapIndexed { index, image ->
Page(index, imageUrl = image)
}
}
private var genresList: List<Pair<String, String>> = emptyList()
private var fetchFiltersAttempts = 0
private var filtersState = FiltersState.NOT_FETCHED
private fun fetchFilters() {
if (filtersState != FiltersState.NOT_FETCHED || fetchFiltersAttempts >= 3) return
filtersState = FiltersState.FETCHING
fetchFiltersAttempts++
thread {
try {
val response = client.newCall(GET("$baseUrl/api/app/genres", headers)).execute()
val filters = response.parseAs<GenreListDto>()
genresList = filters.genres.map { genre -> genre.name.lowercase().replaceFirstChar { it.uppercase() } to genre.name }
filtersState = FiltersState.FETCHED
} catch (_: Throwable) {
filtersState = FiltersState.NOT_FETCHED
}
}
}
override fun getFilterList(): FilterList {
fetchFilters()
val filters = mutableListOf<Filter<*>>(
Filter.Header("El filtro por género no funciona con los demas filtros"),
Filter.Separator(),
SortByFilter(
"Ordenar por",
listOf(
SortProperty("Más vistos", "views"),
SortProperty("Más recientes", "created_at"),
),
1,
),
)
filters += if (filtersState == FiltersState.FETCHED) {
listOf(
Filter.Separator(),
Filter.Header("Filtrar por género"),
GenreFilter(genresList),
)
} else {
listOf(
Filter.Separator(),
Filter.Header("Presione 'Reiniciar' para intentar cargar los filtros"),
)
}
return FilterList(filters)
}
private enum class FiltersState { NOT_FETCHED, FETCHING, FETCHED }
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
private fun String.unescape(): String {
return UNESCAPE_REGEX.replace(this, "$1")
}
companion object {
private val UNESCAPE_REGEX = """\\(.)""".toRegex()
private val COMICS_LIST_REGEX = """\\"comicsData\\":(\{.*?\}),\\"searchParams""".toRegex()
}
}

View File

@ -1,93 +0,0 @@
package eu.kanade.tachiyomi.multisrc.lectormonline
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonPrimitive
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
class ComicListDataDto(
val comics: List<ComicDto>,
private val page: Int,
private val totalPages: Int,
) {
fun hasNextPage() = page < totalPages
}
@Serializable
class ComicDto(
private val slug: String,
private val name: String,
private val state: String?,
private val urlCover: String,
private val description: String?,
private val author: String?,
private val chapters: List<ChapterDto> = emptyList(),
) {
fun toSManga() = SManga.create().apply {
url = slug
title = name.substringBeforeLast("-").trim()
thumbnail_url = urlCover
status = state.parseStatus()
}
fun toSMangaDetails() = SManga.create().apply {
url = slug
title = name.substringBeforeLast("-").trim()
thumbnail_url = urlCover
description = this@ComicDto.description
status = state.parseStatus()
author = this@ComicDto.author
}
fun getChapters(): List<SChapter> {
return chapters.map { it.toSChapter(slug) }
}
private fun String?.parseStatus(): Int {
return when (this?.lowercase()) {
"ongoing" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
}
}
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@Serializable
class ChapterDto(
private val number: JsonPrimitive,
private val createdAt: String,
) {
fun toSChapter(mangaSlug: String) = SChapter.create().apply {
url = "$mangaSlug/$number"
name = "Capítulo $number"
date_upload = dateFormat.tryParse(createdAt)
}
}
@Serializable
class ChapterPagesDataDto(
val chapter: ChapterPagesDto,
)
@Serializable
class ChapterPagesDto(
val urlImagesChapter: List<String> = emptyList(),
)
@Serializable
class GenreListDto(
val genres: List<GenreDto>,
)
@Serializable
class GenreDto(
val name: String,
)

View File

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.multisrc.lectormonline
import eu.kanade.tachiyomi.source.model.Filter
class SortByFilter(title: String, private val sortProperties: List<SortProperty>, defaultIndex: Int) : Filter.Sort(
title,
sortProperties.map { it.name }.toTypedArray(),
Selection(defaultIndex, ascending = false),
) {
val selected: String
get() = sortProperties[state!!.index].value
}
class SortProperty(val name: String, val value: String) {
override fun toString(): String = name
}
class GenreFilter(genres: List<Pair<String, String>>) : UriPartFilter(
"Género",
arrayOf(
Pair("Todos", ""),
*genres.toTypedArray(),
),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 19
baseVersionCode = 18

View File

@ -39,22 +39,7 @@ abstract class MadTheme(
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(1, 1, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request()
val url = request.url
val response = chain.proceed(request)
if (!response.isSuccessful && url.fragment == "image-request") {
response.close()
val newUrl = url.newBuilder()
.host("sb.mbcdn.xyz")
.encodedPath(url.encodedPath.replaceFirst("/res/", "/"))
.fragment(null)
.build()
return@addInterceptor chain.proceed(request.newBuilder().url(newUrl).build())
}
response
}.build()
.build()
protected open val useLegacyApi = false
@ -344,10 +329,6 @@ abstract class MadTheme(
}
}
override fun imageRequest(page: Page): Request {
return GET("${page.imageUrl}#image-request", headers)
}
override fun imageUrlParse(document: Document): String =
throw UnsupportedOperationException()

View File

@ -1,77 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mccms
object Intl {
var lang = "zh"
val sort
get() = when (lang) {
"zh" -> "排序"
else -> "Sort by"
}
val popular
get() = when (lang) {
"zh" -> "热门人气"
else -> "Popular"
}
val latest
get() = when (lang) {
"zh" -> "更新时间"
else -> "Latest"
}
val score
get() = when (lang) {
"zh" -> "评分"
else -> "Score"
}
val status
get() = when (lang) {
"zh" -> "进度"
else -> "Status"
}
val all
get() = when (lang) {
"zh" -> "全部"
else -> "All"
}
val ongoing
get() = when (lang) {
"zh" -> "连载"
else -> "Ongoing"
}
val completed
get() = when (lang) {
"zh" -> "完结"
else -> "Completed"
}
val genreWeb
get() = when (lang) {
"zh" -> "标签"
else -> "Genre"
}
val genreApi
get() = when (lang) {
"zh" -> "标签(搜索文本时无效)"
else -> "Genre (ignored for text search)"
}
val categoryWeb
get() = when (lang) {
"zh" -> "分类筛选(搜索时无效)"
else -> "Category filters (ignored for text search)"
}
val tapReset
get() = when (lang) {
"zh" -> "点击“重置”尝试刷新标签分类"
else -> "Tap 'Reset' to load genres"
}
}

View File

@ -9,13 +9,15 @@ 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.HttpSource
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
import keiyoushi.utils.parseAs as parseAsRaw
/**
* 漫城CMS http://mccms.cn/
@ -23,26 +25,16 @@ import keiyoushi.utils.parseAs as parseAsRaw
open class MCCMS(
override val name: String,
override val baseUrl: String,
final override val lang: String = "zh",
override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest get() = true
override val supportsLatest = true
init {
Intl.lang = lang
}
private val json: Json by injectLazy()
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor { chain -> // for thumbnail requests
var request = chain.request()
val referer = request.header("Referer")
if (referer != null && !request.url.toString().startsWith(referer)) {
request = request.newBuilder().removeHeader("Referer").build()
}
chain.proceed(request)
}
.build()
}
@ -50,14 +42,12 @@ open class MCCMS(
.add("User-Agent", System.getProperty("http.agent")!!)
.add("Referer", baseUrl)
protected open fun SManga.cleanup(): SManga = this
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/api/data/comic?page=$page&size=$PAGE_SIZE&order=hits", headers)
override fun popularMangaParse(response: Response): MangasPage {
val list: List<MangaDto> = response.parseAs()
return MangasPage(list.map { it.toSManga().cleanup() }, list.size >= PAGE_SIZE)
return MangasPage(list.map { it.toSManga() }, list.size >= PAGE_SIZE)
}
override fun latestUpdatesRequest(page: Int): Request =
@ -96,7 +86,7 @@ open class MCCMS(
return client.newCall(GET(url, headers))
.asObservableSuccess().map { response ->
val list = response.parseAs<List<MangaDto>>()
list.first { it.cleanUrl == mangaUrl }.toSManga().cleanup()
list.first { it.cleanUrl == mangaUrl }.toSManga()
}
}
@ -130,7 +120,9 @@ open class MCCMS(
// Don't send referer
override fun imageRequest(page: Page) = GET(page.imageUrl!!, pcHeaders)
private inline fun <reified T> Response.parseAs(): T = parseAsRaw<ResultDto<T>>().data
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream<ResultDto<T>>(it.body.byteStream()).data
}
override fun getFilterList(): FilterList {
val genreData = config.genreData.also { it.fetchGenres(this) }

View File

@ -12,8 +12,6 @@ val pcHeaders = Headers.headersOf("User-Agent", "Mozilla/5.0 (Windows NT 10.0; W
fun String.removePathPrefix() = removePrefix("/index.php")
fun String.mobileUrl() = replace("//www.", "//m.")
open class MCCMSConfig(
hasCategoryPage: Boolean = true,
val textSearchOnlyPageOne: Boolean = false,

View File

@ -26,11 +26,11 @@ data class MangaDto(
title = Entities.unescape(name)
author = Entities.unescape(this@MangaDto.author)
description = Entities.unescape(content)
genre = Entities.unescape(tags.joinToString())
status = when (serialize) {
"连载", "連載中", "En cours", "OnGoing" -> SManga.ONGOING
"完结", "已完結", "Terminé", "Complete", "Complété" -> SManga.COMPLETED
else -> if (isUpdating(addtime)) SManga.ONGOING else SManga.UNKNOWN
genre = tags.joinToString()
status = when {
'连' in serialize || isUpdating(addtime) -> SManga.ONGOING
'完' in serialize -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
thumbnail_url = "$pic#$id"
initialized = true

View File

@ -18,31 +18,32 @@ open class MCCMSFilter(
val query get() = queries[state]
}
class SortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES)
class WebSortFilter : MCCMSFilter(Intl.sort, SORT_NAMES, SORT_QUERIES_WEB)
class SortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES)
class WebSortFilter : MCCMSFilter("排序", SORT_NAMES, SORT_QUERIES_WEB)
private val SORT_NAMES get() = arrayOf(Intl.popular, Intl.latest, Intl.score)
private val SORT_QUERIES get() = arrayOf("order=hits", "order=addtime", "order=score")
private val SORT_QUERIES_WEB get() = arrayOf("order/hits", "order/addtime", "order/score")
private val SORT_NAMES = arrayOf("热门人气", "更新时间", "评分")
private val SORT_QUERIES = arrayOf("order=hits", "order=addtime", "order=score")
private val SORT_QUERIES_WEB = arrayOf("order/hits", "order/addtime", "order/score")
class StatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES)
class WebStatusFilter : MCCMSFilter(Intl.status, STATUS_NAMES, STATUS_QUERIES_WEB)
class StatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES)
class WebStatusFilter : MCCMSFilter("进度", STATUS_NAMES, STATUS_QUERIES_WEB)
private val STATUS_NAMES get() = arrayOf(Intl.all, Intl.ongoing, Intl.completed)
private val STATUS_QUERIES get() = arrayOf("", "serialize=连载", "serialize=完结")
private val STATUS_QUERIES_WEB get() = arrayOf("", "finish/1", "finish/2")
private val STATUS_NAMES = arrayOf("全部", "连载", "完结")
private val STATUS_QUERIES = arrayOf("", "serialize=连载", "serialize=完结")
private val STATUS_QUERIES_WEB = arrayOf("", "finish/1", "finish/2")
class GenreFilter(private val values: Array<String>, private val queries: Array<String>) {
private val apiQueries get() = queries.run {
Array(size) { i -> "type[tags]=" + this[i] }.apply { this[0] = "" }
Array(size) { i -> "type[tags]=" + this[i] }
}
private val webQueries get() = queries.run {
Array(size) { i -> "tags/" + this[i] }.apply { this[0] = "" }
Array(size) { i -> "tags/" + this[i] }
}
val filter get() = MCCMSFilter(Intl.genreApi, values, apiQueries, isTypeQuery = true)
val webFilter get() = MCCMSFilter(Intl.genreWeb, values, webQueries, isTypeQuery = true)
val filter get() = MCCMSFilter("标签(搜索文本时无效)", values, apiQueries, isTypeQuery = true)
val webFilter get() = MCCMSFilter("标签", values, webQueries, isTypeQuery = true)
}
class GenreData(hasCategoryPage: Boolean) {
@ -54,12 +55,7 @@ class GenreData(hasCategoryPage: Boolean) {
status = FETCHING
thread {
try {
val request = when (source) {
// Web sources parse listings whenever possible. They call this function for mobile pages.
is MCCMSWeb -> GET("${source.baseUrl.mobileUrl()}/category/", source.headers)
else -> GET("${source.baseUrl}/category/", pcHeaders)
}
val response = source.client.newCall(request).execute()
val response = source.client.newCall(GET("${source.baseUrl}/category/", pcHeaders)).execute()
parseGenres(response.asJsoup(), this)
} catch (e: Exception) {
status = NOT_FETCHED
@ -78,7 +74,7 @@ class GenreData(hasCategoryPage: Boolean) {
internal fun parseGenres(document: Document, genreData: GenreData) {
if (genreData.status == GenreData.FETCHED || genreData.status == GenreData.NO_DATA) return
val box = document.selectFirst(".cate-selector, .cy_list_l, .ticai, .stui-screen__list")
val box = document.selectFirst(".cate-selector, .cy_list_l")
if (box == null || "/tags/" in document.location()) {
genreData.status = GenreData.NOT_FETCHED
return
@ -89,7 +85,7 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
return
}
val result = buildList(genres.size + 1) {
add(Pair(Intl.all, ""))
add(Pair("全部", ""))
genres.mapTo(this) {
val tagId = it.attr("href").substringAfterLast('/')
Pair(it.text(), tagId)
@ -104,14 +100,14 @@ internal fun parseGenres(document: Document, genreData: GenreData) {
internal fun getFilters(genreData: GenreData): FilterList {
val list = buildList(4) {
if (Intl.lang == "zh") add(StatusFilter())
add(StatusFilter())
add(SortFilter())
if (genreData.status == GenreData.NO_DATA) return@buildList
add(Filter.Separator())
if (genreData.status == GenreData.FETCHED) {
add(genreData.genreFilter.filter)
} else {
add(Filter.Header(Intl.tapReset))
add(Filter.Header("点击“重置”尝试刷新标签分类"))
}
}
return FilterList(list)
@ -119,13 +115,13 @@ internal fun getFilters(genreData: GenreData): FilterList {
internal fun getWebFilters(genreData: GenreData): FilterList {
val list = buildList(4) {
add(Filter.Header(Intl.categoryWeb))
add(Filter.Header("分类筛选(搜索时无效)"))
add(WebStatusFilter())
add(WebSortFilter())
when (genreData.status) {
GenreData.NO_DATA -> return@buildList
GenreData.FETCHED -> add(genreData.genreFilter.webFilter)
else -> add(Filter.Header(Intl.tapReset))
else -> add(Filter.Header("点击“重置”尝试刷新标签分类"))
}
}
return FilterList(list)

View File

@ -13,45 +13,39 @@ import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Evaluator
import rx.Observable
open class MCCMSWeb(
override val name: String,
override val baseUrl: String,
final override val lang: String = "zh",
protected val config: MCCMSConfig = MCCMSConfig(),
override val lang: String = "zh",
private val config: MCCMSConfig = MCCMSConfig(),
) : HttpSource() {
override val supportsLatest get() = true
init {
Intl.lang = lang
}
override val client by lazy {
network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.request.url.encodedPath == "/err/comic") {
throw IOException(response.body.string().substringBefore('\n'))
}
response
}
.build()
}
override fun headersBuilder() = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)
open fun parseListing(document: Document): MangasPage {
private fun parseListing(document: Document): MangasPage {
parseGenres(document, config.genreData)
val mangas = document.select(simpleMangaSelector()).map(::simpleMangaFromElement)
val mangas = document.select(Evaluator.Class("common-comic-item")).map {
SManga.create().apply {
val titleElement = it.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = it.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}
}
val hasNextPage = run { // default pagination
val buttons = document.selectFirst("#Pagination, .NewPages")!!.select(Evaluator.Tag("a"))
val buttons = document.selectFirst(Evaluator.Id("Pagination"))!!.select(Evaluator.Tag("a"))
val count = buttons.size
// Next page != Last page
buttons[count - 1].attr("href") != buttons[count - 2].attr("href")
@ -59,15 +53,6 @@ open class MCCMSWeb(
return MangasPage(mangas, hasNextPage)
}
open fun simpleMangaSelector() = ".common-comic-item"
open fun simpleMangaFromElement(element: Element) = SManga.create().apply {
val titleElement = element.selectFirst(Evaluator.Class("comic__title"))!!.child(0)
url = titleElement.attr("href").removePathPrefix()
title = titleElement.ownText()
thumbnail_url = element.selectFirst(Evaluator.Tag("img"))!!.attr("data-original")
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/order/hits/page/$page", pcHeaders)
override fun popularMangaParse(response: Response) = parseListing(response.asJsoup())
@ -119,8 +104,6 @@ open class MCCMSWeb(
return super.fetchMangaDetails(manga)
}
override fun getMangaUrl(manga: SManga) = baseUrl.mobileUrl() + manga.url
override fun mangaDetailsRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun mangaDetailsParse(response: Response): SManga {
@ -144,23 +127,17 @@ open class MCCMSWeb(
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url, pcHeaders)
override fun chapterListParse(response: Response): List<SChapter> {
return getDescendingChapters(
response.asJsoup().select(chapterListSelector()).map {
return run {
response.asJsoup().selectFirst(Evaluator.Class("chapter__list-box"))!!.children().map {
val link = it.child(0)
SChapter.create().apply {
url = link.attr("href").removePathPrefix()
name = link.text()
name = link.ownText()
}
},
)
}.asReversed()
}
}
open fun chapterListSelector() = ".chapter__list-box > li"
open fun getDescendingChapters(chapters: List<SChapter>) = chapters.asReversed()
override fun getChapterUrl(chapter: SChapter) = baseUrl.mobileUrl() + chapter.url
override fun pageListRequest(chapter: SChapter): Request =
GET(baseUrl + chapter.url, if (config.useMobilePageList) headers else pcHeaders)

View File

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

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import eu.kanade.tachiyomi.source.model.SChapter
import kotlinx.serialization.Serializable
@Serializable
class ResponseDto(val data: List<ChapterDto>)
@Serializable
class ChapterDto(
private val chapterid: String,
private val chaptername: String,
) {
fun toSChapter(mangaId: String) = SChapter.create().apply {
url = "$mangaId/$chapterid"
name = chaptername
}
}

View File

@ -1,44 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import eu.kanade.tachiyomi.source.model.Filter
class Option(val name: String, val value: String)
open class SelectFilter(name: String, val options: Array<Option>) :
Filter.Select<String>(name, Array(options.size) { options[it].name })
class RankingFilter : SelectFilter(
"排行榜",
arrayOf(
Option("不查看", ""),
Option("精品榜", "1"),
Option("人气榜", "2"),
Option("推荐榜", "3"),
Option("黑马榜", "4"),
Option("最近更新", "5"),
Option("新漫画", "6"),
),
)
class CategoryFilter : SelectFilter(
"分类",
arrayOf(
Option("全部", ""),
Option("冒险", "1"),
Option("热血", "2"),
Option("都市", "3"),
Option("玄幻", "4"),
Option("悬疑", "5"),
Option("耽美", "6"),
Option("恋爱", "7"),
Option("生活", "8"),
Option("搞笑", "9"),
Option("穿越", "10"),
Option("修真", "11"),
Option("后宫", "12"),
Option("女主", "13"),
Option("古风", "14"),
Option("连载", "15"),
Option("完结", "16"),
),
)

View File

@ -1,200 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import eu.kanade.tachiyomi.lib.unpacker.Unpacker
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.FormBody
import okhttp3.Request
import okhttp3.Response
// Rumanhua legacy preference:
// const val APP_CUSTOMIZATION_URL = "APP_CUSTOMIZATION_URL"
/** 漫漫看 */
open class MMLook(
override val name: String,
override val baseUrl: String,
private val desktopUrl: String,
private val useLegacyMangaUrl: Boolean,
) : HttpSource() {
override val lang: String get() = "zh"
override val supportsLatest: Boolean get() = true
override val client = network.cloudflareClient.newBuilder()
.followRedirects(false)
.hostnameVerifier { _, _ -> true }
.build()
private fun String.certificateWorkaround() = replace("https:", "http:")
private fun SManga.formatUrl() = apply { if (useLegacyMangaUrl) url = "/$url/" }
private fun rankingRequest(id: String) = GET("$desktopUrl/rank/$id", headers)
override fun popularMangaRequest(page: Int) = rankingRequest("1")
override fun popularMangaParse(response: Response): MangasPage {
val entries = response.asJsoup().select(".likedata").map { element ->
SManga.create().apply {
url = element.select("a").attr("href").mustRemoveSurrounding("/", "/")
title = element.selectFirst(".le-t")!!.text()
author = element.selectFirst(".likeinfo > p")!!.text()
.mustRemoveSurrounding("作者:", "")
description = element.selectFirst(".le-j")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
}.formatUrl()
}
return MangasPage(entries, false)
}
override fun latestUpdatesRequest(page: Int) = rankingRequest("5")
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
override fun getFilterList() = FilterList(
RankingFilter(),
Filter.Separator(),
Filter.Header("分类(搜索文本、查看排行榜时无效)"),
CategoryFilter(),
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotBlank()) {
return POST(
"$desktopUrl/s",
headers,
FormBody.Builder().add("k", query.take(12)).build(),
)
}
for (filter in filters) {
when (filter) {
is RankingFilter -> if (filter.state > 0) {
return rankingRequest(filter.options[filter.state].value)
}
is CategoryFilter -> if (filter.state > 0) {
val id = filter.options[filter.state].value
return GET("$desktopUrl/sort/$id", headers)
}
else -> {}
}
}
return popularMangaRequest(page)
}
override fun searchMangaParse(response: Response): MangasPage {
if (response.request.method == "GET") return popularMangaParse(response)
val entries = response.asJsoup().select(".col-auto").map { element ->
SManga.create().apply {
url = element.selectFirst("a")!!.attr("href").mustRemoveSurrounding("/", "/")
title = element.selectFirst(".e-title")!!.text()
author = element.selectFirst(".tip")!!.text()
thumbnail_url = element.selectFirst("img")!!.attr("data-src")
}.formatUrl()
}
return MangasPage(entries, false)
}
override fun getMangaUrl(manga: SManga): String {
val id = manga.url.removeSurrounding("/")
return "$baseUrl/$id/".certificateWorkaround()
}
// Desktop page has consistent template and more initial chapters
override fun mangaDetailsRequest(manga: SManga): Request {
val id = manga.url.removeSurrounding("/")
return GET("$desktopUrl/$id/", headers)
}
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val comicInfo = response.asJsoup().selectFirst(".comicInfo")!!
thumbnail_url = comicInfo.selectFirst("img")!!.attr("data-src")
val container = comicInfo.selectFirst(".detinfo")!!
title = container.selectFirst("h1")!!.text()
var updated = ""
for (span in container.select("span")) {
val text = span.ownText()
val value = text.substring(4).trimStart()
when (val key = text.substring(0, 4)) {
"作 者:" -> author = value
"更新时间" -> updated = "$text\n\n"
"标 签:" -> genre = value.replace(" ", ", ")
"状 态:" -> status = when (value) {
"连载中" -> SManga.ONGOING
"已完结" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
else -> throw Exception("Unknown field: $key")
}
}
description = updated + container.selectFirst(".content")!!.text()
}
// Desktop page contains more initial chapters
// "more chapter" request must be sent to the same domain
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
override fun chapterListParse(response: Response): List<SChapter> {
val container = response.asJsoup().selectFirst(".chapterlistload")!!
val chapters = container.child(0).children().mapTo(ArrayList()) { element ->
SChapter.create().apply {
url = element.attr("href").mustRemoveSurrounding("/", ".html")
name = element.text()
}
}
if (container.selectFirst(".chaplist-more") != null) {
val mangaId = response.request.url.pathSegments[0]
val request = POST(
"$desktopUrl/morechapter",
headers,
FormBody.Builder().addEncoded("id", mangaId).build(),
)
client.newCall(request).execute().parseAs<ResponseDto>().data
.mapTo(chapters) { it.toSChapter(mangaId) }
}
return chapters
}
private fun SChapter.fullUrl(): String {
val url = this.url
if (url.startsWith('/')) throw Exception("请刷新章节列表")
return "$baseUrl/$url.html"
}
override fun getChapterUrl(chapter: SChapter) = chapter.fullUrl().certificateWorkaround()
override fun pageListRequest(chapter: SChapter): Request = GET(chapter.fullUrl(), headers)
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val id = document.selectFirst(".readerContainer")!!.attr("data-id").toInt()
return document.selectFirst("script:containsData(eval)")!!.data()
.let(Unpacker::unpack)
.mustRemoveSurrounding("var __c0rst96=\"", "\"")
.let { decrypt(it, id) }
.parseAs<List<String>>()
.mapIndexed { i, imageUrl -> Page(i, imageUrl = imageUrl) }
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
}
private fun String.mustRemoveSurrounding(prefix: String, suffix: String): String {
check(startsWith(prefix) && endsWith(suffix)) { "string doesn't match $prefix[...]$suffix" }
return substring(prefix.length, length - suffix.length)
}

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mmlook
import android.util.Base64
import kotlin.experimental.xor
// all2.js?v=2.3
fun decrypt(data: String, index: Int): String {
val key = when (index) {
0 -> "smkhy258"
1 -> "smkd95fv"
2 -> "md496952"
3 -> "cdcsdwq"
4 -> "vbfsa256"
5 -> "cawf151c"
6 -> "cd56cvda"
7 -> "8kihnt9"
8 -> "dso15tlo"
9 -> "5ko6plhy"
else -> throw Exception("Unknown index: $index")
}.encodeToByteArray()
val keyLength = key.size
val bytes = Base64.decode(data, Base64.DEFAULT)
for (i in bytes.indices) {
bytes[i] = bytes[i] xor key[i % keyLength]
}
return String(Base64.decode(bytes, Base64.DEFAULT))
}

View File

@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 4
baseVersionCode = 3

View File

@ -95,7 +95,7 @@ abstract class PizzaReader(
artist = comic.artist
description = comic.description
genre = comic.genres.joinToString(", ") { it.name }
status = comic.status?.toStatus() ?: SManga.UNKNOWN
status = comic.status.toStatus()
thumbnail_url = comic.thumbnail
}

View File

@ -27,7 +27,7 @@ data class PizzaComicDto(
val description: String = "",
val genres: List<PizzaGenreDto> = emptyList(),
@SerialName("last_chapter") val lastChapter: PizzaChapterDto? = null,
val status: String? = null,
val status: String = "",
val title: String = "",
val thumbnail: String = "",
val url: String = "",

View File

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

View File

@ -431,7 +431,7 @@ abstract class Senkuro(
companion object {
private const val offsetCount = 20
private const val API_URL = "https://api.senkuro.me/graphql"
private const val API_URL = "https://api.senkuro.com/graphql"
private val senkuroExcludeGenres = listOf("hentai", "yaoi", "yuri", "shoujo_ai", "shounen_ai")
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
}

View File

@ -1,9 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".en.weebcentral.WeebCentralUrlActivity"
android:name="eu.kanade.tachiyomi.multisrc.slimereadtheme.SlimeReadThemeUrlActivity"
android:excludeFromRecents="true"
android:exported="true"
android:theme="@android:style/Theme.NoDisplay">
@ -12,10 +11,11 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="weebcentral.com"
android:pathPattern="/series/..*"
android:scheme="https" />
<data
android:host="${SOURCEHOST}"
android:pathPattern="/manga/..*"
android:scheme="${SOURCESCHEME}" />
</intent-filter>
</activity>
</application>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,273 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import app.cash.quickjs.QuickJs
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.ChapterDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.LatestResponseDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.MangaInfoDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PageListDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PopularMangaDto
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.toSMangaList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.math.min
abstract class SlimeReadTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val scanId: String = "",
) : HttpSource() {
protected open val apiUrl: String by lazy { getApiUrlFromPage() }
override val supportsLatest = true
override val client = network.cloudflareClient
private val json: Json by injectLazy()
protected open val urlInfix: String = "slimeread.com"
protected open fun getApiUrlFromPage(): String {
val initClient = network.cloudflareClient
val response = initClient.newCall(GET(baseUrl, headers)).execute()
if (!response.isSuccessful) throw Exception("HTTP error ${response.code}")
val document = response.asJsoup()
val scriptUrl = document.selectFirst("script[src*=pages/_app]")?.attr("abs:src")
?: throw Exception("Could not find script URL")
val scriptResponse = initClient.newCall(GET(scriptUrl, headers)).execute()
if (!scriptResponse.isSuccessful) throw Exception("HTTP error ${scriptResponse.code}")
val script = scriptResponse.body.string()
val apiUrl = FUNCTION_REGEX.find(script)?.let { result ->
val varBlock = result.groupValues[1]
val varUrlInfix = result.groupValues[2]
val block = """${varBlock.replace(varUrlInfix, "\"$urlInfix\"")}.toString()"""
try {
QuickJs.create().use { it.evaluate(block) as String }
} catch (e: Exception) {
null
}
}
return apiUrl?.let { "https://$it" } ?: throw Exception("Could not find API URL")
}
// ============================== Popular ===============================
private var popularMangeCache: MangasPage? = null
override fun popularMangaRequest(page: Int): Request {
val url = "$apiUrl/book_search?order=1&status=0".toHttpUrl().newBuilder()
.addIfNotBlank("scan_id", scanId)
.build()
return GET(url, headers)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
popularMangeCache = popularMangeCache?.takeIf { page != 1 }
?: super.fetchPopularManga(page).toBlocking().last()
return pageableOf(page, popularMangeCache!!)
}
override fun popularMangaParse(response: Response): MangasPage {
val items = response.parseAs<List<PopularMangaDto>>()
val mangaList = items.toSMangaList()
return MangasPage(mangaList, mangaList.isNotEmpty())
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = "$apiUrl/books?page=$page".toHttpUrl().newBuilder()
.addIfNotBlank("scan_id", scanId)
.build()
return GET(url, headers)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val dto = response.parseAs<LatestResponseDto>()
val mangaList = dto.data.toSMangaList()
val hasNextPage = dto.page < dto.pages
return MangasPage(mangaList, hasNextPage)
}
// =============================== Search ===============================
private var searchMangaCache: MangasPage? = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
val id = query.removePrefix(PREFIX_SEARCH)
client.newCall(GET("$apiUrl/book/$id", headers))
.asObservableSuccess()
.map(::searchMangaByIdParse)
} else {
searchMangaCache = searchMangaCache?.takeIf { page != 1 }
?: super.fetchSearchManga(page, query, filters).toBlocking().last()
pageableOf(page, searchMangaCache!!)
}
}
private fun searchMangaByIdParse(response: Response): MangasPage {
val details = mangaDetailsParse(response)
return MangasPage(listOf(details), false)
}
override fun getFilterList() = SlimeReadThemeFilters.FILTER_LIST
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val params = SlimeReadThemeFilters.getSearchParameters(filters)
val url = "$apiUrl/book_search".toHttpUrl().newBuilder()
.addIfNotBlank("query", query)
.addIfNotBlank("genre[]", params.genre)
.addIfNotBlank("status", params.status)
.addIfNotBlank("searchMethod", params.searchMethod)
.addIfNotBlank("scan_id", scanId)
.apply {
params.categories.forEach {
addQueryParameter("categories[]", it)
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response) = popularMangaParse(response)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.replace("/book/", "/manga/")
override fun mangaDetailsRequest(manga: SManga) = GET(apiUrl + manga.url, headers)
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val info = response.parseAs<MangaInfoDto>()
thumbnail_url = info.thumbnail_url
title = info.name
description = info.description
genre = info.categories.joinToString()
url = "/book/${info.id}"
status = when (info.status) {
1 -> SManga.ONGOING
2 -> SManga.COMPLETED
3, 4 -> SManga.CANCELLED
5 -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
// ============================== Chapters ==============================
override fun chapterListRequest(manga: SManga) =
GET("$apiUrl/book_cap_units_all?manga_id=${manga.url.substringAfterLast("/")}", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val items = response.parseAs<List<ChapterDto>>()
val mangaId = response.request.url.queryParameter("manga_id")!!
return items.map {
SChapter.create().apply {
name = "Cap " + parseChapterNumber(it.number)
date_upload = parseChapterDate(it.updated_at)
chapter_number = it.number
scanlator = it.scan?.scan_name
url = "/book_cap_units?manga_id=$mangaId&cap=${it.number}"
}
}.reversed()
}
private fun parseChapterNumber(number: Float): String {
val cap = number + 1F
return "%.2f".format(cap)
.let { if (cap < 10F) "0$it" else it }
.replace(",00", "")
.replace(",", ".")
}
private fun parseChapterDate(date: String): Long {
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { 0L }
}
override fun getChapterUrl(chapter: SChapter): String {
val url = "$baseUrl${chapter.url}".toHttpUrl()
val id = url.queryParameter("manga_id")!!
val cap = url.queryParameter("cap")!!.toFloat()
val num = parseChapterNumber(cap)
return "$baseUrl/ler/$id/cap-$num"
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter) = GET(apiUrl + chapter.url, headers)
override fun pageListParse(response: Response): List<Page> {
val body = response.body.string()
val pages = if (body.startsWith("{")) {
json.decodeFromString<Map<String, PageListDto>>(body).values.flatMap { it.pages }
} else {
json.decodeFromString<List<PageListDto>>(body).flatMap { it.pages }
}
return pages.mapIndexed { index, item ->
Page(index, "", item.url)
}
}
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
/**
* Handles a large manga list and returns a paginated response.
* The app can't handle the large JSON list without pagination.
*
* @param page The page number to retrieve.
* @param cache The cached manga page containing the full list of mangas.
*/
private fun pageableOf(page: Int, cache: MangasPage) = Observable.just(cache).map { mangaPage ->
val mangas = mangaPage.mangas
val pageSize = 15
val currentSlice = (page - 1) * pageSize
val startIndex = min(mangas.size, currentSlice)
val endIndex = min(mangas.size, currentSlice + pageSize)
val slice = mangas.subList(startIndex, endIndex)
MangasPage(slice, hasNextPage = endIndex < mangas.size)
}
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
if (value.isNotBlank()) addQueryParameter(query, value)
return this
}
companion object {
const val PREFIX_SEARCH = "id:"
val FUNCTION_REGEX = """(\[""\.concat\("[^,]+,"\."\)\.concat\(([^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -0,0 +1,141 @@
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
object SlimeReadThemeFilters {
open class SelectFilter(
displayName: String,
val vals: Array<Pair<String, String>>,
) : Filter.Select<String>(
displayName,
vals.map { it.first }.toTypedArray(),
) {
val selected get() = vals[state].second
}
private inline fun <reified R> FilterList.getSelected(): String {
return (first { it is R } as SelectFilter).selected
}
open class CheckBoxFilterList(name: String, val pairs: Array<Pair<String, String>>) :
Filter.Group<Filter.CheckBox>(name, pairs.map { CheckBoxVal(it.first) })
private class CheckBoxVal(name: String) : Filter.CheckBox(name, false)
private inline fun <reified R> FilterList.parseCheckbox(
options: Array<Pair<String, String>>,
): Sequence<String> {
return (first { it is R } as CheckBoxFilterList).state
.asSequence()
.filter { it.state }
.map { checkbox -> options.find { it.first == checkbox.name }!!.second }
}
internal class CategoriesFilter : CheckBoxFilterList("Categorias", SlimeReadFiltersData.CATEGORIES)
internal class GenreFilter : SelectFilter("Gênero", SlimeReadFiltersData.GENRES)
internal class SearchMethodFilter : SelectFilter("Método de busca", SlimeReadFiltersData.SEARCH_METHODS)
internal class StatusFilter : SelectFilter("Status", SlimeReadFiltersData.STATUS)
val FILTER_LIST get() = FilterList(
CategoriesFilter(),
GenreFilter(),
SearchMethodFilter(),
StatusFilter(),
)
data class FilterSearchParams(
val categories: Sequence<String> = emptySequence(),
val genre: String = "",
val searchMethod: String = "",
val status: String = "",
)
internal fun getSearchParameters(filters: FilterList): FilterSearchParams {
if (filters.isEmpty()) return FilterSearchParams()
return FilterSearchParams(
filters.parseCheckbox<CategoriesFilter>(SlimeReadFiltersData.CATEGORIES),
filters.getSelected<GenreFilter>(),
filters.getSelected<SearchMethodFilter>(),
filters.getSelected<StatusFilter>(),
)
}
private object SlimeReadFiltersData {
val CATEGORIES = arrayOf(
Pair("Adulto", "125"),
Pair("Artes Marciais", "117"),
Pair("Avant Garde", "154"),
Pair("Aventura", "112"),
Pair("Ação", "146"),
Pair("Comédia", "147"),
Pair("Culinária", "126"),
Pair("Doujinshi", "113"),
Pair("Drama", "148"),
Pair("Ecchi", "127"),
Pair("Erotico", "152"),
Pair("Esporte", "135"),
Pair("Fantasia", "114"),
Pair("Ficção Científica", "120"),
Pair("Filosofico", "150"),
Pair("Harém", "128"),
Pair("Histórico", "115"),
Pair("Isekai", "129"),
Pair("Josei", "116"),
Pair("Mecha", "130"),
Pair("Militar", "149"),
Pair("Mistério", "142"),
Pair("Médico", "118"),
Pair("One-shot", "131"),
Pair("Premiado", "155"),
Pair("Psicológico", "119"),
Pair("Romance", "141"),
Pair("Seinen", "140"),
Pair("Shoujo", "133"),
Pair("Shoujo-ai", "121"),
Pair("Shounen", "139"),
Pair("Shounen-ai", "134"),
Pair("Slice-of-life", "122"),
Pair("Sobrenatural", "123"),
Pair("Sugestivo", "153"),
Pair("Terror", "144"),
Pair("Thriller", "151"),
Pair("Tragédia", "137"),
Pair("Vida Escolar", "132"),
Pair("Yaoi", "124"),
Pair("Yuri", "136"),
)
private val SELECT = Pair("Selecione", "")
val GENRES = arrayOf(
SELECT,
Pair("Manga", "29"),
Pair("Light Novel", "34"),
Pair("Manhua", "31"),
Pair("Manhwa", "30"),
Pair("Novel", "33"),
Pair("Webcomic", "35"),
Pair("Webnovel", "36"),
Pair("Webtoon", "32"),
Pair("4-Koma", "37"),
)
val SEARCH_METHODS = arrayOf(
SELECT,
Pair("Preciso", "0"),
Pair("Geral", "1"),
)
val STATUS = arrayOf(
SELECT,
Pair("Em andamento", "1"),
Pair("Completo", "2"),
Pair("Dropado", "3"),
Pair("Cancelado", "4"),
Pair("Hiato", "5"),
)
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.en.weebcentral
package eu.kanade.tachiyomi.multisrc.slimereadtheme
import android.app.Activity
import android.content.ActivityNotFoundException
@ -7,20 +7,25 @@ import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class WeebCentralUrlActivity : Activity() {
/**
* Springboard that accepts https://slimeread.com/manga/<id>/<slug> intents
* and redirects them to the main Tachiyomi process.
*/
class SlimeReadThemeUrlActivity : Activity() {
private val tag = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size >= 3) {
if (pathSegments != null && pathSegments.size > 1) {
val item = pathSegments[1]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", getEntry(pathSegments))
putExtra("query", "${SlimeReadTheme.PREFIX_SEARCH}$item")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
@ -33,10 +38,4 @@ class WeebCentralUrlActivity : Activity() {
finish()
exitProcess(0)
}
private fun getEntry(pathSegments: MutableList<String>): String? {
val id = pathSegments[1]
val slug = pathSegments[2]
return "${WeebCentral.URL_SEARCH_PREFIX}$id/$slug"
}
}

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.extension.pt.slimeread.dto
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class PopularMangaDto(
@SerialName("book_image") val thumbnail_url: String?,
@SerialName("book_id") val id: Int,
@SerialName("book_name_original") val name: String,
)
@Serializable
data class LatestResponseDto(
val pages: Int,
val page: Int,
val data: List<PopularMangaDto>,
)
fun List<PopularMangaDto>.toSMangaList(): List<SManga> = map { item ->
SManga.create().apply {
thumbnail_url = item.thumbnail_url
title = item.name
url = "/book/${item.id}"
}
}
@Serializable
data class MangaInfoDto(
@SerialName("book_id") val id: Int,
@SerialName("book_image") val thumbnail_url: String?,
@SerialName("book_name_original") val name: String,
@SerialName("book_status") val status: Int,
@SerialName("book_synopsis") val description: String?,
@SerialName("book_categories") private val _categories: List<CategoryDto>,
) {
@Serializable
data class CategoryDto(val categories: CatDto)
@Serializable
data class CatDto(@SerialName("cat_name_ptBR") val name: String)
val categories = _categories.map { it.categories.name }
}
@Serializable
data class ChapterDto(
@SerialName("btc_cap") val number: Float,
@SerialName("btc_date_updated") val updated_at: String,
val scan: ScanDto?,
) {
@Serializable
data class ScanDto(val scan_name: String?)
}
@Serializable
data class PageListDto(@SerialName("book_temp_cap_unit") val pages: List<PageDto>)
@Serializable
data class PageDto(
@SerialName("btcu_image") private val path: String,
@SerialName("btcu_provider_host") private val hostId: Int?,
) {
val url by lazy {
val baseUrl = when (hostId) {
2 -> "https://cdn.slimeread.com/"
5 -> "https://black.slimeread.com/"
else -> "https://objects.slimeread.com/"
}
baseUrl + path
}
}

View File

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

View File

@ -118,9 +118,8 @@ abstract class VerComics(
protected open val pageListSelector =
"div.wp-content p > img:not(noscript img), " +
"div.wp-content div#lector > img:not(noscript img), " +
"div.wp-content > figure img:not(noscript img), " +
"div.wp-content > img, div.wp-content > p img, " +
"div.post-imgs > img"
"div.wp-content > figure img:not(noscript img)," +
"div.wp-content > img, div.wp-content > p img"
override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector)
.mapIndexed { i, img -> Page(i, imageUrl = img.imgAttr()) }

View File

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

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,308 @@
package eu.kanade.tachiyomi.multisrc.webtoons
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter.Header
import eu.kanade.tachiyomi.source.model.Filter.Select
import eu.kanade.tachiyomi.source.model.Filter.Separator
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.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.SocketException
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
open class Webtoons(
override val name: String,
override val baseUrl: String,
override val lang: String,
open val langCode: String = lang,
open val localeForCookie: String = lang,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH),
) : ParsedHttpSource() {
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return listOf<Cookie>(
Cookie.Builder()
.domain("www.webtoons.com")
.path("/")
.name("ageGatePass")
.value("true")
.name("locale")
.value(localeForCookie)
.name("needGDPR")
.value("false")
.build(),
)
}
},
)
.addInterceptor(::sslRetryInterceptor)
.build()
// m.webtoons.com throws an SSL error that can be solved by a simple retry
private fun sslRetryInterceptor(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: SocketException) {
chain.proceed(chain.request())
}
}
private val day: String
get() {
return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
Calendar.SUNDAY -> "div._list_SUNDAY"
Calendar.MONDAY -> "div._list_MONDAY"
Calendar.TUESDAY -> "div._list_TUESDAY"
Calendar.WEDNESDAY -> "div._list_WEDNESDAY"
Calendar.THURSDAY -> "div._list_THURSDAY"
Calendar.FRIDAY -> "div._list_FRIDAY"
Calendar.SATURDAY -> "div._list_SATURDAY"
else -> {
"div"
}
}
}
val json: Json by injectLazy()
override fun popularMangaSelector() = "not using"
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", "https://www.webtoons.com/$langCode/")
protected val mobileHeaders: Headers = super.headersBuilder()
.add("Referer", "https://m.webtoons.com")
.build()
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
override fun popularMangaParse(response: Response): MangasPage {
val mangas = mutableListOf<SManga>()
val document = response.asJsoup()
var maxChild = 0
// For ongoing webtoons rows are ordered by descending popularity, count how many rows there are
document.select("div#dailyList .daily_section").forEach { day ->
day.select("li").count().let { rowCount ->
if (rowCount > maxChild) maxChild = rowCount
}
}
// Process each row
for (i in 1..maxChild) {
document.select("div#dailyList .daily_section li:nth-child($i) a").map { mangas.add(popularMangaFromElement(it)) }
}
// Add completed webtoons, no sorting needed
document.select("div.daily_lst.comp li a").map { mangas.add(popularMangaFromElement(it)) }
return MangasPage(mangas.distinctBy { it.url }, false)
}
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers)
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
manga.setUrlWithoutDomain(element.attr("href"))
manga.title = element.select("p.subj").text()
manga.thumbnail_url = element.select("img").attr("abs:src")
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun popularMangaNextPageSelector(): String? = null
override fun latestUpdatesNextPageSelector(): String? = null
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
if (!query.startsWith(URL_SEARCH_PREFIX)) {
return super.fetchSearchManga(page, query, filters)
}
val emptyResult = Observable.just(MangasPage(emptyList(), false))
// given a url to either a webtoon or an episode, returns a url path to corresponding webtoon
fun webtoonPath(u: HttpUrl) = when {
langCode == u.pathSegments[0] -> "/${u.pathSegments[0]}/${u.pathSegments[1]}/${u.pathSegments[2]}/list"
else -> "/${u.pathSegments[0]}/${u.pathSegments[1]}/list" // dongmanmanhua doesn't include langCode
}
return query.substringAfter(URL_SEARCH_PREFIX).toHttpUrlOrNull()?.let { url ->
val title_no = url.queryParameter("title_no")
val couldBeWebtoonOrEpisode = title_no != null && (url.pathSegments.size >= 3 && url.pathSegments.last().isNotEmpty())
val isThisLang = "$url".startsWith("$baseUrl/$langCode")
if (!(couldBeWebtoonOrEpisode && isThisLang)) {
emptyResult
} else {
val potentialUrl = "${webtoonPath(url)}?title_no=$title_no"
fetchMangaDetails(SManga.create().apply { this.url = potentialUrl }).map {
it.url = potentialUrl
MangasPage(listOf(it), false)
}
}
} ?: emptyResult
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/$langCode/search?keyword=$query".toHttpUrl().newBuilder()
val uriPart = (filters.find { it is SearchType } as? SearchType)?.toUriPart() ?: ""
url.addQueryParameter("searchType", uriPart)
if (uriPart != "WEBTOON" && page > 1) url.addQueryParameter("page", page.toString())
return GET(url.build(), headers)
}
override fun searchMangaSelector() = "#content > div.card_wrap.search ul:not(#filterLayer) li a"
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = "div.more_area, div.paginate a[onclick] + a"
open fun parseDetailsThumbnail(document: Document): String? {
val picElement = document.select("#content > div.cont_box > div.detail_body")
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")").removeSurrounding("\"").removeSurrounding("'")
.ifBlank { discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") }
}
override fun mangaDetailsParse(document: Document): SManga {
val detailElement = document.select("#content > div.cont_box > div.detail_header > div.info")
val infoElement = document.select("#_asideDetail")
val manga = SManga.create()
manga.title = document.selectFirst("h1.subj, h3.subj")!!.text()
manga.author = detailElement.select(".author:nth-of-type(1)").first()?.ownText()
?: detailElement.select(".author_area").first()?.ownText()
manga.artist = detailElement.select(".author:nth-of-type(2)").first()?.ownText()
?: detailElement.select(".author_area").first()?.ownText() ?: manga.author
manga.genre = detailElement.select(".genre").joinToString(", ") { it.text() }
manga.description = infoElement.select("p.summary").text()
manga.status = infoElement.select("p.day_info").firstOrNull()?.text().orEmpty().toStatus()
manga.thumbnail_url = parseDetailsThumbnail(document)
return manga
}
open fun String.toStatus(): Int = when {
contains("UP") -> SManga.ONGOING
contains("COMPLETED") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun imageUrlParse(document: Document): String = document.select("img").first()!!.attr("src")
// Filters
override fun getFilterList(): FilterList {
return FilterList(
Header("Query can not be blank"),
Separator(),
SearchType(getOfficialList()),
)
}
override fun chapterListSelector() = "ul#_episodeList li[id*=episode]"
private class SearchType(vals: Array<Pair<String, String>>) : UriPartFilter("Official or Challenge", vals)
private fun getOfficialList() = arrayOf(
Pair("Any", ""),
Pair("Official only", "WEBTOON"),
Pair("Challenge only", "CHALLENGE"),
)
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a")
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = element.select("a > div.row > div.info > p.sub_title > span.ellipsis").text()
val select = element.select("a > div.row > div.num")
if (select.isNotEmpty()) {
chapter.name += " Ch. " + select.text().substringAfter("#")
}
if (element.select(".ico_bgm").isNotEmpty()) {
chapter.name += ""
}
chapter.date_upload = element.select("a > div.row > div.col > div.sub_info > span.date").text().let { chapterParseDate(it) } ?: 0
return chapter
}
open fun chapterParseDate(date: String): Long {
return try {
dateFormat.parse(date)?.time ?: 0
} catch (e: ParseException) {
0
}
}
override fun chapterListRequest(manga: SManga) = GET("https://m.webtoons.com" + manga.url, mobileHeaders)
override fun pageListParse(document: Document): List<Page> {
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
if (pages.isNotEmpty()) { return pages }
val docString = document.toString()
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
val motiontoonResponse = client.newCall(GET(docUrl, headers)).execute()
val motiontoonJson = json.parseToJsonElement(motiontoonResponse.body.string()).jsonObject
val motiontoonImages = motiontoonJson["assets"]!!.jsonObject["image"]!!.jsonObject
return motiontoonImages.entries
.filter { it.key.contains("layer") }
.mapIndexed { i, entry ->
Page(i, "", motiontoonPath + entry.value.jsonPrimitive.content)
}
}
companion object {
const val URL_SEARCH_PREFIX = "url:"
}
}

View File

@ -0,0 +1,226 @@
package eu.kanade.tachiyomi.multisrc.webtoons
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
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 kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
open class WebtoonsTranslate(
override val name: String,
override val baseUrl: String,
override val lang: String,
private val translateLangCode: String,
) : Webtoons(name, baseUrl, lang) {
// popularMangaRequest already returns manga sorted by latest update
override val supportsLatest = false
private val apiBaseUrl = "https://global.apis.naver.com".toHttpUrl()
private val mobileBaseUrl = "https://m.webtoons.com".toHttpUrl()
private val thumbnailBaseUrl = "https://mwebtoon-phinf.pstatic.net"
private val pageSize = 24
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.removeAll("Referer")
.add("Referer", mobileBaseUrl.toString())
private fun mangaRequest(page: Int, requeztSize: Int): Request {
val url = apiBaseUrl
.resolve("/lineWebtoon/ctrans/translatedWebtoons_jsonp.json")!!
.newBuilder()
.addQueryParameter("orderType", "UPDATE")
.addQueryParameter("offset", "${(page - 1) * requeztSize}")
.addQueryParameter("size", "$requeztSize")
.addQueryParameter("languageCode", translateLangCode)
.build()
return GET(url, headers)
}
// Webtoons translations doesn't really have a "popular" sort; just "UPDATE", "TITLE_ASC",
// and "TITLE_DESC". Pick UPDATE as the most useful sort.
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, pageSize)
override fun popularMangaParse(response: Response): MangasPage {
val offset = response.request.url.queryParameter("offset")!!.toInt()
val result = json.parseToJsonElement(response.body.string()).jsonObject
val responseCode = result["code"]!!.jsonPrimitive.content
if (responseCode != "000") {
throw Exception("Error getting popular manga: error code $responseCode")
}
val titles = result["result"]!!.jsonObject
val totalCount = titles["totalCount"]!!.jsonPrimitive.int
val mangaList = titles["titleList"]!!.jsonArray
.map { mangaFromJson(it.jsonObject) }
return MangasPage(mangaList, hasNextPage = totalCount > pageSize + offset)
}
private fun mangaFromJson(manga: JsonObject): SManga {
val relativeThumnailURL = manga["thumbnailIPadUrl"]?.jsonPrimitive?.contentOrNull
?: manga["thumbnailMobileUrl"]?.jsonPrimitive?.contentOrNull
return SManga.create().apply {
title = manga["representTitle"]!!.jsonPrimitive.content
author = manga["writeAuthorName"]!!.jsonPrimitive.content
artist = manga["pictureAuthorName"]?.jsonPrimitive?.contentOrNull ?: author
thumbnail_url = if (relativeThumnailURL != null) "$thumbnailBaseUrl$relativeThumnailURL" else null
status = SManga.UNKNOWN
url = mobileBaseUrl
.resolve("/translate/episodeList")!!
.newBuilder()
.addQueryParameter("titleNo", manga["titleNo"]!!.jsonPrimitive.int.toString())
.addQueryParameter("languageCode", translateLangCode)
.addQueryParameter("teamVersion", (manga["teamVersion"]?.jsonPrimitive?.intOrNull ?: 0).toString())
.build()
.toString()
}
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query)
}
}
/**
* Don't see a search function for Fan Translations, so let's do it client side.
* There's 75 webtoons as of 2019/11/21, a hardcoded request of 200 should be a sufficient request
* to get all titles, in 1 request, for quite a while
*/
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = mangaRequest(page, 200)
private fun searchMangaParse(response: Response, query: String): MangasPage {
val result = json.parseToJsonElement(response.body.string()).jsonObject
val responseCode = result["code"]!!.jsonPrimitive.content
if (responseCode != "000") {
throw Exception("Error getting manga: error code $responseCode")
}
val mangaList = result["result"]!!.jsonObject["titleList"]!!.jsonArray
.map { mangaFromJson(it.jsonObject) }
.filter { it.title.contains(query, ignoreCase = true) }
return MangasPage(mangaList, false)
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(manga.url, headers)
}
override fun mangaDetailsParse(document: Document): SManga {
val getMetaProp = fun(property: String): String =
document.head().select("meta[property=\"$property\"]").attr("content")
var parsedAuthor = getMetaProp("com-linewebtoon:webtoon:author")
var parsedArtist = parsedAuthor
val authorSplit = parsedAuthor.split(" / ", limit = 2)
if (authorSplit.count() > 1) {
parsedAuthor = authorSplit[0]
parsedArtist = authorSplit[1]
}
return SManga.create().apply {
title = getMetaProp("og:title")
artist = parsedArtist
author = parsedAuthor
description = getMetaProp("og:description")
status = SManga.UNKNOWN
thumbnail_url = getMetaProp("og:image")
}
}
override fun chapterListSelector(): String = throw UnsupportedOperationException()
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun pageListParse(document: Document): List<Page> = throw UnsupportedOperationException()
override fun chapterListRequest(manga: SManga): Request {
val mangaUrl = manga.url.toHttpUrl()
val titleNo = mangaUrl.queryParameter("titleNo")
val teamVersion = mangaUrl.queryParameter("teamVersion")
val chapterListUrl = apiBaseUrl
.resolve("/lineWebtoon/ctrans/translatedEpisodes_jsonp.json")!!
.newBuilder()
.addQueryParameter("titleNo", titleNo)
.addQueryParameter("languageCode", translateLangCode)
.addQueryParameter("offset", "0")
.addQueryParameter("limit", "10000")
.addQueryParameter("teamVersion", teamVersion)
.toString()
return GET(chapterListUrl, mobileHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.parseToJsonElement(response.body.string()).jsonObject
val responseCode = result["code"]!!.jsonPrimitive.content
if (responseCode != "000") {
val message = result["message"]?.jsonPrimitive?.content ?: "error code $responseCode"
throw Exception("Error getting chapter list: $message")
}
return result["result"]!!.jsonObject["episodes"]!!.jsonArray
.filter { it.jsonObject["translateCompleted"]!!.jsonPrimitive.boolean }
.map { parseChapterJson(it.jsonObject) }
.reversed()
}
private fun parseChapterJson(obj: JsonObject): SChapter = SChapter.create().apply {
name = obj["title"]!!.jsonPrimitive.content + " #" + obj["episodeSeq"]!!.jsonPrimitive.int
chapter_number = obj["episodeSeq"]!!.jsonPrimitive.int.toFloat()
date_upload = obj["updateYmdt"]!!.jsonPrimitive.long
scanlator = obj["teamVersion"]!!.jsonPrimitive.int.takeIf { it != 0 }?.toString() ?: "(wiki)"
val chapterUrl = apiBaseUrl
.resolve("/lineWebtoon/ctrans/translatedEpisodeDetail_jsonp.json")!!
.newBuilder()
.addQueryParameter("titleNo", obj["titleNo"]!!.jsonPrimitive.int.toString())
.addQueryParameter("episodeNo", obj["episodeNo"]!!.jsonPrimitive.int.toString())
.addQueryParameter("languageCode", obj["languageCode"]!!.jsonPrimitive.content)
.addQueryParameter("teamVersion", obj["teamVersion"]!!.jsonPrimitive.int.toString())
.toString()
setUrlWithoutDomain(chapterUrl)
}
override fun pageListRequest(chapter: SChapter): Request {
return GET(apiBaseUrl.resolve(chapter.url)!!, headers)
}
override fun pageListParse(response: Response): List<Page> {
val result = json.parseToJsonElement(response.body.string()).jsonObject
return result["result"]!!.jsonObject["imageInfo"]!!.jsonArray
.mapIndexed { i, jsonEl ->
Page(i, "", jsonEl.jsonObject["imageUrl"]!!.jsonPrimitive.content)
}
}
override fun getFilterList(): FilterList = FilterList()
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.all.webtoons
package eu.kanade.tachiyomi.multisrc.webtoons
import android.app.Activity
import android.content.ActivityNotFoundException
@ -18,28 +18,24 @@ import kotlin.system.exitProcess
*/
class WebtoonsUrlActivity : Activity() {
private val name = javaClass.simpleName
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val titleNo = intent?.data?.getQueryParameter("title_no")
val path = intent?.data?.pathSegments
if (titleNo != null && path != null && path.size >= 3) {
val lang = path[0]
val type = path[1]
val pathSegments = intent?.data?.pathSegments
val title_no = intent?.data?.getQueryParameter("title_no")
if (pathSegments != null && pathSegments.size >= 3 && title_no != null) {
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "$ID_SEARCH_PREFIX$type:$lang:$titleNo")
putExtra("query", "${Webtoons.URL_SEARCH_PREFIX}${intent?.data?.toString()}")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e(name, e.toString())
Log.e("WebtoonsUrlActivity", e.toString())
}
} else {
Log.e(name, "could not parse uri from intent $intent")
Log.e("WebtoonsUrlActivity", "could not parse uri from intent $intent")
}
finish()

View File

@ -129,22 +129,19 @@ abstract class YuYu(
genre = details.select(".genre-tag").joinToString { it.text() }
description = details.selectFirst(".sinopse p")?.text()
details.selectFirst(".manga-meta > div")?.ownText()?.let {
status = it.toStatus()
status = when (it.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location())
}
protected fun String.toStatus(): Int {
return when (lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
protected open fun getMangaId(manga: SManga): String {
val document = client.newCall(mangaDetailsRequest(manga)).execute().asJsoup()
private fun SManga.fetchMangaId(): String {
val document = client.newCall(mangaDetailsRequest(this)).execute().asJsoup()
return document.select("script")
.map(Element::data)
.firstOrNull(MANGA_ID_REGEX::containsMatchIn)
@ -162,11 +159,11 @@ abstract class YuYu(
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = getMangaId(manga)
val mangaId = manga.fetchMangaId()
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto<String>>()
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (dto.hasNext())
@ -197,7 +194,7 @@ abstract class YuYu(
// ============================== Utilities ===========================
@Serializable
class ChaptersDto<T>(val chapters: T, private val remaining: Int) {
class ChaptersDto(val chapters: String, private val remaining: Int) {
fun hasNext() = remaining > 0
}

View File

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

View File

@ -1,83 +0,0 @@
package eu.kanade.tachiyomi.multisrc.zerotheme
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.interceptor.rateLimit
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.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
abstract class ZeroTheme(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : HttpSource() {
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(2)
.build()
open val cdnUrl: String = "https://cdn.${baseUrl.substringAfterLast("/")}"
open val imageLocation: String = "images"
private val sourceLocation: String get() = "$cdnUrl/$imageLocation"
// =========================== Popular ================================
override fun popularMangaRequest(page: Int) = searchMangaRequest(page, "", FilterList())
override fun popularMangaParse(response: Response) = searchMangaParse(response)
// =========================== Latest ===================================
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesParse(response: Response): MangasPage =
MangasPage(response.toDto<LatestDto>().toSMangaList(sourceLocation), hasNextPage = false)
// =========================== Search =================================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/api/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val dto = response.parseAs<SearchDto>()
val mangas = dto.mangas.map { it.toSManga(sourceLocation) }
return MangasPage(mangas, hasNextPage = dto.hasNextPage())
}
// =========================== Details =================================
override fun mangaDetailsParse(response: Response) = response.toDto<MangaDetailsDto>().toSManga(sourceLocation)
// =========================== Chapter =================================
override fun chapterListParse(response: Response) = response.toDto<MangaDetailsDto>().toSChapterList()
// =========================== Pages ===================================
override fun pageListParse(response: Response): List<Page> =
response.toDto<PageDto>().toPageList(sourceLocation)
override fun imageUrlParse(response: Response) = ""
// =========================== Utilities ===============================
inline fun <reified T> Response.toDto(): T {
val jsonString = asJsoup().selectFirst("[data-page]")!!.attr("data-page")
return jsonString.parseAs<T>()
}
}

View File

@ -1,139 +0,0 @@
package eu.kanade.tachiyomi.multisrc.zerotheme
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import keiyoushi.utils.tryParse
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonNames
import org.jsoup.Jsoup
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class Props<T>(
@JsonNames("comic_infos", "chapter", "new_chapters")
val content: T,
)
@Serializable
class LatestDto(
private val props: Props<List<Comic>>,
) {
fun toSMangaList(srcPath: String) = props.content.map { it.comic.toSManga(srcPath) }
@Serializable
class Comic(
val comic: MangaDto,
)
}
@Serializable
class MangaDetailsDto(
private val props: Props<MangaDto>,
) {
fun toSManga(srcPath: String) = props.content.toSManga(srcPath)
fun toSChapterList() = props.content.chapters!!.map { it.toSChapter() }
}
@Serializable
class PageDto(
val props: Props<ChapterWrapper>,
) {
fun toPageList(srcPath: String): List<Page> {
return props.content.chapter.pages
.filter { it.pathSegment.contains("xml").not() }
.mapIndexed { index, path ->
Page(index, imageUrl = "$srcPath/${path.pathSegment}")
}
}
@Serializable
class ChapterWrapper(
val chapter: Chapter,
)
@Serializable
class Chapter(
val pages: List<Image>,
)
@Serializable
class Image(
@SerialName("page_path")
val pathSegment: String,
)
}
@Serializable
class SearchDto(
@SerialName("comics")
private val page: PageDto,
) {
val mangas: List<MangaDto> get() = page.data
fun hasNextPage() = page.currentPage < page.lastPage
@Serializable
class PageDto(
val `data`: List<MangaDto>,
@SerialName("last_page")
val lastPage: Int = 0,
@SerialName("current_page")
val currentPage: Int = 0,
)
}
@Serializable
class MangaDto(
val title: String,
val description: String?,
@SerialName("cover")
val thumbnailUrl: String?,
val slug: String,
val status: List<ValueDto>? = emptyList(),
val genres: List<ValueDto>? = emptyList(),
val chapters: List<ChapterDto>? = emptyList(),
) {
fun toSManga(srcPath: String) = SManga.create().apply {
title = this@MangaDto.title
description = this@MangaDto.description?.let { Jsoup.parseBodyFragment(it).text() }
this.thumbnail_url = thumbnailUrl?.let { "$srcPath/$it" }
status = when (this@MangaDto.status?.firstOrNull()?.name?.lowercase()) {
"em andamento" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
genre = genres?.joinToString { it.name }
url = "/comic/$slug"
}
@Serializable
class ValueDto(
val name: String,
)
}
@Serializable
class ChapterDto(
@SerialName("chapter_number")
val number: Float,
@SerialName("chapter_path")
val path: String,
@SerialName("created_at")
val createdAt: String,
) {
fun toSChapter() = SChapter.create().apply {
name = number.toString()
chapter_number = number
date_upload = dateFormat.tryParse(createdAt)
url = "/chapter/$path"
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
}
}

View File

@ -8,6 +8,7 @@ loadAllIndividualExtensions()
* ===================================== COMMON CONFIGURATION ======================================
*/
include(":core")
include(":utils")
// Load all modules under /lib
File(rootDir, "lib").eachDir { include("lib:${it.name}") }

View File

@ -1,8 +1,9 @@
ext {
extName = 'Comic Growl'
extClass = '.ComicGrowl'
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
extVersionCode = 7
overrideVersionCode = 0
isNsfw = false
}

View File

@ -1,212 +1,63 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import eu.kanade.tachiyomi.multisrc.gigaviewer.GigaViewer
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.FilterList
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 keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.SimpleDateFormat
import java.util.Locale
class ComicGrowl(
override val lang: String = "all",
override val baseUrl: String = "https://comic-growl.com",
override val name: String = "コミックグロウル",
override val supportsLatest: Boolean = true,
) : ParsedHttpSource() {
// TODO: get manga status
// TODO: filter by status
// TODO: change cdnUrl as a array(upstream)
class ComicGrowl : GigaViewer(
"コミックグロウル",
"https://comic-growl.com",
"all",
"https://cdn-img.comic-growl.com/public/page",
) {
override val client = super.client.newBuilder()
.addNetworkInterceptor(ImageDescrambler::interceptor)
.build()
override val publisher = "BUSHIROAD WORKS"
override fun headersBuilder(): Headers.Builder {
return super.headersBuilder().set("Referer", "$baseUrl/")
override val chapterListMode = CHAPTER_LIST_LOCKED
override val supportsLatest: Boolean = true
override val client: OkHttpClient =
super.client.newBuilder().addInterceptor(::imageIntercept).build()
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
// Show only ongoing works
override fun popularMangaSelector(): String = "ul[class=\"lineup-list ongoing\"] > li > div > a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.select("h5").text()
thumbnail_url = element.select("div > img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/ranking/manga", headers)
override fun popularMangaNextPageSelector() = null
override fun popularMangaSelector() = ".ranking-item"
override fun popularMangaFromElement(element: Element): SManga {
return SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
title = element.selectFirst(".title-text")!!.text()
setImageUrlFromElement(element)
}
}
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.selectFirst(".series-h-info")!!
val authorElements = infoElement.select(".series-h-credit-user-item .article-text")
val updateDateElement = infoElement.selectFirst(".series-h-tag-label")
return SManga.create().apply {
title = infoElement.selectFirst("h1 > span:not(.g-hidden)")!!.text()
author = authorElements.joinToString { it.text() }
description = infoElement.selectFirst(".series-h-credit-info-text-text p")?.wholeText()?.trim()
setImageUrlFromElement(document.selectFirst(".series-h-img"))
status = if (updateDateElement != null) SManga.ONGOING else SManga.COMPLETED
}
}
override fun chapterListRequest(manga: SManga) = GET(baseUrl + manga.url + "/list", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(chapterListSelector()).mapIndexed { index, element ->
chapterFromElement(element).apply {
chapter_number = index.toFloat()
if (url.isEmpty()) { // need login, set a dummy url and append lock icon for chapter name
val hasLockElement = element.selectFirst(".g-payment-article.wait-free-enabled")
url = response.request.url.newBuilder().fragment("$index-$DUMMY_URL_SUFFIX").build().toString()
name = (if (hasLockElement != null) LOCK_ICON else PAY_ICON) + name
}
}
}
}
override fun chapterListSelector() = ".article-ep-list-item-img-link"
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
setUrlWithoutDomain(element.absUrl("data-href"))
name = element.selectFirst(".series-ep-list-item-h-text")!!.text()
setUploadDate(element.selectFirst(".series-ep-list-date-time"))
scanlator = PUBLISHER
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.endsWith(DUMMY_URL_SUFFIX)) {
throw Exception("Login required to see this chapter")
}
return super.pageListRequest(chapter)
}
override fun pageListParse(document: Document): List<Page> {
val pageList = mutableListOf<Page>()
// Get some essential info from document
val viewer = document.selectFirst("#comici-viewer")!!
val comiciViewerId = viewer.attr("comici-viewer-id")
val memberJwt = viewer.attr("data-member-jwt")
val requestUrl = "$baseUrl/book/contentsInfo".toHttpUrl().newBuilder()
.addQueryParameter("comici-viewer-id", comiciViewerId)
.addQueryParameter("user-id", memberJwt)
.addQueryParameter("page-from", "0")
// Initial request to get total pages
val initialRequest = GET(requestUrl.addQueryParameter("page-to", "1").build(), headers)
client.newCall(initialRequest).execute().use { initialResponseRaw ->
if (!initialResponseRaw.isSuccessful) {
throw Exception("Failed to get page list")
}
// Get all pages
val pageTo = initialResponseRaw.parseAs<PageResponse>().totalPages.toString()
val getAllPagesUrl = requestUrl.setQueryParameter("page-to", pageTo).build()
val getAllPagesRequest = GET(getAllPagesUrl, headers)
client.newCall(getAllPagesRequest).execute().use {
if (!it.isSuccessful) {
throw Exception("Failed to get page list")
}
it.parseAs<PageResponse>().result.forEach { resultItem ->
// Origin scramble string is something like [6, 9, 14, 15, 8, 3, 4, 12, 1, 5, 0, 7, 13, 2, 11, 10]
val scramble = resultItem.scramble.drop(1).dropLast(1).replace(", ", "-")
// Add fragment to let interceptor descramble the image
val imageUrl = resultItem.imageUrl.toHttpUrl().newBuilder().fragment(scramble).build()
pageList.add(
Page(index = resultItem.sort, imageUrl = imageUrl.toString()),
)
}
}
}
return pageList
}
override fun imageUrlParse(document: Document): String {
throw UnsupportedOperationException()
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val searchUrl = "$baseUrl/search".toHttpUrl().newBuilder()
.setQueryParameter("keyword", query)
.setQueryParameter("page", page.toString())
.build()
return GET(searchUrl, headers)
}
override fun searchMangaNextPageSelector() = null
override fun searchMangaSelector() = ".series-list a"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.absUrl("href"))
title = element.selectFirst(".manga-title")!!.text()
setImageUrlFromElement(element)
}
override fun latestUpdatesRequest(page: Int) = GET(baseUrl, headers)
override fun latestUpdatesNextPageSelector() = null
override fun latestUpdatesSelector() = "h2:contains(新連載) + .feature-list > .feature-item"
override fun latestUpdatesSelector() =
"div[class=\"update latest\"] > div.card-board > " + "div[class~=card]:not([class~=ad]) > div > a"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
title = element.selectFirst("h3")!!.text()
setImageUrlFromElement(element)
title = element.select("div.data h3").text()
thumbnail_url = element.select("div.thumb-container img").attr("data-src")
setUrlWithoutDomain(element.attr("href"))
}
// ========================================= Helper Functions =====================================
override fun getCollections(): List<Collection> = listOf(
Collection("連載作品", ""),
)
companion object {
private const val PUBLISHER = "BUSHIROAD WORKS"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
if (query.isNotEmpty()) {
val url = "$baseUrl/search".toHttpUrl().newBuilder().addQueryParameter("q", query)
private val imageUrlRegex by lazy { Regex("^.*?webp") }
private val DATE_PARSER by lazy { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT) }
private const val DUMMY_URL_SUFFIX = "NeedLogin"
private const val PAY_ICON = "💴 "
private const val LOCK_ICON = "🔒 "
}
/**
* Set cover image url from [element] for [SManga]
*/
private fun SManga.setImageUrlFromElement(element: Element?) {
if (element == null) {
return
return GET(url.build(), headers)
}
val match = imageUrlRegex.find(element.selectFirst("source")!!.attr("data-srcset"))
// Add missing protocol
if (match != null) {
this.thumbnail_url = "https:${match.value}"
}
}
/**
* Set date_upload to [SChapter], parsing from string like "3月31日" to UNIX Epoch time.
*/
private fun SChapter.setUploadDate(element: Element?) {
if (element == null) {
return
}
this.date_upload = DATE_PARSER.tryParse(element.attr("datetime"))
return GET(baseUrl, headers) // Currently just get all ongoing works
}
}

View File

@ -1,70 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
object ImageDescrambler {
// Left-top corner position
private class TilePos(val x: Int, val y: Int)
/**
* Interceptor to descramble the image.
*/
fun interceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
val scramble = request.url.fragment ?: return response // return if no scramble fragment
val tiles = buildList {
scramble.split("-").forEachIndexed { index, s ->
val scrambleInt = s.toInt()
add(index, TilePos(scrambleInt / 4, scrambleInt % 4))
}
}
val scrambledImg = BitmapFactory.decodeStream(response.body.byteStream())
val descrambledImg = drawDescrambledImage(scrambledImg, scrambledImg.width, scrambledImg.height, tiles)
val output = ByteArrayOutputStream()
descrambledImg.compress(Bitmap.CompressFormat.JPEG, 90, output)
val body = output.toByteArray().toResponseBody("image/jpeg".toMediaType())
return response.newBuilder().body(body).build()
}
private fun drawDescrambledImage(rawImage: Bitmap, width: Int, height: Int, tiles: List<TilePos>): Bitmap {
// Prepare canvas
val descrambledImg = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(descrambledImg)
// Tile width and height(4x4)
val tileWidth = width / 4
val tileHeight = height / 4
// Draw rect
var count = 0
for (x in 0..3) {
for (y in 0..3) {
val desRect = Rect(x * tileWidth, y * tileHeight, (x + 1) * tileWidth, (y + 1) * tileHeight)
val srcRect = Rect(
tiles[count].x * tileWidth,
tiles[count].y * tileHeight,
(tiles[count].x + 1) * tileWidth,
(tiles[count].y + 1) * tileHeight,
)
canvas.drawBitmap(rawImage, srcRect, desRect, null)
count++
}
}
return descrambledImg
}
}

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.extension.all.comicgrowl
import kotlinx.serialization.Serializable
@Serializable
class PageResponse(
val totalPages: Int,
val result: List<PageResponseResult>,
)
@Serializable
class PageResponseResult(
val imageUrl: String,
val scramble: String,
val sort: Int,
)

View File

@ -22,6 +22,3 @@ score_position_top=Top
score_position_middle=Middle
score_position_bottom=Bottom
score_position_none=Hide Score
chapter_score_filtering_title=Automatically de-duplicate chapters
chapter_score_filtering_on=For each chapter, only displays the scanlator with the highest score
chapter_score_filtering_off=Does not filterout any chapters based on score (any other scanlator filtering will still apply)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comick'
extClass = '.ComickFactory'
extVersionCode = 57
extVersionCode = 56
isNsfw = true
}

View File

@ -178,20 +178,6 @@ abstract class Comick(
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = CHAPTER_SCORE_FILTERING_PREF
title = intl["chapter_score_filtering_title"]
summaryOff = intl["chapter_score_filtering_off"]
summaryOn = intl["chapter_score_filtering_on"]
setDefaultValue(CHAPTER_SCORE_FILTERING_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(CHAPTER_SCORE_FILTERING_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
}
private val SharedPreferences.ignoredGroups: Set<String>
@ -238,9 +224,6 @@ abstract class Comick(
private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
private val SharedPreferences.chapterScoreFiltering: Boolean
get() = getBoolean(CHAPTER_SCORE_FILTERING_PREF, CHAPTER_SCORE_FILTERING_DEFAULT)
override fun headersBuilder() = Headers.Builder().apply {
add("Referer", "$baseUrl/")
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
@ -563,19 +546,9 @@ abstract class Comick(
publishedChapter && noGroupBlock
}
.filterOnScore(preferences.chapterScoreFiltering)
.map { it.toSChapter(mangaUrl) }
}
private fun List<Chapter>.filterOnScore(shouldFilter: Boolean): Collection<Chapter> {
if (shouldFilter) {
return groupBy { it.chap }
.map { (_, chapters) -> chapters.maxBy { it.score } }
} else {
return this
}
}
private val publishedDateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
@ -653,8 +626,6 @@ abstract class Comick(
const val SCORE_POSITION_DEFAULT = "top"
private const val LOCAL_TITLE_PREF = "LocalTitle"
private const val LOCAL_TITLE_DEFAULT = false
private const val CHAPTER_SCORE_FILTERING_PREF = "ScoreAutoFiltering"
private const val CHAPTER_SCORE_FILTERING_DEFAULT = false
private const val LIMIT = 20
private const val CHAPTERS_LIMIT = 99999
}

View File

@ -199,14 +199,10 @@ class Chapter(
private val title: String = "",
@SerialName("created_at") private val createdAt: String = "",
@SerialName("publish_at") val publishedAt: String = "",
val chap: String = "",
private val chap: String = "",
private val vol: String = "",
@SerialName("group_name") val groups: List<String> = emptyList(),
@SerialName("up_count") private val upCount: Int,
@SerialName("down_count") private val downCount: Int,
) {
val score get() = upCount - downCount
fun toSChapter(mangaUrl: String) = SChapter.create().apply {
url = "$mangaUrl/$hid-chapter-$chap-$lang"
name = beautifyChapterName(vol, chap, title)

View File

@ -1,7 +1,7 @@
ext {
extName = 'DeviantArt'
extClass = '.DeviantArt'
extVersionCode = 9
extVersionCode = 8
isNsfw = true
}

View File

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
@ -21,6 +20,7 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -43,6 +43,14 @@ class DeviantArt : HttpSource(), ConfigurableSource {
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
}
private fun parseDate(dateStr: String?): Long {
return try {
dateFormat.parse(dateStr ?: "")!!.time
} catch (_: ParseException) {
0L
}
}
override fun popularMangaRequest(page: Int): Request {
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
}
@ -86,9 +94,9 @@ class DeviantArt : HttpSource(), ConfigurableSource {
return SManga.create().apply {
setUrlWithoutDomain(response.request.url.toString())
author = document.title().substringBefore(" ")
title = when {
artistInTitle -> "$author - $galleryName"
else -> galleryName
title = when (artistInTitle) {
true -> "$author - $galleryName"
false -> galleryName
}
description = gallery?.selectFirst(".legacy-journal")?.wholeText()
thumbnail_url = gallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
@ -134,7 +142,7 @@ class DeviantArt : HttpSource(), ConfigurableSource {
SChapter.create().apply {
setUrlWithoutDomain(it.selectFirst("link")!!.text())
name = it.selectFirst("title")!!.text()
date_upload = dateFormat.tryParse(it.selectFirst("pubDate")?.text())
date_upload = parseDate(it.selectFirst("pubDate")?.text())
scanlator = it.selectFirst("media|credit")?.text()
}
}
@ -154,17 +162,16 @@ class DeviantArt : HttpSource(), ConfigurableSource {
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val buttons = document.selectFirst("[draggable=false]")?.children()
return if (buttons == null) {
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
listOf(Page(0, imageUrl = imageUrl))
} else {
buttons.mapIndexed { i, button ->
val firstImageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
return when (val buttons = document.selectFirst("[draggable=false]")?.children()) {
null -> listOf(Page(0, imageUrl = firstImageUrl))
else -> buttons.mapIndexed { i, button ->
// Remove everything past "/v1/" to get original instead of thumbnail
// But need to preserve the query parameter where the token is
val imageUrl = button.selectFirst("img")?.absUrl("src")
?.replaceFirst(Regex("""/v1(/.*)?(?=\?)"""), "")
val imageUrl = button.selectFirst("img")?.absUrl("src")?.substringBefore("/v1/")
Page(i, imageUrl = imageUrl)
}.also {
// First image needs token to get original, which is included in firstImageUrl
it[0].imageUrl = firstImageUrl
}
}
}

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