Compare commits

..

No commits in common. "07eba2f8f2890c1aa39196ce16d4fb771c5745c6" and "43bd7342e7b6da152c3ea3e0ccb72fcceed0c358" have entirely different histories.

1500 changed files with 8044 additions and 9478 deletions

View File

@ -86,7 +86,7 @@ small, just do a normal full clone instead.**
```bash
git sparse-checkout set --cone --sparse-index
# add project folders
git sparse-checkout add buildSrc core gradle lib lib-multisrc utils
git sparse-checkout add buildSrc core gradle lib lib-multisrc
# add a single source
git sparse-checkout add src/<lang>/<source>
```

View File

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

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.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

2
gradlew generated vendored
View File

@ -205,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.bakkin
import android.app.Application
import android.os.Build
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
@ -13,7 +14,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject
@ -33,7 +33,9 @@ abstract class BakkinReaderX(
"Android ${Build.VERSION.RELEASE}; Mobile) " +
"Tachiyomi/${AppInfo.getVersionName()}"
protected val preferences by getPreferencesLazy()
protected val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
private val json by lazy { Injekt.get<Json>() }

View File

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

View File

@ -19,23 +19,18 @@ 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.getPreferencesLazy
import keiyoushi.utils.tryParse
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@ -51,7 +46,9 @@ abstract class ColaManga(
private val intl = ColaMangaIntl(lang)
private val preferences by getPreferencesLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val client = network.cloudflareClient.newBuilder()
.rateLimitHost(
@ -153,8 +150,6 @@ abstract class ColaManga(
protected abstract val genreTitle: String
protected abstract val statusOngoing: String
protected abstract val statusCompleted: String
protected abstract val lastUpdated: String
private val dateFormat: SimpleDateFormat = SimpleDateFormat("yyyy-MM-dd")
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
title = document.selectFirst("h1.fed-part-eone")!!.text()
@ -173,15 +168,6 @@ abstract class ColaManga(
override fun chapterListSelector(): String = "div:not(.fed-hidden) > div.all_data_list > ul.fed-part-rows a"
override fun chapterListParse(response: Response): List<SChapter> {
val document = response.asJsoup()
return document.select(chapterListSelector()).map { chapterFromElement(it) }.apply {
if (isNotEmpty()) {
this[0].date_upload = dateFormat.tryParse(document.selectFirst("span.fed-text-muted:contains($lastUpdated) + a")?.text())
}
}
}
override fun chapterFromElement(element: Element) = SChapter.create().apply {
setUrlWithoutDomain(element.attr("href"))
name = element.attr("title")

View File

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

View File

@ -28,7 +28,7 @@ open class ComicGamma(
private val json = Injekt.get<Json>()
override val client = network.cloudflareClient.newBuilder()
override val client = network.client.newBuilder()
.addInterceptor(SpeedBinbInterceptor(json))
.build()

View File

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

View File

@ -39,7 +39,7 @@ abstract class FansubsCat(
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Tachiyomi/${AppInfo.getVersionName()}")
override val client: OkHttpClient = network.cloudflareClient
override val client: OkHttpClient = network.client
private val json: Json by injectLazy()

View File

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

View File

@ -19,7 +19,6 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
import java.nio.charset.Charset
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -247,13 +246,7 @@ abstract class FMReader(
name = element.attr(chapterNameAttrSelector).substringAfter("$mangaTitle ")
}
}
date_upload = element.select(chapterTimeSelector).let { dateElement ->
if (dateElement.hasText()) {
parseRelativeDate(dateElement.text()).takeIf { it != 0L } ?: parseAbsoluteDate(dateElement.text())
} else {
0L
}
}
date_upload = element.select(chapterTimeSelector).let { if (it.hasText()) parseRelativeDate(it.text()) else 0 }
}
}
@ -264,7 +257,6 @@ abstract class FMReader(
open val dateWordIndex = 1
open fun parseRelativeDate(date: String): Long {
try {
val value = date.split(' ')[dateValueIndex].toInt()
val dateWord = date.split(' ')[dateWordIndex].let {
if (it.contains("(")) {
@ -307,20 +299,13 @@ abstract class FMReader(
set(Calendar.MILLISECOND, 0)
}.timeInMillis
else -> {
return 0L
return 0
}
}
} catch (_: Exception) {
return 0L
}
}
open fun parseAbsoluteDate(dateStr: String): Long {
return try {
dateFormat.parse(dateStr)?.time ?: 0L
} catch (_: ParseException) {
0L
}
return runCatching { dateFormat.parse(dateStr)?.time }
.getOrNull() ?: 0L
}
open val pageListImageSelector = "img.chapter-img"

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.foolslide
import android.app.Application
import androidx.preference.CheckBoxPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
@ -11,7 +12,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -273,7 +273,9 @@ abstract class FoolSlide(
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException()
protected val preferences by getPreferencesLazy()
protected val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
CheckBoxPreference(screen.context).apply {

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.galleryadults
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceScreen
@ -17,7 +18,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -36,6 +36,8 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
@ -54,7 +56,9 @@ abstract class GalleryAdults(
.build()
/* Preferences */
protected val preferences: SharedPreferences by getPreferencesLazy()
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
protected open val useShortTitlePreference = true

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.grouple
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import eu.kanade.tachiyomi.network.GET
@ -13,7 +14,6 @@ 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.getPreferencesLazy
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
@ -22,6 +22,8 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.DecimalFormat
import java.text.ParseException
@ -35,7 +37,9 @@ abstract class GroupLe(
final override val lang: String,
) : ConfigurableSource, ParsedHttpSource() {
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val supportsLatest = true

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.guya
import android.app.Application
import android.content.SharedPreferences
import android.os.Build
import androidx.preference.ListPreference
@ -15,7 +16,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
@ -25,6 +25,8 @@ import org.jsoup.Jsoup
import org.jsoup.select.Evaluator
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class Guya(
override val name: String,
@ -46,7 +48,9 @@ abstract class Guya(
private val scanlators: ScanlatorStore = ScanlatorStore()
// Preferences configuration
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
// Request builder for the "browse" page of the manga
override fun popularMangaRequest(page: Int): Request {

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 29
baseVersionCode = 28
dependencies {
api(project(":lib:i18n"))

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.heancms
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.PreferenceScreen
@ -15,7 +16,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -26,6 +26,8 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import kotlin.concurrent.thread
@ -37,7 +39,9 @@ abstract class HeanCms(
protected val apiUrl: String = baseUrl.replace("://", "://api."),
) : ConfigurableSource, HttpSource() {
protected val preferences: SharedPreferences by getPreferencesLazy()
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val supportsLatest = true

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.hentaihand
import android.app.Application
import android.content.SharedPreferences
import android.text.InputType
import android.widget.Toast
@ -15,7 +16,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
@ -32,6 +32,8 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat
@ -316,7 +318,9 @@ abstract class HentaiHand(
// Preferences
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
screen.addPreference(screen.editTextPreference(USERNAME_TITLE, USERNAME_DEFAULT, username))

View File

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

View File

@ -93,24 +93,21 @@ class Chapter(
private val id: Int,
private val slug: String,
private val number: JsonPrimitive,
private val createdBy: Name,
private val createdAt: String,
private val chapterStatus: String,
private val isAccessible: Boolean,
private val isLocked: Boolean? = false,
private val isTimeLocked: Boolean? = false,
private val mangaPost: ChapterPostDetails,
) {
fun isPublic() = chapterStatus == "PUBLIC"
fun isAccessible() = isAccessible
fun isLocked() = (isLocked == true) || (isTimeLocked == true)
fun toSChapter(mangaSlug: String?) = SChapter.create().apply {
val prefix = if (isLocked()) "🔒 " else ""
val seriesSlug = mangaSlug ?: mangaPost.slug
url = "/series/$seriesSlug/$slug#$id"
name = "${prefix}Chapter $number"
name = "Chapter $number"
scanlator = createdBy.name
date_upload = try {
dateFormat.parse(createdAt)!!.time
} catch (_: ParseException) {

View File

@ -1,10 +1,6 @@
package eu.kanade.tachiyomi.multisrc.iken
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -13,34 +9,32 @@ 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.parseAs
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class Iken(
override val name: String,
override val lang: String,
override val baseUrl: String,
val apiUrl: String = baseUrl,
) : HttpSource(), ConfigurableSource {
) : HttpSource() {
override val supportsLatest = true
override val client = network.cloudflareClient
private val preferences: SharedPreferences by getPreferencesLazy()
private val json by injectLazy<Json>()
override fun headersBuilder() = super.headersBuilder()
.set("Referer", "$baseUrl/")
private var genres = emptyList<Pair<String, String>>()
protected val titleCache by lazy {
val response = client.newCall(GET("$apiUrl/api/query?perPage=9999", headers)).execute()
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
val data = response.parseAs<SearchResponse>()
data.posts
@ -71,7 +65,7 @@ abstract class Iken(
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/api/query".toHttpUrl().newBuilder().apply {
val url = "$baseUrl/api/query".toHttpUrl().newBuilder().apply {
addQueryParameter("page", page.toString())
addQueryParameter("perPage", perPage.toString())
addQueryParameter("searchTerm", query.trim())
@ -120,76 +114,35 @@ abstract class Iken(
throw UnsupportedOperationException()
override fun chapterListRequest(manga: SManga): Request {
return GET("$baseUrl/series/${manga.url}", headers)
val id = manga.url.substringAfterLast("#")
val url = "$baseUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid="
return GET(url, headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
val userId = userIdRegex.find(response.body.string())?.groupValues?.get(1) ?: ""
val id = response.request.url.fragment!!
val chapterUrl = "$apiUrl/api/chapters?postId=$id&skip=0&take=1000&order=desc&userid=$userId"
val chapterResponse = client.newCall(GET(chapterUrl, headers)).execute()
val data = chapterResponse.parseAs<Post<ChapterListResponse>>()
val data = response.parseAs<Post<ChapterListResponse>>()
assert(!data.post.isNovel) { "Novels are unsupported" }
return data.post.chapters
.filter { it.isPublic() && (it.isAccessible() || (preferences.getBoolean(showLockedChapterPrefKey, false) && it.isLocked())) }
.filter { it.isPublic() && it.isAccessible() }
.map { it.toSChapter(data.post.slug) }
}
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
if (document.selectFirst("svg.lucide-lock") != null) {
throw Exception("Unlock chapter in webview")
return document.select("main section img").mapIndexed { idx, img ->
Page(idx, imageUrl = img.absUrl("src"))
}
return document.getNextJson("images").parseAs<List<PageParseDto>>().mapIndexed { idx, p ->
Page(idx, imageUrl = p.url)
}
}
@Serializable
class PageParseDto(
val url: String,
)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = showLockedChapterPrefKey
title = "Show locked chapters"
setDefaultValue(false)
}.also(screen::addPreference)
}
override fun imageUrlParse(response: Response) =
throw UnsupportedOperationException()
protected fun Document.getNextJson(key: String): String {
val data = selectFirst("script:containsData($key)")
?.data()
?: throw Exception("Unable to retrieve NEXT data")
val keyIndex = data.indexOf(key)
val start = data.indexOf('[', keyIndex)
var depth = 1
var i = start + 1
while (i < data.length && depth > 0) {
when (data[i]) {
'[' -> depth++
']' -> depth--
}
i++
}
return "\"${data.substring(start, i)}\"".parseAs<String>()
}
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString(body.string())
}
private const val perPage = 18
private const val showLockedChapterPrefKey = "pref_show_locked_chapters"
private val userIdRegex = Regex(""""user\\":\{\\"id\\":\\"([^"']+)\\"""")

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.kemono
import android.app.Application
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -14,12 +15,13 @@ 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 keiyoushi.utils.getPreferences
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.lang.Thread.sleep
import java.util.TimeZone
@ -32,14 +34,15 @@ open class Kemono(
) : HttpSource(), ConfigurableSource {
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder().rateLimit(1).build()
override val client = network.client.newBuilder().rateLimit(1).build()
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
private val preferences = getPreferences()
private val preferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
private val apiPath = "api/v1"

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 15
baseVersionCode = 13
dependencies {
api(project(":lib:i18n"))

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.keyoapp
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -14,7 +15,6 @@ 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.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -23,6 +23,8 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.ParseException
import java.text.SimpleDateFormat
@ -35,7 +37,9 @@ abstract class Keyoapp(
final override val lang: String,
) : ParsedHttpSource(), ConfigurableSource {
protected val preferences: SharedPreferences by getPreferencesLazy()
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val supportsLatest = true
@ -59,8 +63,7 @@ abstract class Keyoapp(
override fun popularMangaRequest(page: Int): Request = GET(baseUrl, headers)
override fun popularMangaSelector(): String =
"div.flex-col div.grid > div.group.border, div:has(h2:contains(Trending)) + div .group.overflow-hidden.grid"
override fun popularMangaSelector(): String = "div.flex-col div.grid > div.group.border"
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
thumbnail_url = element.getImageUrl("*[style*=background-image]")
@ -188,7 +191,7 @@ abstract class Keyoapp(
}
}
protected open fun genresRequest(): Request = GET("$baseUrl/series/", headers)
private fun genresRequest(): Request = GET("$baseUrl/series/", headers)
/**
* Get the genres from the search page document.

View File

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

View File

@ -111,11 +111,7 @@ abstract class LectorMoe(
override fun chapterListParse(response: Response): List<SChapter> {
val result = json.decodeFromString<Data<SeriesDto>>(response.body.string())
val seriesSlug = result.data.slug
return result.data.chapters
?.filter { it.subscribersOnly.not() }
?.map { it.toSChapter(seriesSlug) }
?.filter { it.date_upload < System.currentTimeMillis() }
?: emptyList()
return result.data.chapters?.map { it.toSChapter(seriesSlug) } ?: emptyList()
}
override fun pageListRequest(chapter: SChapter): Request {

View File

@ -7,7 +7,6 @@ import kotlinx.serialization.Serializable
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
class Data<T>(val data: T)
@ -54,21 +53,18 @@ class SeriesAuthorDto(
val name: String,
)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
@Serializable
class SeriesChapterDto(
private val title: String,
private val number: Float,
private val releasedAt: String,
val subscribersOnly: Boolean,
private val createdAt: String,
) {
fun toSChapter(seriesSlug: String) = SChapter.create().apply {
name = "Capítulo ${number.toString().removeSuffix(".0")} - $title"
date_upload = try {
dateFormat.parse(releasedAt)?.time ?: 0L
dateFormat.parse(createdAt)?.time ?: 0L
} catch (_: ParseException) {
0L
}

View File

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

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.multisrc.libgroup
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.os.Handler
import android.os.Looper
import android.util.Log
@ -23,7 +24,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -57,10 +57,9 @@ abstract class LibGroup(
encodeDefaults = true
}
private val preferences by getPreferencesLazy {
if (getString(SERVER_PREF, "main") == "fourth") {
edit().putString(SERVER_PREF, "secondary").apply()
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.migrateOldImageServer()
}
override val supportsLatest = true
@ -686,4 +685,11 @@ abstract class LibGroup(
}
}
}
// api changed id of servers, remap SERVER_PREF old("fourth") to new("secondary")
private fun SharedPreferences.migrateOldImageServer(): SharedPreferences {
if (getString(SERVER_PREF, "main") != "fourth") return this
edit().putString(SERVER_PREF, "secondary").apply()
return this
}
}

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 5
baseVersionCode = 4
dependencies {
api(project(":lib:i18n"))

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.machinetranslations
import android.app.Application
import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
@ -19,7 +20,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@ -30,6 +30,8 @@ import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Calendar
@ -49,7 +51,9 @@ abstract class MachineTranslations(
override val lang = language.lang
protected val preferences: SharedPreferences by getPreferencesLazy()
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
/**
* A flag that tracks whether the settings have been changed. It is used to indicate if

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 41
baseVersionCode = 40
dependencies {
api(project(":lib:cryptoaes"))

View File

@ -160,7 +160,7 @@ abstract class Madara(
}
// exclude/filter bilibili manga from list
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector , .manga__item"
override fun popularMangaSelector() = "div.page-item-detail:not(:has(a[href*='bilibilicomics.com']))$mangaEntrySelector"
open val popularMangaUrlSelector = "div.post-title a"
@ -584,7 +584,7 @@ abstract class Madara(
return MangasPage(entries, hasNextPage)
}
override fun searchMangaSelector() = "div.c-tabs-item__content , .manga__item"
override fun searchMangaSelector() = "div.c-tabs-item__content"
protected open val searchMangaUrlSelector = "div.post-title a"
@ -754,7 +754,7 @@ abstract class Madara(
open val mangaDetailsSelectorTitle = "div.post-title h3, div.post-title h1, #manga-title > h1"
open val mangaDetailsSelectorAuthor = "div.author-content > a, div.manga-authors > a"
open val mangaDetailsSelectorArtist = "div.artist-content > a"
open val mangaDetailsSelectorStatus = "div.summary-content, div.summary-heading:contains(Status) + div"
open val mangaDetailsSelectorStatus = "div.summary-content"
open val mangaDetailsSelectorDescription = "div.description-summary div.summary__content, div.summary_content div.post-content_item > h5 + div, div.summary_content div.manga-excerpt"
open val mangaDetailsSelectorThumbnail = "div.summary_image img"
open val mangaDetailsSelectorGenre = "div.genres-content a"

View File

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

View File

@ -41,10 +41,6 @@ abstract class MadTheme(
.rateLimit(1, 1, TimeUnit.SECONDS)
.build()
protected open val useLegacyApi = false
protected open val useSlugSearch = false
// TODO: better cookie sharing
// TODO: don't count cached responses against rate limit
private val chapterClient: OkHttpClient = network.cloudflareClient.newBuilder()
@ -181,57 +177,59 @@ abstract class MadTheme(
val document = response.asJsoup()
// Need the total chapters to check against the request
val totalChapters = document.selectFirst(".title span:containsOwn(CHAPTERS \\()")?.text()
?.substringAfter("(")
?.substringBefore(")")
?.toIntOrNull()
val script = document.selectFirst("script:containsData(bookId)")
?: throw Exception("Cannot find script")
val bookId = script.data().substringAfter("bookId = ").substringBefore(";")
val bookSlug = script.data().substringAfter("bookSlug = \"").substringBefore("\";")
var chaptersList = document.select(chapterListSelector()).map { chapterFromElement(it) }
val fetchApi = document.selectFirst("div#show-more-chapters > span")
?.attr("onclick")?.equals("getChapters()")
?: false
if (fetchApi) {
val apiChapters = client.newCall(GET(buildChapterUrl(bookId, bookSlug), headers)).execute()
.asJsoup().select(chapterListSelector()).map { chapterFromElement(it) }
val cutIndex = chaptersList.indexOfFirst { chapter ->
apiChapters.any { it.url == chapter.url }
}.takeIf { it != -1 } ?: chaptersList.size
chaptersList = (chaptersList.subList(0, cutIndex) + apiChapters)
// Use slug search by default
val slugRequest = chapterClient.newCall(GET(buildChapterUrl(bookSlug), headers)).execute()
if (!slugRequest.isSuccessful) {
throw Exception("HTTP error ${slugRequest.code}")
}
return chaptersList
var finalDocument = slugRequest.asJsoup().select(chapterListSelector())
if (totalChapters != null && finalDocument.size < totalChapters) {
val idRequest = chapterClient.newCall(GET(buildChapterUrl(bookId), headers)).execute()
finalDocument = idRequest.asJsoup().select(chapterListSelector())
}
private fun buildChapterUrl(mangaId: String, mangaSlug: String): HttpUrl {
return finalDocument.map {
SChapter.create().apply {
url = it.selectFirst("a")!!.absUrl("href").removePrefix(baseUrl)
name = it.selectFirst(".chapter-title")!!.text()
date_upload = parseChapterDate(it.selectFirst(".chapter-update")?.text())
}
}
}
private fun buildChapterUrl(fetchByParam: String): HttpUrl {
return baseUrl.toHttpUrl().newBuilder().apply {
addPathSegment("api")
addPathSegment("manga")
addPathSegment(if (useSlugSearch) mangaSlug else mangaId)
addPathSegment(fetchByParam)
addPathSegment("chapters")
addQueryParameter("source", "detail")
}.build()
}
override fun chapterListRequest(manga: SManga): Request {
if (useLegacyApi) {
val mangaId = MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)
val url = mangaId?.let {
"$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", it)
override fun chapterListRequest(manga: SManga): Request =
MANGA_ID_REGEX.find(manga.url)?.groupValues?.get(1)?.let { mangaId ->
val url = "$baseUrl/service/backend/chaplist/".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("manga_name", manga.title)
.fragment("idFound")
.build()
.toString()
} ?: (baseUrl + manga.url)
return GET(url, headers)
}
return GET(baseUrl + manga.url, headers)
}
GET(url, headers)
} ?: GET("$baseUrl${manga.url}", headers)
override fun searchMangaParse(response: Response): MangasPage {
if (genresList == null) {

View File

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

View File

@ -1,11 +1,7 @@
package eu.kanade.tachiyomi.multisrc.mangabox
import android.annotation.SuppressLint
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
@ -13,144 +9,42 @@ 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.getPreferencesLazy
import keiyoushi.utils.tryParse
import okhttp3.Headers
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import java.util.regex.Pattern
import java.util.concurrent.TimeUnit
// Based off of Mangakakalot 1.2.8
abstract class MangaBox(
override val name: String,
private val mirrorEntries: Array<String>,
override val baseUrl: String,
override val lang: String,
private val dateFormat: SimpleDateFormat = SimpleDateFormat(
"MMM-dd-yyyy HH:mm",
Locale.ENGLISH,
).apply {
timeZone = TimeZone.getTimeZone("UTC")
},
) : ParsedHttpSource(), ConfigurableSource {
private val dateformat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH),
) : ParsedHttpSource() {
override val supportsLatest = true
override val baseUrl: String get() = mirror
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.addInterceptor(::useAltCdnInterceptor)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
private fun SharedPreferences.getMirrorPref(): String =
getString(PREF_USE_MIRROR, mirrorEntries[0])!!
private val preferences: SharedPreferences by getPreferencesLazy {
// if current mirror is not in mirrorEntries, set default
if (getMirrorPref() !in mirrorEntries.map { "${URL_PREFIX}$it" }) {
edit().putString(PREF_USE_MIRROR, "${URL_PREFIX}${mirrorEntries[0]}").apply()
}
}
private var mirror = ""
get() {
if (field.isNotEmpty()) {
return field
}
field = preferences.getMirrorPref()
return field
}
private val cdnSet =
MangaBoxLinkedCdnSet() // Stores all unique CDNs that the extension can use to retrieve chapter images
private class MangaBoxFallBackTag // Custom empty class tag to use as an identifier that the specific request is fallback-able
private fun HttpUrl.getBaseUrl(): String =
"${URL_PREFIX}${this.host}${
when (this.port) {
80, 443 -> ""
else -> ":${this.port}"
}
}"
private fun useAltCdnInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
val requestTag = request.tag(MangaBoxFallBackTag::class.java)
val originalResponse: Response? = try {
chain.proceed(request)
} catch (e: IOException) {
if (requestTag == null) {
throw e
} else {
null
}
}
if (requestTag == null || originalResponse?.isSuccessful == true) {
requestTag?.let {
// Move working cdn to first so it gets priority during iteration
cdnSet.moveItemToFirst(request.url.getBaseUrl())
}
return originalResponse!!
}
// Close the original response if it's not successful
originalResponse?.close()
for (cdnUrl in cdnSet) {
var tryResponse: Response? = null
try {
val newUrl = cdnUrl.toHttpUrl().newBuilder()
.encodedPath(request.url.encodedPath)
.fragment(request.url.fragment)
.build()
// Create a new request with the updated URL
val newRequest = request.newBuilder()
.url(newUrl)
.build()
// Proceed with the new request
tryResponse = chain.proceed(newRequest)
// Check if the response is successful
if (tryResponse.isSuccessful) {
// Move working cdn to first so it gets priority during iteration
cdnSet.moveItemToFirst(newRequest.url.getBaseUrl())
return tryResponse
}
tryResponse.close()
} catch (_: IOException) {
tryResponse?.close()
}
}
// If all CDNs fail, throw an error
return throw IOException("All CDN attempts failed.")
}
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", "$baseUrl/")
.add("Referer", baseUrl) // for covers
open val popularUrlPath = "manga-list/hot-manga?page="
open val popularUrlPath = "manga_list?type=topview&category=all&state=all&page="
open val latestUrlPath = "manga-list/latest-manga?page="
open val latestUrlPath = "manga_list?type=latest&category=all&state=all&page="
open val simpleQueryPath = "search/story/"
open val simpleQueryPath = "search/"
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
@ -164,11 +58,10 @@ abstract class MangaBox(
return GET("$baseUrl/$latestUrlPath$page", headers)
}
private fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
protected fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
return SManga.create().apply {
element.select(urlSelector).first()!!.let {
url = it.attr("abs:href")
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
title = it.text()
}
thumbnail_url = element.select("img").first()!!.attr("abs:src")
@ -179,47 +72,62 @@ abstract class MangaBox(
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element)
override fun popularMangaNextPageSelector() =
"div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
override fun popularMangaNextPageSelector() = "div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank()) {
val url = "$baseUrl/$simpleQueryPath".toHttpUrl().newBuilder()
.addPathSegment(normalizeSearchQuery(query))
.addQueryParameter("page", page.toString())
.build()
return GET(url, headers)
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
GET("$baseUrl/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
} else {
val url = "$baseUrl/genre".toHttpUrl().newBuilder()
val url = baseUrl.toHttpUrl().newBuilder()
if (getAdvancedGenreFilters().isNotEmpty()) {
url.addPathSegment("advanced_search")
url.addQueryParameter("page", page.toString())
url.addQueryParameter("keyw", normalizeSearchQuery(query))
var genreInclude = ""
var genreExclude = ""
filters.forEach { filter ->
when (filter) {
is KeywordFilter -> filter.toUriPart()?.let { url.addQueryParameter("keyt", it) }
is SortFilter -> url.addQueryParameter("orby", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("sts", filter.toUriPart())
is AdvGenreFilter -> {
filter.state.forEach { if (it.isIncluded()) genreInclude += "_${it.id}" }
filter.state.forEach { if (it.isExcluded()) genreExclude += "_${it.id}" }
}
else -> {}
}
}
url.addQueryParameter("g_i", genreInclude)
url.addQueryParameter("g_e", genreExclude)
} else {
url.addPathSegment("manga_list")
url.addQueryParameter("page", page.toString())
filters.forEach { filter ->
when (filter) {
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
is GenreFilter -> url.addPathSegment(filter.toUriPart()!!)
is GenreFilter -> url.addQueryParameter("category", filter.toUriPart())
else -> {}
}
}
}
GET(url.build(), headers)
}
}
override fun searchMangaSelector() = ".panel_story_list .story_item, div.list-truyen-item-wrap"
override fun searchMangaSelector() = ".panel_story_list .story_item"
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
override fun searchMangaNextPageSelector() =
"a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
override fun searchMangaNextPageSelector() = "a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
open val mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
open val descriptionSelector = "div#noidungm, div#panel-story-info-description, div#contentBox"
open val descriptionSelector = "div#noidungm, div#panel-story-info-description"
override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
@ -238,15 +146,11 @@ abstract class MangaBox(
return SManga.create().apply {
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
title = infoElement.select("h1, h2").first()!!.text()
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a")
.eachText().joinToString()
status = parseStatus(
infoElement.select("li:contains(status), td:containsOwn(status) + td").text(),
)
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td a").eachText().joinToString()
status = parseStatus(infoElement.select("li:contains(status), td:containsOwn(status) + td").text())
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
?.select("a")?.joinToString { it.text() } // kakalot
?: infoElement.select("td:containsOwn(genres) + td a")
.joinToString { it.text() } // nelo
?: infoElement.select("td:containsOwn(genres) + td a").joinToString { it.text() } // nelo
} ?: checkForRedirectMessage(document)
description = document.select(descriptionSelector).firstOrNull()?.ownText()
?.replace("""^$title summary:\s""".toRegex(), "")
@ -297,21 +201,42 @@ abstract class MangaBox(
private fun Element.selectDateFromElement(): Element {
val defaultChapterDateSelector = "span"
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(
alternateChapterDateSelector,
).last()!!
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(alternateChapterDateSelector).last()!!
}
override fun chapterFromElement(element: Element): SChapter {
return SChapter.create().apply {
element.select("a").let {
url = it.attr("abs:href")
.substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
name = it.text()
scanlator =
it.attr("abs:href").toHttpUrl().host // show where chapters are actually from
}
date_upload = dateFormat.tryParse(element.selectDateFromElement().attr("title"))
date_upload = parseChapterDate(element.selectDateFromElement().text(), scanlator!!) ?: 0
}
}
private fun parseChapterDate(date: String, host: String): Long? {
return if ("ago" in date) {
val value = date.split(' ')[0].toIntOrNull()
val cal = Calendar.getInstance()
when {
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, -value) }
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, -value) }
value != null && "day" in date -> cal.apply { add(Calendar.DATE, -value) }
else -> null
}?.timeInMillis
} else {
try {
if (host.contains("manganato", ignoreCase = true)) {
// Nelo's date format
SimpleDateFormat("MMM dd,yy", Locale.ENGLISH).parse(date)
} else {
dateformat.parse(date)
}
} catch (e: ParseException) {
null
}?.time
}
}
@ -322,59 +247,26 @@ abstract class MangaBox(
return super.pageListRequest(chapter)
}
private fun extractArray(scriptContent: String, arrayName: String): List<String> {
val pattern = Pattern.compile("$arrayName\\s*=\\s*\\[([^]]+)]")
val matcher = pattern.matcher(scriptContent)
val arrayValues = mutableListOf<String>()
if (matcher.find()) {
val arrayContent = matcher.group(1)
val values = arrayContent?.split(",")
if (values != null) {
for (value in values) {
arrayValues.add(
value.trim()
.removeSurrounding("\"")
.replace("\\/", "/")
.removeSuffix("/"),
)
}
}
}
return arrayValues
}
open val pageListSelector = "div#vungdoc img, div.container-chapter-reader img"
override fun pageListParse(document: Document): List<Page> {
val element = document.select("head > script").lastOrNull()
?: return emptyList()
val cdns =
extractArray(element.html(), "cdns") + extractArray(element.html(), "backupImage")
val chapterImages = extractArray(element.html(), "chapterImages")
// Add all parsed cdns to set
cdnSet.addAll(cdns)
return chapterImages.mapIndexed { i, imagePath ->
val parsedUrl = cdns[0].toHttpUrl().run {
newBuilder()
.encodedPath(
"/$imagePath".replace(
"//",
"/",
),
) // replace ensures that there's at least one trailing slash prefix
.build()
.toString()
return document.select(pageListSelector)
// filter out bad elements for mangakakalots
.filterNot { it.attr("src").endsWith("log") }
.mapIndexed { i, element ->
val url = element.attr("abs:src").let { src ->
if (src.startsWith("https://convert_image_digi.mgicdn.com")) {
"https://images.weserv.nl/?url=" + src.substringAfter("//")
} else {
src
}
Page(i, document.location(), parsedUrl)
}
Page(i, document.location(), url)
}
}
override fun imageRequest(page: Page): Request {
return GET(page.imageUrl!!, headers).newBuilder()
.tag(MangaBoxFallBackTag::class.java, MangaBoxFallBackTag()).build()
return GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
@ -390,27 +282,46 @@ abstract class MangaBox(
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
str = str.replace("đ".toRegex(), "d")
str = str.replace(
"""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(),
"_",
)
str = str.replace("""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(), "_")
str = str.replace("_+_".toRegex(), "_")
str = str.replace("""^_+|_+$""".toRegex(), "")
return str
}
override fun getFilterList() = FilterList(
override fun getFilterList() = if (getAdvancedGenreFilters().isNotEmpty()) {
FilterList(
KeywordFilter(getKeywordFilters()),
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
AdvGenreFilter(getAdvancedGenreFilters()),
)
} else {
FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
SortFilter(getSortFilters()),
StatusFilter(getStatusFilters()),
GenreFilter(getGenreFilters()),
)
}
private class KeywordFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Keyword search ", vals)
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
private class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", vals)
// For advanced search, specifically tri-state genres
private class AdvGenreFilter(vals: List<AdvGenre>) : Filter.Group<AdvGenre>("Category", vals)
class AdvGenre(val id: String?, name: String) : Filter.TriState(name)
// keyt query parameter
private fun getKeywordFilters(): Array<Pair<String?, String>> = arrayOf(
Pair(null, "Everything"),
Pair("title", "Title"),
Pair("alternative", "Alt title"),
Pair("author", "Author"),
)
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("latest", "Latest"),
Pair("newest", "Newest"),
@ -426,72 +337,53 @@ abstract class MangaBox(
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
Pair("all", "ALL"),
Pair("action", "Action"),
Pair("adult", "Adult"),
Pair("adventure", "Adventure"),
Pair("comedy", "Comedy"),
Pair("cooking", "Cooking"),
Pair("doujinshi", "Doujinshi"),
Pair("drama", "Drama"),
Pair("ecchi", "Ecchi"),
Pair("fantasy", "Fantasy"),
Pair("gender-bender", "Gender bender"),
Pair("harem", "Harem"),
Pair("historical", "Historical"),
Pair("horror", "Horror"),
Pair("isekai", "Isekai"),
Pair("josei", "Josei"),
Pair("manhua", "Manhua"),
Pair("manhwa", "Manhwa"),
Pair("martial-arts", "Martial arts"),
Pair("mature", "Mature"),
Pair("mecha", "Mecha"),
Pair("medical", "Medical"),
Pair("mystery", "Mystery"),
Pair("one-shot", "One shot"),
Pair("psychological", "Psychological"),
Pair("romance", "Romance"),
Pair("school-life", "School life"),
Pair("sci-fi", "Sci fi"),
Pair("seinen", "Seinen"),
Pair("shoujo", "Shoujo"),
Pair("shoujo-ai", "Shoujo ai"),
Pair("shounen", "Shounen"),
Pair("shounen-ai", "Shounen ai"),
Pair("slice-of-life", "Slice of life"),
Pair("smut", "Smut"),
Pair("sports", "Sports"),
Pair("supernatural", "Supernatural"),
Pair("tragedy", "Tragedy"),
Pair("webtoons", "Webtoons"),
Pair("yaoi", "Yaoi"),
Pair("yuri", "Yuri"),
Pair("2", "Action"),
Pair("3", "Adult"),
Pair("4", "Adventure"),
Pair("6", "Comedy"),
Pair("7", "Cooking"),
Pair("9", "Doujinshi"),
Pair("10", "Drama"),
Pair("11", "Ecchi"),
Pair("12", "Fantasy"),
Pair("13", "Gender bender"),
Pair("14", "Harem"),
Pair("15", "Historical"),
Pair("16", "Horror"),
Pair("45", "Isekai"),
Pair("17", "Josei"),
Pair("44", "Manhua"),
Pair("43", "Manhwa"),
Pair("19", "Martial arts"),
Pair("20", "Mature"),
Pair("21", "Mecha"),
Pair("22", "Medical"),
Pair("24", "Mystery"),
Pair("25", "One shot"),
Pair("26", "Psychological"),
Pair("27", "Romance"),
Pair("28", "School life"),
Pair("29", "Sci fi"),
Pair("30", "Seinen"),
Pair("31", "Shoujo"),
Pair("32", "Shoujo ai"),
Pair("33", "Shounen"),
Pair("34", "Shounen ai"),
Pair("35", "Slice of life"),
Pair("36", "Smut"),
Pair("37", "Sports"),
Pair("38", "Supernatural"),
Pair("39", "Tragedy"),
Pair("40", "Webtoons"),
Pair("41", "Yaoi"),
Pair("42", "Yuri"),
)
// To be overridden if using tri-state genres
protected open fun getAdvancedGenreFilters(): List<AdvGenre> = emptyList()
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
fun toUriPart() = vals[state].first
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_USE_MIRROR
title = "Mirror"
entries = mirrorEntries
entryValues = mirrorEntries.map { "${URL_PREFIX}$it" }.toTypedArray()
setDefaultValue(entryValues[0])
summary = "%s"
setOnPreferenceChangeListener { _, newValue ->
// Update values
mirror = newValue as String
true
}
}.let(screen::addPreference)
}
companion object {
private const val PREF_USE_MIRROR = "pref_use_mirror"
private const val URL_PREFIX = "https://"
}
}

View File

@ -1,20 +0,0 @@
package eu.kanade.tachiyomi.multisrc.mangabox
class MangaBoxLinkedCdnSet : LinkedHashSet<String>() {
fun moveItemToFirst(item: String) {
// Lock the object to avoid multi threading issues
synchronized(this) {
if (this.contains(item) && this.first() != item) {
// Remove the item from the current set
this.remove(item)
// Create a new list with the item at the first position
val newItems = mutableListOf(item)
// Add the remaining items
newItems.addAll(this)
// Clear the current set and add all items from the new list
this.clear()
this.addAll(newItems)
}
}
}
}

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 4
baseVersionCode = 3
dependencies {
api(project(":lib:i18n"))

View File

@ -48,7 +48,7 @@ abstract class MangaEsp(
protected open val useApiSearch = false
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
override val client: OkHttpClient = network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()

View File

@ -2,7 +2,7 @@ plugins {
id("lib-multisrc")
}
baseVersionCode = 30
baseVersionCode = 29
dependencies {
api(project(":lib:randomua"))

View File

@ -17,6 +17,7 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.Cookie
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -30,7 +31,7 @@ import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.net.URLEncoder
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
@ -48,7 +49,6 @@ abstract class MangaHub(
private var baseApiUrl = "https://api.mghcdn.com"
private var baseCdnUrl = "https://imgx.mghcdn.com"
private val regex = Regex("mhub_access=([^;]+)")
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.setRandomUserAgent(
@ -91,6 +91,8 @@ abstract class MangaHub(
}
private fun refreshApiKey(chapter: SChapter) {
val now = Calendar.getInstance().time.time
val slug = "$baseUrl${chapter.url}"
.toHttpUrlOrNull()
?.pathSegments
@ -102,28 +104,32 @@ abstract class MangaHub(
baseUrl.toHttpUrl()
}
val oldKey = client.cookieJar
.loadForRequest(baseUrl.toHttpUrl())
.firstOrNull { it.name == "mhub_access" && it.value.isNotEmpty() }?.value
for (i in 1..2) {
// Clear key cookie
val cookie = Cookie.parse(url, "mhub_access=; Max-Age=0; Path=/")!!
client.cookieJar.saveFromResponse(url, listOf(cookie))
// We try requesting again with param if the first one fails
val query = if (i == 2) "?reloadKey=1" else ""
try {
val response = client.newCall(GET("$url$query", headers)).execute()
val returnedKey = response.headers["set-cookie"]?.let { regex.find(it)?.groupValues?.get(1) }
response.close() // Avoid potential resource leaks
if (returnedKey != oldKey) break; // Break out of loop since we got an allegedly valid API key
} catch (_: IOException) {
throw IOException("An error occurred while obtaining a new API key") // Show error
}
// Set required cookie (for cache busting?)
val recently = buildJsonObject {
putJsonObject((now - (0..3600).random()).toString()) {
put("mangaID", (1..42_000).random())
put("number", (1..20).random())
}
}.toString()
client.cookieJar.saveFromResponse(
url,
listOf(
Cookie.Builder()
.domain(url.host)
.name("recently")
.value(URLEncoder.encode(recently, "utf-8"))
.expiresAt(now + 2 * 60 * 60 * 24 * 31) // +2 months
.build(),
),
)
val request = GET("$url?reloadKey=1", headers)
client.newCall(request).execute()
}
data class SMangaDTO(

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.multisrc.mangathemesia
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -8,7 +9,6 @@ import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -16,6 +16,8 @@ import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.ref.SoftReference
import java.text.SimpleDateFormat
import java.util.Locale
@ -32,12 +34,14 @@ abstract class MangaThemesiaAlt(
protected open val listUrl = "$mangaUrlDirectory/list-mode/"
protected open val listSelector = "div#content div.soralist ul li a.series"
protected val preferences by getPreferencesLazy {
if (contains("__random_part_cache")) {
edit().remove("__random_part_cache").apply()
protected val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000).also {
if (it.contains("__random_part_cache")) {
it.edit().remove("__random_part_cache").apply()
}
if (it.contains("titles_without_random_part")) {
it.edit().remove("titles_without_random_part").apply()
}
if (contains("titles_without_random_part")) {
edit().remove("titles_without_random_part").apply()
}
}

View File

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

View File

@ -33,7 +33,7 @@ open class MCCMS(
private val json: Json by injectLazy()
override val client by lazy {
network.cloudflareClient.newBuilder()
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}

View File

@ -26,7 +26,7 @@ open class MCCMSWeb(
override val supportsLatest get() = true
override val client by lazy {
network.cloudflareClient.newBuilder()
network.client.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 2)
.build()
}

View File

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

View File

@ -26,7 +26,7 @@ abstract class MultiChan(
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
override val client: OkHttpClient = network.client.newBuilder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.rateLimit(2)

View File

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

View File

@ -40,7 +40,7 @@ abstract class Senkuro(
.add("Content-Type", "application/json")
override val client: OkHttpClient =
network.cloudflareClient.newBuilder()
network.client.newBuilder()
.rateLimit(3)
.build()

View File

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

View File

@ -34,7 +34,7 @@ abstract class SinMH(
protected open val mobileUrl = _baseUrl.replaceFirst("www.", "m.")
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder().rateLimit(2).build()
override val client = network.client.newBuilder().rateLimit(2).build()
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", System.getProperty("http.agent")!!)

View File

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

View File

@ -45,7 +45,7 @@ open class Webtoons(
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
override val client: OkHttpClient = super.client.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
@ -197,7 +197,7 @@ open class Webtoons(
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("'")
return picElement.attr("style").substringAfter("url(").substringBeforeLast(")")
.ifBlank { discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") }
}

View File

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

View File

@ -1,206 +0,0 @@
package eu.kanade.tachiyomi.multisrc.yuyu
import android.net.Uri
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.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import kotlinx.serialization.Serializable
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
import java.net.URLEncoder
abstract class YuYu(
override val name: String,
override val baseUrl: String,
override val lang: String,
) : ParsedHttpSource() {
override val client = network.cloudflareClient
override val supportsLatest = true
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
override fun popularMangaSelector() = ".top10-section .top10-item a"
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst("h3")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(element.absUrl("href"))
}
override fun popularMangaNextPageSelector() = null
// ============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("pagina", page.toString())
.build()
return GET(url, headers)
}
override fun latestUpdatesSelector() = ".manga-list .manga-card"
override fun latestUpdatesNextPageSelector() = "a.page-link:contains(>)"
override fun latestUpdatesFromElement(element: Element) = SManga.create().apply {
val url = element.selectFirst("a.manga-cover")!!.absUrl("href")
val uri = Uri.parse(url)
val pathSegments = uri.pathSegments
val lastSegment = URLEncoder.encode(pathSegments.last(), "UTF-8")
val encodedUrl = uri.buildUpon()
.path(pathSegments.dropLast(1).joinToString("/") + "/$lastSegment")
.toString()
title = element.selectFirst("a.manga-title")!!.text()
thumbnail_url = element.selectFirst("a.manga-cover img")?.absUrl("data-src")
setUrlWithoutDomain(encodedUrl)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map(::latestUpdatesFromElement)
return MangasPage(mangas, document.hasNextPage())
}
private fun Document.hasNextPage() =
selectFirst(latestUpdatesNextPageSelector())?.absUrl("href")?.let {
selectFirst("a.page-link.active")
?.absUrl("href")
.equals(it, ignoreCase = true).not()
} ?: false
// ============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = baseUrl.toHttpUrl().newBuilder()
.addQueryParameter("search", query)
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 client.newCall(GET("$baseUrl/manga/$slug", headers))
.asObservableSuccess()
.map {
val manga = mangaDetailsParse(it.asJsoup())
MangasPage(listOf(manga), false)
}
}
return super.fetchSearchManga(page, query, filters)
}
override fun searchMangaSelector() = ".search-result-item"
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
title = element.selectFirst(".search-result-title")!!.text()
thumbnail_url = element.selectFirst("img")?.absUrl("src")
setUrlWithoutDomain(
element.attr("onclick").let {
SEARCH_URL_REGEX.find(it)?.groups?.get(1)?.value!!
},
)
}
override fun searchMangaNextPageSelector() = null
// ============================== Manga Details =========================
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
val details = document.selectFirst(".manga-banner .container")!!
title = details.selectFirst("h1")!!.text()
thumbnail_url = details.selectFirst("img")?.absUrl("src")
genre = details.select(".genre-tag").joinToString { it.text() }
description = details.selectFirst(".sinopse p")?.text()
details.selectFirst(".manga-meta > div")?.ownText()?.let {
status = when (it.lowercase()) {
"em andamento" -> SManga.ONGOING
"completo" -> SManga.COMPLETED
"cancelado" -> SManga.CANCELLED
"hiato" -> SManga.ON_HIATUS
else -> SManga.UNKNOWN
}
}
setUrlWithoutDomain(document.location())
}
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)
?.let { MANGA_ID_REGEX.find(it)?.groups?.get(1)?.value }
?: throw Exception("Manga ID não encontrado")
}
// ============================== Chapters ===============================
override fun chapterListSelector() = "a.chapter-item"
override fun chapterFromElement(element: Element) = SChapter.create().apply {
name = element.selectFirst(".capitulo-numero")!!.ownText()
setUrlWithoutDomain(element.absUrl("href"))
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
val mangaId = manga.fetchMangaId()
val chapters = mutableListOf<SChapter>()
var page = 1
do {
val dto = fetchChapterListPage(mangaId, page++).parseAs<ChaptersDto>()
val document = Jsoup.parseBodyFragment(dto.chapters, baseUrl)
chapters += document.select(chapterListSelector()).map(::chapterFromElement)
} while (dto.hasNext())
return Observable.just(chapters)
}
private fun fetchChapterListPage(mangaId: String, page: Int): Response {
val url = "$baseUrl/ajax/lzmvke.php?order=DESC".toHttpUrl().newBuilder()
.addQueryParameter("manga_id", mangaId)
.addQueryParameter("page", page.toString())
.build()
return client
.newCall(GET(url, headers))
.execute()
}
// ============================== Pages ===============================
override fun pageListParse(document: Document): List<Page> {
return document.select("picture img").mapIndexed { idx, element ->
Page(idx, imageUrl = element.absUrl("src"))
}
}
override fun imageUrlParse(document: Document) = ""
// ============================== Utilities ===========================
@Serializable
class ChaptersDto(val chapters: String, private val remaining: Int) {
fun hasNext() = remaining > 0
}
companion object {
const val PREFIX_SEARCH = "id:"
val SEARCH_URL_REGEX = "'([^']+)".toRegex()
val MANGA_ID_REGEX = """obra_id:\s+(\d+)""".toRegex()
}
}

View File

@ -1,7 +1,7 @@
ext {
extName = 'Akuma'
extClass = '.AkumaFactory'
extVersionCode = 7
extVersionCode = 5
isNsfw = true
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.akuma
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -15,7 +16,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.FormBody
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
@ -25,6 +25,8 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.text.ParseException
import java.text.SimpleDateFormat
@ -46,12 +48,12 @@ class Akuma(
private var storedToken: String? = null
private val ddosGuardIntercept = DDosGuardInterceptor(network.cloudflareClient)
private val ddosGuardIntercept = DDosGuardInterceptor(network.client)
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
override val client: OkHttpClient = network.client.newBuilder()
.addInterceptor(ddosGuardIntercept)
.addInterceptor(::tokenInterceptor)
.rateLimit(2)
@ -110,7 +112,9 @@ class Akuma(
return storedToken!!
}
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val displayFullTitle: Boolean get() = preferences.getBoolean(PREF_TITLE, false)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Bato.to'
extClass = '.BatoToFactory'
extVersionCode = 49
extVersionCode = 48
isNsfw = true
}

View File

@ -20,7 +20,6 @@ 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.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@ -49,7 +48,10 @@ open class BatoTo(
private val siteLang: String,
) : ConfigurableSource, ParsedHttpSource() {
private val preferences by getPreferencesLazy { migrateMirrorPref() }
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.migrateMirrorPref()
}
override val name: String = "Bato.to"
override val baseUrl: String get() = mirror
@ -123,12 +125,14 @@ open class BatoTo(
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
}
private fun SharedPreferences.migrateMirrorPref() {
private fun SharedPreferences.migrateMirrorPref(): SharedPreferences {
val selectedMirror = getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)!!
if (selectedMirror in DEPRECATED_MIRRORS) {
edit().putString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE).commit()
}
return this
}
override val supportsLatest = true

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comic Fury'
extClass = '.ComicFuryFactory'
extVersionCode = 5
extVersionCode = 3
isNsfw = true
}

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.comicfury
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
@ -15,12 +16,13 @@ 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 okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
@ -33,7 +35,7 @@ class ComicFury(
override val name: String = "Comic Fury$extraName" // Used for No Text
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient.newBuilder().addInterceptor(TextInterceptor()).build()
override val client = super.client.newBuilder().addInterceptor(TextInterceptor()).build()
/**
* Archive is on a separate page from manga info
@ -203,7 +205,9 @@ class ComicFury(
private fun Boolean.toInt(): Int = if (this) { 0 } else { 1 }
// START OF AUTHOR NOTES //
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
}

View File

@ -4,7 +4,6 @@ ext {
themePkg = 'gigaviewer'
baseUrl = 'https://comic-growl.com'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

View File

@ -1,8 +1,5 @@
ignored_groups_title=Ignored Groups
ignored_groups_summary=Chapters from these groups won't be shown.\nOne group name per line (case-insensitive)
show_alternative_titles_title=Show Alternative Titles
show_alternative_titles_on=Adds alternative titles to the description
show_alternative_titles_off=Does not show alternative titles to the description
include_tags_title=Include Tags
include_tags_on=More specific, but might contain spoilers!
include_tags_off=Only the broader genres
@ -12,9 +9,6 @@ group_tags_off=List all tags together
update_cover_title=Update Covers
update_cover_on=Keep cover updated
update_cover_off=Prefer first cover
local_title_title=Translated Title
local_title_on=if available
local_title_off=Use the default title from the site
score_position_title=Score Position in the Description
score_position_top=Top
score_position_middle=Middle

View File

@ -1,8 +1,5 @@
ignored_groups_title=Grupos Ignorados
ignored_groups_summary=Capítulos desses grupos não aparecerão.\nUm grupo por linha
show_alternative_titles_title=Mostrar Títulos Alternativos
show_alternative_titles_on=Adiciona títulos alternativos à descrição
show_alternative_titles_off=Não mostra títulos alternativos na descrição
include_tags_title=Incluir Tags
include_tags_on=Mais detalhadas, mas podem conter spoilers
include_tags_off=Apenas os gêneros básicos
@ -12,9 +9,6 @@ group_tags_off=Listar todas as tags juntas
update_cover_title=Atualizar Capas
update_cover_on=Manter capas atualizadas
update_cover_off=Usar apenas a primeira capa
local_title_title=Título Traduzido
local_title_on=se disponível
local_title_off=Usar o título padrão do site
score_position_title=Posição da Nota na Descrição
score_position_top=Topo
score_position_middle=Meio

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.comickfun
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -16,16 +17,16 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Builder
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -64,7 +65,10 @@ abstract class Comick(
)
}
private val preferences by getPreferencesLazy { newLineIgnoredGroups() }
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
.newLineIgnoredGroups()
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
EditTextPreference(screen.context).apply {
@ -79,20 +83,6 @@ abstract class Comick(
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = SHOW_ALTERNATIVE_TITLES_PREF
title = intl["show_alternative_titles_title"]
summaryOn = intl["show_alternative_titles_on"]
summaryOff = intl["show_alternative_titles_off"]
setDefaultValue(SHOW_ALTERNATIVE_TITLES_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(SHOW_ALTERNATIVE_TITLES_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = INCLUDE_MU_TAGS_PREF
title = intl["include_tags_title"]
@ -135,20 +125,6 @@ abstract class Comick(
}
}.also(screen::addPreference)
SwitchPreferenceCompat(screen.context).apply {
key = LOCAL_TITLE_PREF
title = intl["local_title_title"]
summaryOff = intl["local_title_off"]
summaryOn = intl["local_title_on"]
setDefaultValue(LOCAL_TITLE_DEFAULT)
setOnPreferenceChangeListener { _, newValue ->
preferences.edit()
.putBoolean(LOCAL_TITLE_PREF, newValue as Boolean)
.commit()
}
}.also(screen::addPreference)
ListPreference(screen.context).apply {
key = SCORE_POSITION_PREF
title = intl["score_position_title"]
@ -184,9 +160,6 @@ abstract class Comick(
.orEmpty()
.toSet()
private val SharedPreferences.showAlternativeTitles: Boolean
get() = getBoolean(SHOW_ALTERNATIVE_TITLES_PREF, SHOW_ALTERNATIVE_TITLES_DEFAULT)
private val SharedPreferences.includeMuTags: Boolean
get() = getBoolean(INCLUDE_MU_TAGS_PREF, INCLUDE_MU_TAGS_DEFAULT)
@ -196,17 +169,6 @@ abstract class Comick(
private val SharedPreferences.updateCover: Boolean
get() = getBoolean(FIRST_COVER_PREF, FIRST_COVER_DEFAULT)
private val SharedPreferences.localTitle: String
get() = if (getBoolean(
LOCAL_TITLE_PREF,
LOCAL_TITLE_DEFAULT,
)
) {
comickLang.lowercase()
} else {
"all"
}
private val SharedPreferences.scorePosition: String
get() = getString(SCORE_POSITION_PREF, SCORE_POSITION_DEFAULT) ?: SCORE_POSITION_DEFAULT
@ -315,16 +277,6 @@ abstract class Comick(
return MangasPage(entries, end < searchResponse.size)
}
private fun addTagQueryParameters(builder: Builder, tags: String, parameterName: String) {
tags.split(",").forEach {
builder.addQueryParameter(
parameterName,
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
.replace("'-", "-and-039-").replace("'", "-and-039-"),
)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/v1.0/search".toHttpUrl().newBuilder().apply {
filters.forEach { it ->
@ -346,7 +298,7 @@ abstract class Comick(
}
is DemographicFilter -> {
it.state.filter { it.state }.forEach {
it.state.filter { it.isIncluded() }.forEach {
addQueryParameter("demographic", it.value)
}
}
@ -367,12 +319,6 @@ abstract class Comick(
}
}
is ContentRatingFilter -> {
if (it.state > 0) {
addQueryParameter("content_rating", it.getValue())
}
}
is CreatedAtFilter -> {
if (it.state > 0) {
addQueryParameter("time", it.getValue())
@ -399,14 +345,14 @@ abstract class Comick(
is TagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "tags")
it.state.split(",").forEach {
addQueryParameter(
"tags",
it.trim().lowercase().replace(SPACE_AND_SLASH_REGEX, "-")
.replace("'-", "-and-039-").replace("'", "-and-039-"),
)
}
}
is ExcludedTagFilter -> {
if (it.state.isNotEmpty()) {
addTagQueryParameters(this, it.state, "excluded-tags")
}
}
else -> {}
@ -459,18 +405,14 @@ abstract class Comick(
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
covers = localCovers.ifEmpty { originalCovers }.ifEmpty { firstVol },
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
)
}
return mangaData.toSManga(
includeMuTags = preferences.includeMuTags,
scorePosition = preferences.scorePosition,
showAlternativeTitles = preferences.showAlternativeTitles,
groupTags = preferences.groupTags,
titleLang = preferences.localTitle,
)
}
@ -565,9 +507,8 @@ abstract class Comick(
override fun getFilterList() = getFilters()
private fun SharedPreferences.newLineIgnoredGroups() {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return
private fun SharedPreferences.newLineIgnoredGroups(): SharedPreferences {
if (getBoolean(MIGRATED_IGNORED_GROUPS, false)) return this
val ignoredGroups = getString(IGNORED_GROUPS_PREF, "").orEmpty()
edit()
@ -581,14 +522,14 @@ abstract class Comick(
)
.putBoolean(MIGRATED_IGNORED_GROUPS, true)
.apply()
return this
}
companion object {
const val SLUG_SEARCH_PREFIX = "id:"
private val SPACE_AND_SLASH_REGEX = Regex("[ /]")
private const val IGNORED_GROUPS_PREF = "IgnoredGroups"
private const val SHOW_ALTERNATIVE_TITLES_PREF = "ShowAlternativeTitles"
const val SHOW_ALTERNATIVE_TITLES_DEFAULT = false
private const val INCLUDE_MU_TAGS_PREF = "IncludeMangaUpdatesTags"
const val INCLUDE_MU_TAGS_DEFAULT = false
private const val GROUP_TAGS_PREF = "GroupTags"
@ -598,8 +539,6 @@ abstract class Comick(
private const val FIRST_COVER_DEFAULT = true
private const val SCORE_POSITION_PREF = "ScorePosition"
const val SCORE_POSITION_DEFAULT = "top"
private const val LOCAL_TITLE_PREF = "LocalTitle"
private const val LOCAL_TITLE_DEFAULT = false
private const val LIMIT = 20
private const val CHAPTERS_LIMIT = 99999
}

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.extension.all.comickfun
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.GROUP_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.INCLUDE_MU_TAGS_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SCORE_POSITION_DEFAULT
import eu.kanade.tachiyomi.extension.all.comickfun.Comick.Companion.SHOW_ALTERNATIVE_TITLES_DEFAULT
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
@ -37,20 +36,13 @@ class Manga(
fun toSManga(
includeMuTags: Boolean = INCLUDE_MU_TAGS_DEFAULT,
scorePosition: String = SCORE_POSITION_DEFAULT,
showAlternativeTitles: Boolean = SHOW_ALTERNATIVE_TITLES_DEFAULT,
covers: List<MDcovers>? = null,
groupTags: Boolean = GROUP_TAGS_DEFAULT,
titleLang: String,
): SManga {
val entryTitle = comic.altTitles.firstOrNull {
titleLang != "all" && !it.lang.isNullOrBlank() && titleLang.startsWith(it.lang)
}?.title ?: comic.title
val titles = listOf(Title(title = comic.title)) + comic.altTitles
return SManga.create().apply {
) =
SManga.create().apply {
// appennding # at end as part of migration from slug to hid
url = "/comic/${comic.hid}#"
title = entryTitle
title = comic.title
description = buildString {
if (scorePosition == "top") append(comic.fancyScore)
val desc = comic.desc?.beautifyDescription()
@ -62,12 +54,11 @@ class Manga(
if (this.isNotEmpty()) append("\n\n")
append(comic.fancyScore)
}
if (showAlternativeTitles && comic.altTitles.isNotEmpty()) {
if (comic.altTitles.isNotEmpty()) {
if (this.isNotEmpty()) append("\n\n")
append("Alternative Titles:\n")
append(
titles.distinctBy { it.title }.filter { it.title != entryTitle }
.mapNotNull { title ->
comic.altTitles.mapNotNull { title ->
title.title?.let { "$it" }
}.joinToString("\n"),
)
@ -105,7 +96,6 @@ class Manga(
.joinToString { if (groupTags) "${it.group}:${it.name?.trim()}" else "${it.name?.trim()}" }
}
}
}
@Serializable
class Comic(
@ -179,7 +169,6 @@ class MDcovers(
@Serializable
class Title(
val title: String?,
val lang: String? = null,
)
@Serializable

View File

@ -11,7 +11,6 @@ fun getFilters(): FilterList {
TypeFilter("Type", getTypeList),
SortFilter("Sort", getSortsList),
StatusFilter("Status", getStatusList),
ContentRatingFilter("Content Rating", getContentRatingList),
CompletedFilter("Completely Scanlated?"),
CreatedAtFilter("Created at", getCreatedAtList),
MinimumFilter("Minimum Chapters"),
@ -21,7 +20,6 @@ fun getFilters(): FilterList {
ToYearFilter("To"),
Filter.Header("Separate tags with commas"),
TagFilter("Tags"),
ExcludedTagFilter("Excluded Tags"),
)
}
@ -31,10 +29,8 @@ internal class GenreFilter(name: String, genreList: List<Pair<String, String>>)
internal class TagFilter(name: String) : TextFilter(name)
internal class ExcludedTagFilter(name: String) : TextFilter(name)
internal class DemographicFilter(name: String, demographicList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, demographicList.map { CheckBoxFilter(it.first, it.second) })
Filter.Group<TriFilter>(name, demographicList.map { TriFilter(it.first, it.second) })
internal class TypeFilter(name: String, typeList: List<Pair<String, String>>) :
Filter.Group<CheckBoxFilter>(name, typeList.map { CheckBoxFilter(it.first, it.second) })
@ -56,9 +52,6 @@ internal class SortFilter(name: String, sortList: List<Pair<String, String>>, st
internal class StatusFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
internal class ContentRatingFilter(name: String, statusList: List<Pair<String, String>>, state: Int = 0) :
SelectFilter(name, statusList, state)
/** Generics **/
internal open class TriFilter(name: String, val value: String) : Filter.TriState(name)
@ -163,14 +156,12 @@ private val getDemographicList: List<Pair<String, String>> = listOf(
Pair("Shoujo", "2"),
Pair("Seinen", "3"),
Pair("Josei", "4"),
Pair("None", "5"),
)
private val getTypeList: List<Pair<String, String>> = listOf(
Pair("Manga", "jp"),
Pair("Manhwa", "kr"),
Pair("Manhua", "cn"),
Pair("Others", "others"),
)
private val getCreatedAtList: List<Pair<String, String>> = listOf(
@ -199,10 +190,3 @@ private val getStatusList: List<Pair<String, String>> = listOf(
Pair("Cancelled", "3"),
Pair("Hiatus", "4"),
)
private val getContentRatingList: List<Pair<String, String>> = listOf(
Pair("All", ""),
Pair("Safe", "safe"),
Pair("Suggestive", "suggestive"),
Pair("Erotica", "erotica"),
)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Comico'
extClass = '.ComicoFactory'
extVersionCode = 7
extVersionCode = 6
isNsfw = true
}

View File

@ -62,7 +62,7 @@ open class Comico(
this["Origin"] = baseUrl
}.build()
override val client = network.cloudflareClient.newBuilder()
override val client = network.client.newBuilder()
.cookieJar(
object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) =

View File

@ -1,7 +1,7 @@
ext {
extName = 'ComicsKingdom'
extClass = '.ComicsKingdomFactory'
extVersionCode = 2
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.comicskingdom
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
@ -11,7 +12,6 @@ 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 keiyoushi.utils.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
@ -19,6 +19,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
@ -299,7 +301,9 @@ class ComicsKingdom(override val lang: String) : ConfigurableSource, HttpSource(
screen.addPreference(compactpref)
}
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun shouldCompact() = preferences.getBoolean("compactPref", true)

View File

@ -1,7 +1,7 @@
ext {
extName = "Comikey"
extClass = ".ComikeyFactory"
extVersionCode = 3
extVersionCode = 2
}
apply from: "$rootDir/common.gradle"

View File

@ -24,7 +24,6 @@ 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.getPreferencesLazy
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl
@ -76,7 +75,9 @@ open class Comikey(
classLoader = this::class.java.classLoader!!,
)
private val preferences by getPreferencesLazy()
private val preferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun popularMangaRequest(page: Int) = GET("$baseUrl/comics/?order=-views&page=$page", headers)

View File

@ -1,7 +1,7 @@
ext {
extName = 'Danbooru'
extClass = '.Danbooru'
extVersionCode = 3
extVersionCode = 2
isNsfw = true
}

View File

@ -1,46 +1,51 @@
package eu.kanade.tachiyomi.extension.all.danbooru
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
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.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
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 uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
class Danbooru : HttpSource(), ConfigurableSource {
class Danbooru : ParsedHttpSource() {
override val name: String = "Danbooru"
override val baseUrl: String = "https://danbooru.donmai.us"
override val lang: String = "all"
override val supportsLatest: Boolean = true
override val client = network.cloudflareClient
override val client: OkHttpClient = network.cloudflareClient
private val json: Json by injectLazy()
private val dateFormat =
private val dateFormat: SimpleDateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH)
private val preference by getPreferencesLazy()
}
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList())
override fun popularMangaParse(response: Response) =
searchMangaParse(response)
override fun popularMangaFromElement(element: Element): SManga =
searchMangaFromElement(element)
override fun popularMangaNextPageSelector(): String =
searchMangaNextPageSelector()
override fun popularMangaSelector(): String =
searchMangaSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/pools/gallery".toHttpUrl().newBuilder()
@ -82,100 +87,86 @@ class Danbooru : HttpSource(), ConfigurableSource {
return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
override fun searchMangaSelector(): String =
"article.post-preview"
val entries = document.select("article.post-preview").map {
searchMangaFromElement(it)
}
val hasNextPage = document.selectFirst("a.paginator-next") != null
return MangasPage(entries, hasNextPage)
}
private fun searchMangaFromElement(element: Element) = SManga.create().apply {
url = element.selectFirst(".post-preview-link")!!.attr("href")
title = element.selectFirst("div.text-center")!!.text()
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
url = element.selectFirst(".post-preview-link")?.attr("href")!!
title = element.selectFirst("div.text-center")?.text() ?: ""
thumbnail_url = element.selectFirst("source")?.attr("srcset")
?.substringAfterLast(',')?.trim()
?.substringBeforeLast(' ')?.trimStart()
}
override fun searchMangaNextPageSelector(): String =
"a.paginator-next"
override fun latestUpdatesRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(FilterOrder("created_at")))
override fun latestUpdatesParse(response: Response): MangasPage =
searchMangaParse(response)
override fun latestUpdatesSelector(): String =
searchMangaSelector()
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
val document = response.asJsoup()
override fun latestUpdatesFromElement(element: Element): SManga =
searchMangaFromElement(element)
override fun latestUpdatesNextPageSelector(): String =
searchMangaNextPageSelector()
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
setUrlWithoutDomain(document.location())
title = document.selectFirst(".pool-category-series, .pool-category-collection")!!.text()
description = document.getElementById("description")?.wholeText()
author = document.selectFirst("#description a[href*=artists]")?.ownText()
artist = author
update_strategy = if (!preference.splitChaptersPref) {
UpdateStrategy.ONLY_FETCH_ONCE
} else {
UpdateStrategy.ALWAYS_UPDATE
}
title = document.selectFirst(".pool-category-series, .pool-category-collection")?.text() ?: ""
description = document.getElementById("description")?.wholeText() ?: ""
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
}
override fun chapterListRequest(manga: SManga): Request =
GET("$baseUrl${manga.url}.json", headers)
GET("$baseUrl${manga.url}.json?only=id,created_at", headers)
override fun chapterListParse(response: Response): List<SChapter> {
val data = response.parseAs<Pool>()
override fun chapterListParse(response: Response): List<SChapter> = listOf(
SChapter.create().apply {
val data = json.decodeFromString<JsonObject>(response.body.string())
return if (preference.splitChaptersPref) {
data.postIds.mapIndexed { index, id ->
SChapter.create().apply {
url = "/posts/$id"
name = "Post ${index + 1}"
chapter_number = index + 1f
}
}.reversed().apply {
if (isNotEmpty()) {
this[0].date_upload = dateFormat.tryParse(data.updatedAt)
}
}
} else {
listOf(
SChapter.create().apply {
url = "/pools/${data.id}"
val id = data["id"]!!.jsonPrimitive.content
val createdAt = data["created_at"]?.jsonPrimitive?.content
url = "/pools/$id"
name = "Oneshot"
date_upload = dateFormat.tryParse(data.updatedAt)
date_upload = createdAt?.let(::parseTimestamp) ?: 0
chapter_number = 0F
},
)
}
}
override fun chapterListSelector(): String =
throw IllegalStateException("Not used")
override fun chapterFromElement(element: Element): SChapter =
throw IllegalStateException("Not used")
override fun pageListRequest(chapter: SChapter): Request =
GET("$baseUrl${chapter.url}.json", headers)
GET("$baseUrl${chapter.url}.json?only=post_ids", headers)
override fun pageListParse(response: Response): List<Page> =
if (response.request.url.toString().contains("/posts/")) {
val data = response.parseAs<Post>()
json.decodeFromString<JsonObject>(response.body.string())
.get("post_ids")?.jsonArray
?.map { it.jsonPrimitive.content }
?.mapIndexed { i, id -> Page(index = i, url = "/posts/$id") }
?: emptyList()
listOf(
Page(index = 0, imageUrl = data.fileUrl),
)
} else {
val data = response.parseAs<Pool>()
data.postIds.mapIndexed { index, id ->
Page(index, url = "/posts/$id")
}
}
override fun pageListParse(document: Document): List<Page> =
throw IllegalStateException("Not used")
override fun imageUrlRequest(page: Page): Request =
GET("$baseUrl${page.url}.json", headers)
GET("$baseUrl${page.url}.json?only=file_url", headers)
override fun imageUrlParse(response: Response): String =
response.parseAs<Post>().fileUrl
json.decodeFromString<JsonObject>(response.body.string())
.get("file_url")!!.jsonPrimitive.content
override fun imageUrlParse(document: Document): String =
throw IllegalStateException("Not used")
override fun getChapterUrl(chapter: SChapter): String =
baseUrl + chapter.url
@ -190,20 +181,6 @@ class Danbooru : HttpSource(), ConfigurableSource {
),
)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
SwitchPreferenceCompat(screen.context).apply {
key = CHAPTER_LIST_PREF
title = "Split posts into individual chapters"
summary = """
Instead of showing one 'OneShot' chapter,
each post will be it's own chapter
""".trimIndent()
setDefaultValue(false)
}.also(screen::addPreference)
private fun parseTimestamp(string: String): Long? =
runCatching { dateFormat.parse(string)?.time!! }.getOrNull()
}
private val SharedPreferences.splitChaptersPref: Boolean
get() = getBoolean(CHAPTER_LIST_PREF, false)
}
private const val CHAPTER_LIST_PREF = "prefChapterList"

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.extension.all.danbooru
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
class Pool(
val id: Int,
@SerialName("updated_at") val updatedAt: String,
@SerialName("post_ids") val postIds: List<Int>,
)
@Serializable
class Post(
@SerialName("file_url") val fileUrl: String,
)

View File

@ -1,5 +1,4 @@
package eu.kanade.tachiyomi.extension.all.danbooru
import eu.kanade.tachiyomi.source.model.Filter
internal class FilterTags : Filter.Text("Tags")

View File

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

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.extension.all.deviantart
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
@ -12,7 +13,6 @@ 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 okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
@ -20,6 +20,8 @@ import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.parser.Parser
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@ -30,7 +32,9 @@ class DeviantArt : HttpSource(), ConfigurableSource {
override val lang = "all"
override val supportsLatest = false
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")

View File

@ -1,7 +1,7 @@
ext {
extName = 'E-Hentai'
extClass = '.EHFactory'
extVersionCode = 25
extVersionCode = 24
isNsfw = true
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import android.webkit.CookieManager
@ -22,7 +23,6 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.getPreferencesLazy
import okhttp3.CacheControl
import okhttp3.CookieJar
import okhttp3.Headers
@ -30,6 +30,8 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URLEncoder
abstract class EHentai(
@ -39,7 +41,9 @@ abstract class EHentai(
override val name = "E-Hentai"
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
private val memberId: String by lazy { getMemberIdPref() }

View File

@ -1,7 +1,7 @@
ext {
extName = 'Galaxy'
extClass = '.GalaxyFactory'
extVersionCode = 5
extVersionCode = 4
isNsfw = false
}

View File

@ -1,11 +1,13 @@
package eu.kanade.tachiyomi.extension.all.galaxy
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.SourceFactory
import keiyoushi.utils.getPreferencesLazy
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class GalaxyFactory : SourceFactory {
@ -20,7 +22,9 @@ class GalaxyFactory : SourceFactory {
override val baseUrl by lazy { getPrefBaseUrl() }
private val preferences: SharedPreferences by getPreferencesLazy()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
companion object {
private const val RESTART_APP = ".لتطبيق الإعدادات الجديدة أعد تشغيل التطبيق"

View File

@ -4,7 +4,6 @@ ext {
themePkg = 'madara'
baseUrl = 'https://grabber.zone'
overrideVersionCode = 0
isNsfw = false
}
apply from: "$rootDir/common.gradle"

View File

@ -1,7 +1,7 @@
ext {
extName = 'Hitomi'
extClass = '.HitomiFactory'
extVersionCode = 38
extVersionCode = 36
isNsfw = true
}

View File

@ -1,8 +1,13 @@
package eu.kanade.tachiyomi.extension.all.hitomi
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
@ -10,28 +15,34 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.source.online.HttpSource
import keiyoushi.utils.parseAs
import keiyoushi.utils.tryParse
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okhttp3.internal.http2.StreamResetException
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.MessageDigest
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.LinkedList
import java.util.Locale
import kotlin.math.max
import kotlin.math.min
import kotlin.time.Duration.Companion.seconds
@ -39,25 +50,35 @@ import kotlin.time.Duration.Companion.seconds
class Hitomi(
override val lang: String,
private val nozomiLang: String,
) : HttpSource() {
) : HttpSource(), ConfigurableSource {
override val name = "Hitomi"
private val cdnDomain = "gold-usergeneratedcontent.net"
private val domain = "hitomi.la"
override val baseUrl = "https://hitomi.la"
override val baseUrl = "https://$domain"
private val ltnUrl = "https://ltn.$cdnDomain"
private val ltnUrl = "https://ltn.$domain"
override val supportsLatest = true
private val json: Json by injectLazy()
private val REGEX_IMAGE_URL = """https://.*?a\.$domain/(jxl|avif|webp)/\d+?/\d+/([0-9a-f]{64})\.\1""".toRegex()
override val client = network.cloudflareClient.newBuilder()
.addInterceptor(::jxlContentTypeInterceptor)
.addInterceptor(::updateImageUrlInterceptor)
.apply {
interceptors().add(0, ::streamResetRetry)
}
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
private fun imageType() = preferences.getString(PREF_IMAGETYPE, "webp")!!
override fun headersBuilder() = super.headersBuilder()
.set("referer", "$baseUrl/")
.set("origin", baseUrl)
@ -502,7 +523,7 @@ class Hitomi(
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
"https://${subDomain}tn.$cdnDomain/avifbigtn/${thumbPathFromHash(hash)}/$hash.avif"
"https://${subDomain}tn.$domain/webpbigtn/${thumbPathFromHash(hash)}/$hash.webp"
}
description = buildString {
japaneseTitle?.let {
@ -547,7 +568,11 @@ class Hitomi(
name = "Chapter"
url = gallery.galleryurl
scanlator = gallery.type
date_upload = dateFormat.tryParse(gallery.date.substringBeforeLast("-"))
date_upload = try {
dateFormat.parse(gallery.date.substringBeforeLast("-"))!!.time
} catch (_: ParseException) {
0L
}
},
)
}
@ -564,18 +589,28 @@ class Hitomi(
return GET("$ltnUrl/galleries/$id.js", headers)
}
override fun pageListParse(response: Response): List<Page> {
override fun pageListParse(response: Response) = runBlocking {
val gallery = response.parseScriptAs<Gallery>()
val id = gallery.galleryurl
.substringAfterLast("-")
.substringBefore(".")
return gallery.files.mapIndexed { idx, img ->
// actual logic in updateImageUrlInterceptor
val imageUrl = "http://127.0.0.1".toHttpUrl().newBuilder()
.fragment(img.hash)
.build()
.toString()
gallery.files.mapIndexed { idx, img ->
val hash = img.hash
val typePref = imageType()
val avif = img.hasavif == 1 && typePref == "avif"
val jxl = img.hasjxl == 1 && typePref == "jxl"
val commonId = commonImageId()
val imageId = imageIdFromHash(hash)
val subDomain = 'a' + subdomainOffset(imageId)
val imageUrl = when {
jxl -> "https://${subDomain}a.$domain/jxl/$commonId$imageId/$hash.jxl"
avif -> "https://${subDomain}a.$domain/avif/$commonId$imageId/$hash.avif"
else -> "https://${subDomain}a.$domain/webp/$commonId$imageId/$hash.webp"
}
Page(
idx,
@ -601,7 +636,7 @@ class Hitomi(
val body = use { it.body.string() }
val transformed = transform(body)
return transformed.parseAs()
return json.decodeFromString(transformed)
}
private suspend fun Call.awaitSuccess() =
@ -663,6 +698,45 @@ class Hitomi(
return hash.replace(Regex("""^.*(..)(.)$"""), "$2/$1")
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
ListPreference(screen.context).apply {
key = PREF_IMAGETYPE
title = "Images Type"
entries = arrayOf("webp", "avif", "jxl")
entryValues = arrayOf("webp", "avif", "jxl")
summary = "Clear chapter cache to apply changes"
setDefaultValue("webp")
}.also(screen::addPreference)
}
private fun List<Int>.toBytesList(): ByteArray = this.map { it.toByte() }.toByteArray()
private val signatureOne = listOf(0xFF, 0x0A).toBytesList()
private val signatureTwo = listOf(0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20, 0x0D, 0x0A, 0x87, 0x0A).toBytesList()
fun ByteArray.startsWith(byteArray: ByteArray): Boolean {
if (this.size < byteArray.size) return false
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
}
private fun jxlContentTypeInterceptor(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
if (response.headers["Content-Type"] != "application/octet-stream") {
return response
}
val bytesPeek = max(signatureOne.size, signatureTwo.size).toLong()
val bytesArray = response.peekBody(bytesPeek).bytes()
if (!(bytesArray.startsWith(signatureOne) || bytesArray.startsWith(signatureTwo))) {
return response
}
val type = "image/jxl"
val body = response.body.bytes().toResponseBody(type.toMediaType())
return response.newBuilder()
.body(body)
.header("Content-Type", type)
.build()
}
private fun streamResetRetry(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
@ -679,24 +753,23 @@ class Hitomi(
private fun updateImageUrlInterceptor(chain: Interceptor.Chain): Response {
val request = chain.request()
if (request.url.host != "127.0.0.1") {
return chain.proceed(request)
}
val hash = request.url.fragment!!
val cleanUrl = request.url.run { "$scheme://$host$encodedPath" }
REGEX_IMAGE_URL.matchEntire(cleanUrl)?.let { match ->
val (ext, hash) = match.destructured
val commonId = runBlocking { commonImageId() }
val imageId = imageIdFromHash(hash)
val subDomain = runBlocking { (subdomainOffset(imageId) + 1) }
val imageUrl = "https://a$subDomain.$cdnDomain/$commonId$imageId/$hash.avif"
val newRequest = request.newBuilder()
.url(imageUrl)
.build()
val subDomain = 'a' + runBlocking { subdomainOffset(imageId) }
val newUrl = "https://${subDomain}a.$domain/$ext/$commonId$imageId/$hash.$ext"
val newRequest = request.newBuilder().url(newUrl).build()
return chain.proceed(newRequest)
}
return chain.proceed(request)
}
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
@ -704,4 +777,8 @@ class Hitomi(
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw UnsupportedOperationException()
override fun searchMangaParse(response: Response) = throw UnsupportedOperationException()
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
companion object {
const val PREF_IMAGETYPE = "pref_image_type"
}
}

View File

@ -22,6 +22,9 @@ class Gallery(
@Serializable
class ImageFile(
val hash: String,
val haswebp: Int?,
val hasavif: Int?,
val hasjxl: Int?,
)
@Serializable

View File

@ -4,7 +4,6 @@ ext {
themePkg = 'pizzareader'
baseUrl = 'https://hni-scantrad.net'
overrideVersionCode = 5
isNsfw = false
}
apply from: "$rootDir/common.gradle"

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