diff --git a/src/ru/hentailib/AndroidManifest.xml b/lib-multisrc/libgroup/AndroidManifest.xml
similarity index 83%
rename from src/ru/hentailib/AndroidManifest.xml
rename to lib-multisrc/libgroup/AndroidManifest.xml
index 29cb39094..24b61b1aa 100644
--- a/src/ru/hentailib/AndroidManifest.xml
+++ b/lib-multisrc/libgroup/AndroidManifest.xml
@@ -15,9 +15,9 @@
+ android:host="${SOURCEHOST}"
+ android:pathPattern="/ru/manga/..*"
+ android:scheme="${SOURCESCHEME}" />
diff --git a/lib-multisrc/libgroup/build.gradle.kts b/lib-multisrc/libgroup/build.gradle.kts
index f24e51157..a8eb4dc7d 100644
--- a/lib-multisrc/libgroup/build.gradle.kts
+++ b/lib-multisrc/libgroup/build.gradle.kts
@@ -2,4 +2,4 @@ plugins {
id("lib-multisrc")
}
-baseVersionCode = 25
+baseVersionCode = 26
diff --git a/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroup.kt b/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroup.kt
index d5602412b..cf8ff660f 100644
--- a/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroup.kt
+++ b/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroup.kt
@@ -1,12 +1,16 @@
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.webkit.WebView
+import android.webkit.WebViewClient
import android.widget.Toast
import androidx.preference.ListPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
-import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservable
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@@ -18,36 +22,27 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
-import eu.kanade.tachiyomi.util.asJsoup
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonArray
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.contentOrNull
-import kotlinx.serialization.json.int
-import kotlinx.serialization.json.intOrNull
-import kotlinx.serialization.json.jsonArray
-import kotlinx.serialization.json.jsonObject
-import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
-import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
-import okhttp3.ResponseBody.Companion.toResponseBody
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-import uy.kohesive.injekt.injectLazy
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Locale
+import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
-import kotlin.math.absoluteValue
-import kotlin.random.Random
abstract class LibGroup(
override val name: String,
@@ -55,247 +50,220 @@ abstract class LibGroup(
final override val lang: String,
) : ConfigurableSource, HttpSource() {
- private val json: Json by injectLazy()
+ private val json: Json = Json {
+ ignoreUnknownKeys = true
+ explicitNulls = false
+ encodeDefaults = true
+ }
private val preferences: SharedPreferences by lazy {
- Injekt.get().getSharedPreferences("source_${id}_2", 0x0000)
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
+ .migrateOldImageServer()
}
override val supportsLatest = true
- private fun imageContentTypeIntercept(chain: Interceptor.Chain): Response {
- val originalRequest = chain.request()
- val response = chain.proceed(originalRequest)
- val urlRequest = originalRequest.url.toString()
- val possibleType = urlRequest.substringAfterLast("/").substringBefore("?").split(".")
- return if (urlRequest.contains("/chapters/") and (possibleType.size == 2)) {
- val realType = possibleType[1]
- val image = response.body.byteString().toResponseBody("image/$realType".toMediaType())
- response.newBuilder().body(image).build()
- } else {
- response
- }
- }
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimit(3)
.connectTimeout(5, TimeUnit.MINUTES)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
- .addNetworkInterceptor { imageContentTypeIntercept(it) }
+ .addInterceptor(::checkForToken)
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.code == 419) {
throw IOException("HTTP error ${response.code}. Проверьте сайт. Для завершения авторизации необходимо перезапустить приложение с полной остановкой.")
}
if (response.code == 404) {
- throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E︎ и обновите список глав.")
+ throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E︎ и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
}
return@addInterceptor response
}
.build()
- private val userAgentMobile = "Mozilla/5.0 (Linux; Android 10; SM-G980F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
+ private val userAgentMobile = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.3"
- private val userAgentRandomizer = "${Random.nextInt().absoluteValue}"
+ private var bearerToken: String? = null
- protected var csrfToken: String = ""
+ abstract val siteId: Int // Important in api calls
+
+ private val apiDomain: String = "lib.social"
override fun headersBuilder() = Headers.Builder().apply {
// User-Agent required for authorization through third-party accounts (mobile version for correct display in WebView)
add("User-Agent", userAgentMobile)
- add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
+ add("Accept", "text/html,application/json,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
add("Referer", baseUrl)
+ add("Site-Id", siteId.toString())
}
- private fun imgHeader() = Headers.Builder().apply {
- add("User-Agent", userAgentMobile)
- add("Accept", "image/avif,image/webp,*/*")
- add("Referer", baseUrl)
- }.build()
-
- protected fun catalogHeaders() = Headers.Builder()
- .apply {
- add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36 Edg/100.0.$userAgentRandomizer")
- add("Accept", "application/json, text/plain, */*")
- add("X-Requested-With", "XMLHttpRequest")
- add("x-csrf-token", csrfToken)
+ private var _constants: Constants? = null
+ private fun getConstants(): Constants {
+ if (_constants == null) {
+ try {
+ _constants = client.newCall(
+ GET("https://api.$apiDomain/api/constants?fields[]=genres&fields[]=tags&fields[]=types&fields[]=scanlateStatus&fields[]=status&fields[]=format&fields[]=ageRestriction&fields[]=imageServers", headers),
+ ).execute().parseAs>().data
+ return _constants!!
+ } catch (ex: SerializationException) {
+ throw Exception("Ошибка сериализации. Проверьте сайт.")
+ }
}
- .build()
+ return _constants!!
+ }
+
+ private fun checkForToken(chain: Interceptor.Chain): Response {
+ val req = chain.request().newBuilder()
+ val url = chain.request().url.toString()
+ if (url.contains("api.$apiDomain") && !url.contains("/api/auth/me")) {
+ if (bearerToken.isNullOrBlank()) {
+ bearerToken = loadToken()
+ }
+ if (bearerToken != "none") {
+ req.apply {
+ addHeader("Authorization", bearerToken.orEmpty())
+ }
+ }
+ }
+ return chain.proceed(req.build())
+ }
+
+ @SuppressLint("ApplySharedPref")
+ private fun loadToken(): String {
+ try {
+ var token = preferences.getString(TOKEN_STORE, "")!!.parseAs()
+ if (token.isExpired() || !isUserTokenValid(token.getToken())) {
+ val refreshedToken: AuthToken? = refreshToken()
+ if (refreshedToken != null) {
+ val str = json.encodeToString(refreshedToken)
+ preferences.edit().putString(TOKEN_STORE, str).commit()
+ token = refreshedToken
+ }
+ }
+ return token.getToken()
+ } catch (ex: SerializationException) {
+ val refreshedToken: AuthToken? = refreshToken()
+ if (refreshedToken != null) {
+ val str = json.encodeToString(refreshedToken)
+ preferences.edit().putString(TOKEN_STORE, str).commit()
+ return refreshedToken.getToken()
+ }
+ }
+ return "none"
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ @Suppress("NAME_SHADOWING")
+ private fun refreshToken(): AuthToken? {
+ val latch = CountDownLatch(1)
+ var returnValue: AuthToken? = null
+ Handler(Looper.getMainLooper()).post {
+ val webView = WebView(Injekt.get())
+ with(webView.settings) {
+ javaScriptEnabled = true
+ domStorageEnabled = true
+ databaseEnabled = true
+ }
+ webView.webViewClient = object : WebViewClient() {
+ override fun onPageFinished(view: WebView?, url: String?) {
+ val view = view!!
+ val script = "javascript:localStorage['auth']"
+ view.evaluateJavascript(script) {
+ view.stopLoading()
+ view.destroy()
+ if (!it.isNullOrBlank() && it != "null") {
+ val str: String = if (it.first() == '"' && it.last() == '"') {
+ it.substringAfter("\"").substringBeforeLast("\"")
+ .replace("\\", "")
+ } else {
+ it.replace("\\", "")
+ }
+ returnValue = str.parseAs()
+ }
+ latch.countDown()
+ }
+ }
+ }
+ webView.loadUrl(baseUrl)
+ }
+ latch.await(20, TimeUnit.SECONDS)
+
+ return returnValue
+ }
+
+ private fun isUserTokenValid(token: String): Boolean {
+ val headers = Headers.Builder().apply {
+ add("Accept", "application/json")
+ add("Authorization", token)
+ }.build()
+ client.newCall(GET("https://api.$apiDomain/api/auth/me", headers)).execute().also { response ->
+ return when (response.code) {
+ 401 -> throw Exception("Попробуйте авторизоваться через WebView\uD83C\uDF0E\uFE0E. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
+ else -> true
+ }
+ }
+ }
+
+ override fun getMangaUrl(manga: SManga): String {
+ return "$baseUrl/ru/manga${manga.url}"
+ }
// Latest
- override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException() // popularMangaRequest()
- override fun fetchLatestUpdates(page: Int): Observable {
- if (csrfToken.isEmpty()) {
- return client.newCall(popularMangaRequest(page))
- .asObservableSuccess()
- .flatMap { response ->
- // Obtain token
- val resBody = response.body.string()
- csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
- return@flatMap fetchLatestMangaFromApi(page)
- }
- }
- return fetchLatestMangaFromApi(page)
+ override fun latestUpdatesRequest(page: Int): Request {
+ val url = "https://api.$apiDomain/api/latest-updates".toHttpUrl().newBuilder()
+ .addQueryParameter("page", page.toString())
+ return GET(url.build(), headers)
}
- private fun fetchLatestMangaFromApi(page: Int): Observable {
- return client.newCall(POST("$baseUrl/filterlist?dir=desc&sort=last_chapter_at&page=$page&chapters[min]=1", catalogHeaders()))
- .asObservableSuccess()
- .map { response ->
- latestUpdatesParse(response)
- }
- }
-
- override fun latestUpdatesParse(response: Response) =
- popularMangaParse(response)
+ override fun latestUpdatesParse(response: Response): MangasPage = popularMangaParse(response)
// Popular
- override fun popularMangaRequest(page: Int) = GET(baseUrl, headers)
- override fun fetchPopularManga(page: Int): Observable {
- if (csrfToken.isEmpty()) {
- return client.newCall(popularMangaRequest(page))
- .asObservableSuccess()
- .flatMap { response ->
- // Obtain token
- val resBody = response.body.string()
- csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
- return@flatMap fetchPopularMangaFromApi(page)
- }
- }
- return fetchPopularMangaFromApi(page)
- }
-
- private fun fetchPopularMangaFromApi(page: Int): Observable {
- return client.newCall(POST("$baseUrl/filterlist?dir=desc&sort=views&page=$page&chapters[min]=1", catalogHeaders()))
- .asObservableSuccess()
- .map { response ->
- popularMangaParse(response)
- }
+ override fun popularMangaRequest(page: Int): Request {
+ val url = "https://api.$apiDomain/api/manga".toHttpUrl().newBuilder()
+ .addQueryParameter("site_id[]", siteId.toString())
+ .addQueryParameter("page", page.toString())
+ return GET(url.build(), headers)
}
override fun popularMangaParse(response: Response): MangasPage {
- val resBody = response.body.string()
- val result = json.decodeFromString(resBody)
- val items = result["items"]!!.jsonObject
- val popularMangas = items["data"]?.jsonArray?.map { popularMangaFromElement(it) }
- if (popularMangas != null) {
- val hasNextPage = items["next_page_url"]?.jsonPrimitive?.contentOrNull != null
- return MangasPage(popularMangas, hasNextPage)
+ val data = response.parseAs()
+ val popularMangas = data.mapToSManga(isEng())
+ if (popularMangas.isNotEmpty()) {
+ return MangasPage(popularMangas, data.meta.hasNextPage)
}
return MangasPage(emptyList(), false)
}
- // Popular cross Latest
- private fun popularMangaFromElement(el: JsonElement) = SManga.create().apply {
- val slug = el.jsonObject["slug"]!!.jsonPrimitive.content
- title = when {
- isEng.equals("rus") && el.jsonObject["rus_name"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() -> el.jsonObject["rus_name"]!!.jsonPrimitive.content
- isEng.equals("eng") && el.jsonObject["eng_name"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() -> el.jsonObject["eng_name"]!!.jsonPrimitive.content
- else -> el.jsonObject["name"]!!.jsonPrimitive.content
- }
- thumbnail_url = if (el.jsonObject["coverImage"] != null) {
- el.jsonObject["coverImage"]!!.jsonPrimitive.content
- } else {
- "/uploads/cover/" + slug + "/cover/" + el.jsonObject["cover"]!!.jsonPrimitive.content + "_250x350.jpg"
- }
- if (!thumbnail_url!!.contains("://")) {
- thumbnail_url = baseUrl + thumbnail_url
- }
- url = "/$slug"
- }
-
// Details
- override fun mangaDetailsParse(response: Response): SManga {
- val document = response.asJsoup()
- val dataStr = document
- .toString()
- .substringAfter("window.__DATA__ = ")
- .substringBefore("window._SITE_COLOR_")
- .substringBeforeLast(";")
+ override fun mangaDetailsRequest(manga: SManga): Request {
+ // throw exception if old url
+ if (!manga.url.contains("--")) throw Exception(urlChangedError(name))
- val dataManga = json.decodeFromString(dataStr)["manga"]
+ val url = "https://api.$apiDomain/api/manga${manga.url}".toHttpUrl().newBuilder()
+ .addQueryParameter("fields[]", "eng_name")
+ .addQueryParameter("fields[]", "otherNames")
+ .addQueryParameter("fields[]", "summary")
+ .addQueryParameter("fields[]", "rate")
+ .addQueryParameter("fields[]", "genres")
+ .addQueryParameter("fields[]", "tags")
+ .addQueryParameter("fields[]", "teams")
+ .addQueryParameter("fields[]", "authors")
+ .addQueryParameter("fields[]", "publisher")
+ .addQueryParameter("fields[]", "userRating")
+ .addQueryParameter("fields[]", "manga_status_id")
+ .addQueryParameter("fields[]", "status_id")
+ .addQueryParameter("fields[]", "artists")
- val manga = SManga.create()
-
- val body = document.select("div.media-info-list").first()!!
- val rawCategory = document.select(".media-short-info a.media-short-info__item").text()
- val category = when {
- rawCategory == "Комикс западный" -> "Комикс"
- rawCategory.isNotBlank() -> rawCategory
- else -> "Манга"
- }
-
- val rawAgeStop = document.select(".media-short-info .media-short-info__item[data-caution]").text()
-
- val ratingValue = document.select(".media-rating__value").last()!!.text().toFloat()
- val ratingVotes = document.select(".media-rating__votes").last()!!.text()
- val ratingStar = when {
- ratingValue > 9.5 -> "★★★★★"
- ratingValue > 8.5 -> "★★★★✬"
- ratingValue > 7.5 -> "★★★★☆"
- ratingValue > 6.5 -> "★★★✬☆"
- ratingValue > 5.5 -> "★★★☆☆"
- ratingValue > 4.5 -> "★★✬☆☆"
- ratingValue > 3.5 -> "★★☆☆☆"
- ratingValue > 2.5 -> "★✬☆☆☆"
- ratingValue > 1.5 -> "★☆☆☆☆"
- ratingValue > 0.5 -> "✬☆☆☆☆"
- else -> "☆☆☆☆☆"
- }
- val genres = document.select(".media-tags > a").map { it.text().capitalize() }
- manga.title = when {
- isEng.equals("rus") && dataManga!!.jsonObject["rusName"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() -> dataManga.jsonObject["rusName"]!!.jsonPrimitive.content
- isEng.equals("eng") && dataManga!!.jsonObject["engName"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() -> dataManga.jsonObject["engName"]!!.jsonPrimitive.content
- else -> dataManga!!.jsonObject["name"]!!.jsonPrimitive.content
- }
- manga.thumbnail_url = document.select(".media-header__cover").attr("src")
- manga.author = body.select("div.media-info-list__title:contains(Автор) + div a").joinToString { it.text() }
- manga.artist = body.select("div.media-info-list__title:contains(Художник) + div a").joinToString { it.text() }
-
- val statusTranslate = body.select("div.media-info-list__title:contains(Статус перевода) + div").text().lowercase(Locale.ROOT)
- val statusTitle = body.select("div.media-info-list__title:contains(Статус тайтла) + div").text().lowercase(Locale.ROOT)
-
- manga.status = if (document.html().contains("paper empty section")
- ) {
- SManga.LICENSED
- } else {
- when {
- statusTranslate.contains("завершен") && statusTitle.contains("приостановлен") || statusTranslate.contains("заморожен") || statusTranslate.contains("заброшен") -> SManga.ON_HIATUS
- statusTranslate.contains("завершен") && statusTitle.contains("выпуск прекращён") -> SManga.CANCELLED
- statusTranslate.contains("продолжается") -> SManga.ONGOING
- statusTranslate.contains("завершен") -> SManga.COMPLETED
- else -> when (statusTitle) {
- "онгоинг" -> SManga.ONGOING
- "анонс" -> SManga.ONGOING
- "завершён" -> SManga.COMPLETED
- "приостановлен" -> SManga.ON_HIATUS
- "выпуск прекращён" -> SManga.CANCELLED
- else -> SManga.UNKNOWN
- }
- }
- }
- manga.genre = category + ", " + rawAgeStop + ", " + genres.joinToString { it.trim() }
-
- val altName = if (dataManga.jsonObject["altNames"]?.jsonArray.orEmpty().isNotEmpty()) {
- "Альтернативные названия:\n" + dataManga.jsonObject["altNames"]!!.jsonArray.joinToString(" / ") { it.jsonPrimitive.content } + "\n\n"
- } else {
- ""
- }
-
- val mediaNameLanguage = when {
- isEng.equals("eng") && dataManga.jsonObject["rusName"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() -> dataManga.jsonObject["rusName"]!!.jsonPrimitive.content + "\n"
- isEng.equals("rus") && dataManga.jsonObject["engName"]?.jsonPrimitive?.content.orEmpty().isNotEmpty() -> dataManga.jsonObject["engName"]!!.jsonPrimitive.content + "\n"
- else -> ""
- }
- manga.description = mediaNameLanguage + ratingStar + " " + ratingValue + " (голосов: " + ratingVotes + ")\n" + altName + document.select(".media-description__text").text()
- return manga
+ return GET(url.build(), headers)
}
+ override fun mangaDetailsParse(response: Response): SManga = response.parseAs>().data.toSManga(isEng())
+
override fun fetchMangaDetails(manga: SManga): Observable {
return client.newCall(mangaDetailsRequest(manga))
.asObservable().doOnNext { response ->
if (!response.isSuccessful) {
- if (response.code == 404 && response.asJsoup().select(".m-menu__sign-in").isNotEmpty()) throw Exception("HTTP error ${response.code}. Для просмотра 18+ контента необходима авторизация через WebView\uD83C\uDF0E︎") else throw Exception("HTTP error ${response.code}")
+ if (response.code == 404) throw Exception("HTTP error ${response.code}. Для просмотра 18+ контента необходима авторизация через WebView\uD83C\uDF0E︎") else throw Exception("HTTP error ${response.code}")
}
}
.map { response ->
@@ -304,47 +272,58 @@ abstract class LibGroup(
}
// Chapters
+ override fun chapterListRequest(manga: SManga): Request {
+ // throw exception if old url
+ if (!manga.url.contains("--")) throw Exception(urlChangedError(name))
+
+ return GET("https://api.$apiDomain/api/manga${manga.url}/chapters", headers)
+ }
+
+ override fun getChapterUrl(chapter: SChapter): String = "$baseUrl${chapter.url}"
+
+ private fun getDefaultBranch(id: String): List =
+ client.newCall(GET("https://api.$apiDomain/api/branches/$id", headers)).execute().parseAs>>().data
+
override fun chapterListParse(response: Response): List {
- val document = response.asJsoup()
- val rawAgeStop = document.select(".media-short-info .media-short-info__item[data-caution]").text()
- if (rawAgeStop == "18+" && document.select(".m-menu__sign-in").isNotEmpty()) {
- throw Exception("Для просмотра 18+ контента необходима авторизация через WebView\uD83C\uDF0E︎")
+ val slugUrl = response.request.url.toString().substringAfter("manga/").substringBefore("/chapters")
+ val chaptersData = response.parseAs>>()
+ if (chaptersData.data.isEmpty()) {
+ throw Exception("Нет глав")
}
- val redirect = document.html()
- if (redirect.contains("paper empty section")) {
- throw Exception("Лицензировано - Нет глав")
- }
- val dataStr = document
- .toString()
- .substringAfter("window.__DATA__ = ")
- .substringBefore("window._SITE_COLOR_")
- .substringBeforeLast(";")
- val data = json.decodeFromString(dataStr)
- val chaptersList = data["chapters"]!!.jsonObject["list"]?.jsonArray
- val slug = data["manga"]!!.jsonObject["slug"]!!.jsonPrimitive.content
- val branches = data["chapters"]!!.jsonObject["branches"]!!.jsonArray.reversed()
- val teams = data["chapters"]!!.jsonObject["teams"]!!.jsonArray
val sortingList = preferences.getString(SORTING_PREF, "ms_mixing")
- val auth = data["auth"]!!.jsonPrimitive.content
- val userId = if (auth == "true") data["user"]!!.jsonObject["id"]!!.jsonPrimitive.content else "not"
+ val defaultBranchId = runCatching { getDefaultBranch(slugUrl.substringBefore("-")).first().id }.getOrNull()
- val chapters: List? = if (branches.isNotEmpty()) {
- sortChaptersByTranslator(sortingList, chaptersList, slug, userId, branches)
- } else {
- chaptersList
- ?.filter { it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 && it.jsonObject["price"]?.jsonPrimitive?.intOrNull == 0 }
- ?.map { chapterFromElement(it, sortingList, slug, userId, null, null, teams, chaptersList) }
+ val chapters = mutableListOf()
+ for (it in chaptersData.data.withIndex()) {
+ if (it.value.branchesCount > 1) {
+ for (currentBranch in it.value.branches.withIndex()) {
+ if (currentBranch.value.branchId == defaultBranchId && sortingList == "ms_mixing") { // ms_mixing with default branch from api
+ chapters.add(it.value.toSChapter(slugUrl, defaultBranchId, isScanUser()))
+ } else if (defaultBranchId == null && sortingList == "ms_mixing") { // ms_mixing with first branch in chapter
+ if (chapters.any { chpIt -> chpIt.chapter_number == it.value.itemNumber }) {
+ chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
+ }
+ } else if (sortingList == "ms_combining") { // ms_combining
+ chapters.add(it.value.toSChapter(slugUrl, currentBranch.value.branchId, isScanUser()))
+ }
+ }
+ } else {
+ chapters.add(it.value.toSChapter(slugUrl, isScanUser = isScanUser()))
+ }
}
- return chapters ?: emptyList()
+ return chapters.reversed()
}
override fun fetchChapterList(manga: SManga): Observable> {
- return client.newCall(mangaDetailsRequest(manga))
+ if (manga.status == SManga.LICENSED) {
+ throw Exception("Лицензировано - Нет глав")
+ }
+ return client.newCall(chapterListRequest(manga))
.asObservable().doOnNext { response ->
if (!response.isSuccessful) {
- if (response.code == 404 && response.asJsoup().select(".m-menu__sign-in").isNotEmpty()) throw Exception("HTTP error ${response.code}. Для просмотра 18+ контента необходима авторизация через WebView\uD83C\uDF0E︎") else throw Exception("HTTP error ${response.code}")
+ if (response.code == 404) throw Exception("HTTP error ${response.code}. Для просмотра 18+ контента необходима авторизация через WebView\uD83C\uDF0E︎") else throw Exception("HTTP error ${response.code}")
}
}
.map { response ->
@@ -352,178 +331,47 @@ abstract class LibGroup(
}
}
- private fun sortChaptersByTranslator(sortingList: String?, chaptersList: JsonArray?, slug: String, userId: String, branches: List): List? {
- var chapters: List? = null
- val volume = "(?<=/v)[0-9]+(?=/c[0-9]+)".toRegex()
- val tempChaptersList = mutableListOf()
- for (currentBranch in branches.withIndex()) {
- val branch = branches[currentBranch.index]
- val teamId = branch.jsonObject["id"]!!.jsonPrimitive.int
- val teams = branch.jsonObject["teams"]!!.jsonArray
- val isActive = teams.filter { it.jsonObject["is_active"]?.jsonPrimitive?.intOrNull == 1 }
- val teamsBranch = if (isActive.size == 1) {
- isActive[0].jsonObject["name"]?.jsonPrimitive?.contentOrNull
- } else if (teams.isNotEmpty() && isActive.isEmpty()) {
- teams[0].jsonObject["name"]?.jsonPrimitive?.contentOrNull
- } else {
- "Неизвестный"
- }
- chapters = chaptersList
- ?.filter { it.jsonObject["branch_id"]?.jsonPrimitive?.intOrNull == teamId && it.jsonObject["status"]?.jsonPrimitive?.intOrNull != 2 }
- ?.map { chapterFromElement(it, sortingList, slug, userId, teamId, branches) }
- when (sortingList) {
- "ms_mixing" -> {
- chapters?.let {
- if ((tempChaptersList.size < it.size) && !groupTranslates.contains(teamsBranch.toString())) {
- tempChaptersList.addAll(0, it)
- } else {
- tempChaptersList.addAll(it)
- }
- }
- chapters = tempChaptersList.distinctBy { volume.find(it.url)?.value + "--" + it.chapter_number }.sortedWith(compareBy({ -it.chapter_number }, { volume.find(it.url)?.value }))
- }
- "ms_combining" -> {
- if (!groupTranslates.contains(teamsBranch.toString())) {
- chapters?.let { tempChaptersList.addAll(it) }
- }
- chapters = tempChaptersList
- }
- }
- }
- return chapters
- }
-
- private fun chapterFromElement(chapterItem: JsonElement, sortingList: String?, slug: String, userId: String, teamIdParam: Int? = null, branches: List? = null, teams: List? = null, chaptersList: JsonArray? = null): SChapter {
- val chapter = SChapter.create()
-
- val volume = chapterItem.jsonObject["chapter_volume"]!!.jsonPrimitive.int
- val number = chapterItem.jsonObject["chapter_number"]!!.jsonPrimitive.content
- val chapterScanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
- val isScanlatorId = teams?.filter { it.jsonObject["id"]?.jsonPrimitive?.intOrNull == chapterScanlatorId }
-
- val teamId = if (teamIdParam != null) "&bid=$teamIdParam" else ""
-
- val url = "/$slug/v$volume/c$number?ui=$userId$teamId"
-
- chapter.setUrlWithoutDomain(url)
-
- val nameChapter = chapterItem.jsonObject["chapter_name"]?.jsonPrimitive?.contentOrNull
- val fullNameChapter = "Том $volume. Глава $number"
- chapter.scanlator = if (teams?.size == 1) teams[0].jsonObject["name"]?.jsonPrimitive?.content else if (isScanlatorId.orEmpty().isNotEmpty()) isScanlatorId!![0].jsonObject["name"]?.jsonPrimitive?.content else branches?.let { getScanlatorTeamName(it, chapterItem) } ?: if ((preferences.getBoolean(isScan_USER, false)) || (chaptersList?.distinctBy { it.jsonObject["username"]!!.jsonPrimitive.content }?.size == 1)) chapterItem.jsonObject["username"]!!.jsonPrimitive.content else null
- chapter.name = if (nameChapter.isNullOrBlank()) fullNameChapter else "$fullNameChapter - $nameChapter"
- chapter.date_upload = simpleDateFormat.parse(chapterItem.jsonObject["chapter_created_at"]!!.jsonPrimitive.content.substringBefore(" "))?.time ?: 0L
- chapter.chapter_number = number.toFloatOrNull() ?: -1f
-
- return chapter
- }
-
- private fun getScanlatorTeamName(branches: List, chapterItem: JsonElement): String? {
- var scanlatorData: String? = null
- for (currentBranch in branches.withIndex()) {
- val branch = branches[currentBranch.index].jsonObject
- val teams = branch["teams"]!!.jsonArray
- if (chapterItem.jsonObject["branch_id"]!!.jsonPrimitive.int == branch["id"]!!.jsonPrimitive.int && teams.isNotEmpty()) {
- for (currentTeam in teams.withIndex()) {
- val team = teams[currentTeam.index].jsonObject
- val scanlatorId = chapterItem.jsonObject["chapter_scanlator_id"]!!.jsonPrimitive.int
- if ((scanlatorId == team.jsonObject["id"]!!.jsonPrimitive.int) ||
- (scanlatorId == 0 && team["is_active"]!!.jsonPrimitive.int == 1)
- ) {
- return team["name"]!!.jsonPrimitive.content
- } else {
- scanlatorData = branch["teams"]!!.jsonArray[0].jsonObject["name"]!!.jsonPrimitive.content
- }
- }
- }
- }
- return scanlatorData
- }
-
// Pages
- override fun pageListParse(response: Response): List {
- val document = response.asJsoup()
+ override fun pageListRequest(chapter: SChapter): Request {
+ // throw exception if old url
+ if (!chapter.url.contains("--")) throw Exception(urlChangedError(name))
- // redirect Регистрация 18+
- val redirect = document.html()
- if (!redirect.contains("window.__info")) {
- if (redirect.contains("auth-layout")) {
- throw Exception("Для просмотра 18+ контента необходима авторизация через WebView\uD83C\uDF0E︎")
- }
- }
-
- val chapInfo = document
- .select("script:containsData(window.__info)")
- .first()!!
- .html()
- .split("window.__info = ")
- .last()
- .trim()
- .split(";")
- .first()
-
- val chapInfoJson = json.decodeFromString(chapInfo)
- val servers = chapInfoJson["servers"]!!.jsonObject.toMap()
- val imgUrl: String = chapInfoJson["img"]!!.jsonObject["url"]!!.jsonPrimitive.content
-
- val serverToUse = listOf(isServer, "secondary", "fourth", "main", "compress").distinct()
-
- // Get pages
- val pagesArr = document
- .select("script:containsData(window.__pg)")
- .first()!!
- .html()
- .trim()
- .removePrefix("window.__pg = ")
- .removeSuffix(";")
-
- val pagesJson = json.decodeFromString(pagesArr)
- val pages = mutableListOf()
-
- pagesJson.forEach { page ->
- val keys = servers.keys.filter { serverToUse.indexOf(it) >= 0 }.sortedBy { serverToUse.indexOf(it) }
- val serversUrls = keys.map {
- servers[it]?.jsonPrimitive?.contentOrNull + imgUrl + page.jsonObject["u"]!!.jsonPrimitive.content
- }.distinct().joinToString(separator = ",,") { it }
- pages.add(Page(page.jsonObject["p"]!!.jsonPrimitive.int, serversUrls))
- }
-
- return pages
+ return GET("https://api.$apiDomain/api/manga${chapter.url}", headers)
}
- private fun checkImage(url: String): Boolean {
- val response = client.newCall(GET(url, imgHeader())).execute()
- return response.isSuccessful && (response.header("content-length", "0")?.toInt()!! > 600)
+ override fun pageListParse(response: Response): List {
+ val chapter = response.parseAs>().data.toPageList().toMutableList()
+ chapter.sortBy { it.index }
+ return chapter
}
override fun fetchImageUrl(page: Page): Observable {
if (page.imageUrl != null) {
return Observable.just(page.imageUrl)
}
-
- val urls = page.url.split(",,")
-
- return Observable.from(urls).filter { checkImage(it) }.first()
+ val server = getConstants().getServer(isServer(), siteId).url
+ return Observable.just("$server${page.url}")
}
- override fun imageUrlParse(response: Response): String = ""
+ override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
- return GET(page.imageUrl!!, imgHeader())
- }
-
- // Workaround to allow "Open in browser" use the
- private fun searchMangaByIdRequest(id: String): Request {
- return GET("$baseUrl/$id", headers)
+ val imageHeader = Headers.Builder().apply {
+ // User-Agent required for authorization through third-party accounts (mobile version for correct display in WebView)
+ add("User-Agent", userAgentMobile)
+ add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
+ add("Referer", baseUrl)
+ }
+ return GET(page.imageUrl!!, imageHeader.build())
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable {
return if (query.startsWith(PREFIX_SLUG_SEARCH)) {
- val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH)
- client.newCall(searchMangaByIdRequest(realQuery))
+ val realQuery = query.removePrefix(PREFIX_SLUG_SEARCH).substringBefore("/").substringBefore("?")
+ client.newCall(GET("https://api.$apiDomain/api/manga/$realQuery", headers))
.asObservableSuccess()
.map { response ->
- val details = mangaDetailsParse(response)
- details.url = "/$realQuery"
+ val details = response.parseAs>().data.toSManga(isEng())
MangasPage(listOf(details), false)
}
} else {
@@ -537,14 +385,11 @@ abstract class LibGroup(
// Search
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- if (csrfToken.isEmpty()) {
- val tokenResponse = client.newCall(popularMangaRequest(page)).execute()
- val resBody = tokenResponse.body.string()
- csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
- }
- val url = "$baseUrl/filterlist?page=$page&chapters[min]=1".toHttpUrl().newBuilder()
+ val url = "https://api.$apiDomain/api/manga".toHttpUrl().newBuilder()
+ url.addQueryParameter("page", page.toString())
+ url.addQueryParameter("site_id[]", siteId.toString())
if (query.isNotEmpty()) {
- url.addQueryParameter("name", query)
+ url.addQueryParameter("q", query)
}
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
when (filter) {
@@ -553,44 +398,62 @@ abstract class LibGroup(
url.addQueryParameter("types[]", category.id)
}
}
- is FormatList -> filter.state.forEach { forma ->
- if (forma.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(if (forma.isIncluded()) "format[include][]" else "format[exclude][]", forma.id)
+ is FormatList -> filter.state.forEach { format ->
+ if (format.state != Filter.TriState.STATE_IGNORE) {
+ url.addQueryParameter(if (format.isIncluded()) "format[]" else "format_exclude[]", format.id)
}
}
is StatusList -> filter.state.forEach { status ->
if (status.state) {
- url.addQueryParameter("status[]", status.id)
+ url.addQueryParameter("scanlate_status[]", status.id)
}
}
is StatusTitleList -> filter.state.forEach { title ->
if (title.state) {
- url.addQueryParameter("manga_status[]", title.id)
+ url.addQueryParameter("status[]", title.id)
}
}
is GenreList -> filter.state.forEach { genre ->
if (genre.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(if (genre.isIncluded()) "genres[include][]" else "genres[exclude][]", genre.id)
+ url.addQueryParameter(if (genre.isIncluded()) "genres[]" else "genres_exclude[]", genre.id)
}
}
is OrderBy -> {
- url.addQueryParameter("dir", if (filter.state!!.ascending) "asc" else "desc")
- url.addQueryParameter("sort", arrayOf("rate", "name", "views", "created_at", "last_chapter_at", "chap_count")[filter.state!!.index])
+ if (filter.state!!.index == 0) {
+ url.addQueryParameter("sort_type", if (filter.state!!.ascending) "asc" else "desc")
+ return@forEach
+ }
+ val orderArray = arrayOf("", "rate_avg", "name", "rus_name", "views", "releaseDate", "created_at", "last_chapter_at", "chap_count")
+ url.addQueryParameter("sort_type", if (filter.state!!.ascending) "asc" else "desc")
+ url.addQueryParameter("sort_by", orderArray[filter.state!!.index])
+ if (orderArray[filter.state!!.index] == "rate") {
+ url.addQueryParameter("rate_min", "50")
+ }
}
is MyList -> filter.state.forEach { favorite ->
if (favorite.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(if (favorite.isIncluded()) "bookmarks[include][]" else "bookmarks[exclude][]", favorite.id)
+ url.addQueryParameter(if (favorite.isIncluded()) "bookmarks[]" else "bookmarks_exclude[]", favorite.id)
}
}
is RequireChapters -> {
- if (filter.state == 1) {
- url.setQueryParameter("chapters[min]", "0")
+ if (filter.state == 0) {
+ url.setQueryParameter("chap_count_min", "1")
+ }
+ }
+ is AgeList -> filter.state.forEach { age ->
+ if (age.state) {
+ url.addQueryParameter("caution[]", age.id)
+ }
+ }
+ is TagList -> filter.state.forEach { tag ->
+ if (tag.state != Filter.TriState.STATE_IGNORE) {
+ url.addQueryParameter(if (tag.isIncluded()) "tags[]" else "tags_exclude[]", tag.id)
}
}
else -> {}
}
}
- return POST(url.toString(), catalogHeaders())
+ return GET(url.build(), headers)
}
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
@@ -600,107 +463,50 @@ abstract class LibGroup(
private class CheckFilter(name: String, val id: String) : Filter.CheckBox(name)
private class CategoryList(categories: List) : Filter.Group("Тип", categories)
- private class FormatList(formas: List) : Filter.Group("Формат выпуска", formas)
+ private class FormatList(formats: List) : Filter.Group("Формат выпуска", formats)
+ private class GenreList(genres: List) : Filter.Group("Жанры", genres)
+ private class TagList(tags: List) : Filter.Group("Теги", tags)
private class StatusList(statuses: List) : Filter.Group("Статус перевода", statuses)
private class StatusTitleList(titles: List) : Filter.Group("Статус тайтла", titles)
- private class GenreList(genres: List) : Filter.Group("Жанры", genres)
+ private class AgeList(ages: List) : Filter.Group("Возрастное ограничение", ages)
private class MyList(favorites: List) : Filter.Group("Мои списки", favorites)
- override fun getFilterList() = FilterList(
- OrderBy(),
- CategoryList(getCategoryList()),
- FormatList(getFormatList()),
- GenreList(getGenreList()),
- StatusList(getStatusList()),
- StatusTitleList(getStatusTitleList()),
- MyList(getMyList()),
- RequireChapters(),
- )
+ override fun getFilterList(): FilterList {
+ launchIO { getConstants() }
+
+ val filters = mutableListOf>()
+ filters += listOf(
+ OrderBy(),
+ )
+
+ filters += if (_constants != null) {
+ listOf(
+ CategoryList(getConstants().getCategories(siteId).map { CheckFilter(it.label, it.id.toString()) }),
+ FormatList(getConstants().getFormats(siteId).map { SearchFilter(it.name, it.id.toString()) }),
+ GenreList(getConstants().getGenres(siteId).map { SearchFilter(it.name, it.id.toString()) }),
+ TagList(getConstants().getTags(siteId).map { SearchFilter(it.name, it.id.toString()) }),
+ StatusList(getConstants().getScanlateStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
+ StatusTitleList(getConstants().getTitleStatuses(siteId).map { CheckFilter(it.label, it.id.toString()) }),
+ AgeList(getConstants().getAgeRestrictions(siteId).map { CheckFilter(it.label, it.id.toString()) }),
+ )
+ } else {
+ listOf(
+ Filter.Header("Нажмите «Сбросить», чтобы попытаться отобразить дополнительные фильтры."),
+ )
+ }
+
+ filters += listOf(
+ MyList(getMyList()),
+ RequireChapters(),
+ )
+
+ return FilterList(filters)
+ }
private class OrderBy : Filter.Sort(
"Сортировка",
- arrayOf("Рейтинг", "Имя", "Просмотры", "Дате добавления", "Дате обновления", "Кол-во глав"),
- Selection(2, false),
- )
-
- private fun getCategoryList() = listOf(
- CheckFilter("Манга", "1"),
- CheckFilter("OEL-манга", "4"),
- CheckFilter("Манхва", "5"),
- CheckFilter("Маньхуа", "6"),
- CheckFilter("Руманга", "8"),
- CheckFilter("Комикс западный", "9"),
- )
-
- private fun getFormatList() = listOf(
- SearchFilter("4-кома (Ёнкома)", "1"),
- SearchFilter("Сборник", "2"),
- SearchFilter("Додзинси", "3"),
- SearchFilter("Сингл", "4"),
- SearchFilter("В цвете", "5"),
- SearchFilter("Веб", "6"),
- )
-
- private fun getStatusList() = listOf(
- CheckFilter("Продолжается", "1"),
- CheckFilter("Завершен", "2"),
- CheckFilter("Заморожен", "3"),
- CheckFilter("Заброшен", "4"),
- )
-
- private fun getStatusTitleList() = listOf(
- CheckFilter("Онгоинг", "1"),
- CheckFilter("Завершён", "2"),
- CheckFilter("Анонс", "3"),
- CheckFilter("Приостановлен", "4"),
- CheckFilter("Выпуск прекращён", "5"),
- )
-
- private fun getGenreList() = listOf(
- SearchFilter("арт", "32"),
- SearchFilter("боевик", "34"),
- SearchFilter("боевые искусства", "35"),
- SearchFilter("вампиры", "36"),
- SearchFilter("гарем", "37"),
- SearchFilter("гендерная интрига", "38"),
- SearchFilter("героическое фэнтези", "39"),
- SearchFilter("детектив", "40"),
- SearchFilter("дзёсэй", "41"),
- SearchFilter("драма", "43"),
- SearchFilter("игра", "44"),
- SearchFilter("исекай", "79"),
- SearchFilter("история", "45"),
- SearchFilter("киберпанк", "46"),
- SearchFilter("кодомо", "76"),
- SearchFilter("комедия", "47"),
- SearchFilter("махо-сёдзё", "48"),
- SearchFilter("меха", "49"),
- SearchFilter("мистика", "50"),
- SearchFilter("научная фантастика", "51"),
- SearchFilter("омегаверс", "77"),
- SearchFilter("повседневность", "52"),
- SearchFilter("постапокалиптика", "53"),
- SearchFilter("приключения", "54"),
- SearchFilter("психология", "55"),
- SearchFilter("романтика", "56"),
- SearchFilter("самурайский боевик", "57"),
- SearchFilter("сверхъестественное", "58"),
- SearchFilter("сёдзё", "59"),
- SearchFilter("сёдзё-ай", "60"),
- SearchFilter("сёнэн", "61"),
- SearchFilter("сёнэн-ай", "62"),
- SearchFilter("спорт", "63"),
- SearchFilter("сэйнэн", "64"),
- SearchFilter("трагедия", "65"),
- SearchFilter("триллер", "66"),
- SearchFilter("ужасы", "67"),
- SearchFilter("фантастика", "68"),
- SearchFilter("фэнтези", "69"),
- SearchFilter("школа", "70"),
- SearchFilter("эротика", "71"),
- SearchFilter("этти", "72"),
- SearchFilter("юри", "73"),
- SearchFilter("яой", "74"),
+ arrayOf("Популярность", "Рейтинг", "Имя (A-Z)", "Имя (А-Я)", "Просмотры", "Дата релиза", "Дате добавления", "Дате обновления", "Кол-во глав"),
+ Selection(0, false),
)
private fun getMyList() = listOf(
@@ -716,47 +522,58 @@ abstract class LibGroup(
arrayOf("Да", "Все"),
)
+ // Utils
+ private inline fun String.parseAs(): T = json.decodeFromString(this)
+
+ private inline fun Response.parseAs(): T = body.string().parseAs()
+
+ private fun urlChangedError(sourceName: String): String =
+ "URL серии изменился. Перенесите с $sourceName " +
+ "на $sourceName, чтобы обновить URL-адрес."
+
+ private val scope = CoroutineScope(Dispatchers.IO)
+ private fun launchIO(block: () -> Unit) = scope.launch { block() }
+
companion object {
const val PREFIX_SLUG_SEARCH = "slug:"
private const val SERVER_PREF = "MangaLibImageServer"
private const val SORTING_PREF = "MangaLibSorting"
- private const val SORTING_PREF_Title = "Способ выбора переводчиков"
+ private const val SORTING_PREF_TITLE = "Способ выбора переводчиков"
- private const val isScan_USER = "ScanlatorUsername"
- private const val isScan_USER_Title = "Альтернативный переводчик"
+ private const val IS_SCAN_USER = "ScanlatorUsername"
+ private const val IS_SCAN_USER_TITLE = "Альтернативный переводчик"
private const val TRANSLATORS_TITLE = "Чёрный список переводчиков\n(для красоты через «/» или с новой строки)"
private const val TRANSLATORS_DEFAULT = ""
private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
- private const val LANGUAGE_PREF_Title = "Выбор языка на обложке"
+ private const val LANGUAGE_PREF_TITLE = "Выбор языка на обложке"
- private val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.US) }
+ private const val TOKEN_STORE = "TokenStore"
+
+ val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US) }
}
- private var isServer: String? = preferences.getString(SERVER_PREF, "fourth")
- private var isEng: String? = preferences.getString(LANGUAGE_PREF, "eng")
- private var groupTranslates: String = preferences.getString(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT)!!
+ private fun isServer(): String = preferences.getString(SERVER_PREF, "main")!!
+ private fun isEng(): String = preferences.getString(LANGUAGE_PREF, "eng")!!
+ private fun groupTranslates(): String = preferences.getString(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT)!!
+ private fun isScanUser(): Boolean = preferences.getBoolean(IS_SCAN_USER, false)
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val serverPref = ListPreference(screen.context).apply {
key = SERVER_PREF
title = "Сервер изображений"
entries = arrayOf("Первый", "Второй", "Сжатия")
- entryValues = arrayOf("secondary", "fourth", "compress")
+ entryValues = arrayOf("main", "secondary", "compress")
summary = "%s \n\nВыбор приоритетного сервера изображений. \n" +
- "По умолчанию «Второй». \n\n" +
+ "По умолчанию «Первый». \n\n" +
"ⓘВыбор другого помогает при долгой автоматической смене/загрузке изображений текущего."
- setDefaultValue("fourth")
- setOnPreferenceChangeListener { _, newValue ->
- isServer = newValue.toString()
- true
- }
+ setDefaultValue("main")
}
val sortingPref = ListPreference(screen.context).apply {
key = SORTING_PREF
- title = SORTING_PREF_Title
+ title = SORTING_PREF_TITLE
entries = arrayOf(
"Полный список (без повторных переводов)",
"Все переводы (друг за другом)",
@@ -764,39 +581,29 @@ abstract class LibGroup(
entryValues = arrayOf("ms_mixing", "ms_combining")
summary = "%s"
setDefaultValue("ms_mixing")
- setOnPreferenceChangeListener { _, newValue ->
- val selected = newValue as String
- preferences.edit().putString(SORTING_PREF, selected).commit()
- }
}
val scanlatorUsername = androidx.preference.CheckBoxPreference(screen.context).apply {
- key = isScan_USER
- title = isScan_USER_Title
+ key = IS_SCAN_USER
+ title = IS_SCAN_USER_TITLE
summary = "Отображает Ник переводчика если Группа не указана явно."
setDefaultValue(false)
-
- setOnPreferenceChangeListener { _, newValue ->
- val checkValue = newValue as Boolean
- preferences.edit().putBoolean(key, checkValue).commit()
- }
}
val titleLanguagePref = ListPreference(screen.context).apply {
key = LANGUAGE_PREF
- title = LANGUAGE_PREF_Title
+ title = LANGUAGE_PREF_TITLE
entries = arrayOf("Английский", "Русский")
entryValues = arrayOf("eng", "rus")
summary = "%s"
setDefaultValue("eng")
- setOnPreferenceChangeListener { _, newValue ->
- val titleLanguage = preferences.edit().putString(LANGUAGE_PREF, newValue as String).commit()
+ setOnPreferenceChangeListener { _, _ ->
val warning = "Если язык обложки не изменился очистите базу данных в приложении (Настройки -> Дополнительно -> Очистить базу данных)"
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
- titleLanguage
+ true
}
}
screen.addPreference(serverPref)
screen.addPreference(sortingPref)
- screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates))
+ screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates()))
screen.addPreference(scanlatorUsername)
screen.addPreference(titleLanguagePref)
}
@@ -807,16 +614,17 @@ abstract class LibGroup(
summary = value.replace("/", "\n")
this.setDefaultValue(default)
dialogTitle = title
- setOnPreferenceChangeListener { _, newValue ->
- try {
- val res = preferences.edit().putString(title, newValue as String).commit()
- Toast.makeText(context, "Для обновления списка необходимо перезапустить приложение с полной остановкой.", Toast.LENGTH_LONG).show()
- res
- } catch (e: Exception) {
- e.printStackTrace()
- false
- }
+ setOnPreferenceChangeListener { _, _ ->
+ Toast.makeText(context, "Для обновления списка необходимо перезапустить приложение с полной остановкой.", Toast.LENGTH_LONG).show()
+ true
}
}
}
+
+ // 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
+ }
}
diff --git a/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroupDto.kt b/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroupDto.kt
new file mode 100644
index 000000000..d4cc52de0
--- /dev/null
+++ b/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibGroupDto.kt
@@ -0,0 +1,285 @@
+package eu.kanade.tachiyomi.multisrc.libgroup
+
+import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.model.SChapter
+import eu.kanade.tachiyomi.source.model.SManga
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+class Data(
+ val data: T,
+)
+
+@Serializable
+class Constants(
+ @SerialName("ageRestriction") val ageRestrictions: List,
+ @SerialName("format") val formats: List,
+ val genres: List,
+ val imageServers: List,
+ @SerialName("scanlateStatus") val scanlateStatuses: List,
+ @SerialName("status") val titleStatuses: List,
+ val tags: List,
+ val types: List,
+) {
+ @Serializable
+ class IdLabelSiteType(
+ val id: Int,
+ val label: String,
+ @SerialName("site_ids") val siteIds: List,
+ )
+
+ @Serializable
+ class IdNameSiteType(
+ val id: Int,
+ val name: String,
+ @SerialName("site_ids") val siteIds: List,
+ )
+
+ @Serializable
+ class ImageServer(
+ val id: String,
+ val label: String,
+ val url: String,
+ @SerialName("site_ids") val siteIds: List,
+ )
+
+ fun getServer(isServers: String?, siteId: Int): ImageServer =
+ if (!isServers.isNullOrBlank()) {
+ imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
+ } else {
+ imageServers.first { it.siteIds.contains(siteId) }
+ }
+
+ fun getCategories(siteId: Int): List = types.filter { it.siteIds.contains(siteId) }
+ fun getFormats(siteId: Int): List = formats.filter { it.siteIds.contains(siteId) }
+ fun getGenres(siteId: Int): List = genres.filter { it.siteIds.contains(siteId) }
+ fun getTags(siteId: Int): List = tags.filter { it.siteIds.contains(siteId) }
+ fun getScanlateStatuses(siteId: Int): List = scanlateStatuses.filter { it.siteIds.contains(siteId) }
+ fun getTitleStatuses(siteId: Int): List = titleStatuses.filter { it.siteIds.contains(siteId) }
+ fun getAgeRestrictions(siteId: Int): List = ageRestrictions.filter { it.siteIds.contains(siteId) }
+}
+
+@Serializable
+class MangasPageDto(
+ val data: List,
+ val meta: MangaPageMeta,
+) {
+ @Serializable
+ class MangaPageMeta(
+ @SerialName("has_next_page") val hasNextPage: Boolean,
+ )
+
+ fun mapToSManga(isEng: String): List {
+ return this.data.map { it.toSManga(isEng) }
+ }
+}
+
+@Serializable
+class MangaShort(
+ val name: String,
+ @SerialName("rus_name") val rusName: String?,
+ @SerialName("eng_name") val engName: String?,
+ @SerialName("slug_url") val slugUrl: String,
+ val cover: Cover,
+) {
+ @Serializable
+ data class Cover(
+ val default: String?,
+ )
+
+ fun toSManga(isEng: String) = SManga.create().apply {
+ title = getSelectedLanguage(isEng, rusName, engName, name)
+ thumbnail_url = cover.default.orEmpty()
+ url = "/$slugUrl"
+ }
+}
+
+@Serializable
+class Manga(
+ val type: LabelType,
+ val ageRestriction: LabelType,
+ val rating: Rating,
+ val genres: List,
+ val tags: List,
+ @SerialName("rus_name") val rusName: String?,
+ @SerialName("eng_name") val engName: String?,
+ val name: String,
+ val cover: MangaShort.Cover,
+ val authors: List,
+ val artists: List,
+ val status: LabelType,
+ val scanlateStatus: LabelType,
+ @SerialName("is_licensed") val isLicensed: Boolean,
+ val otherNames: List,
+ val summary: String,
+) {
+ @Serializable
+ class LabelType(
+ val label: String,
+ )
+
+ @Serializable
+ class NameType(
+ val name: String,
+ )
+
+ @Serializable
+ class Rating(
+ val average: Float,
+ val votes: Int,
+ )
+
+ fun toSManga(isEng: String): SManga = SManga.create().apply {
+ title = getSelectedLanguage(isEng, rusName, engName, name)
+ thumbnail_url = cover.default
+ author = authors.joinToString { it.name }
+ artist = artists.joinToString { it.name }
+ status = parseStatus(isLicensed, scanlateStatus.label, this@Manga.status.label)
+ genre = type.label.ifBlank { "Манга" } + ", " + ageRestriction.label + ", " +
+ genres.joinToString { it.name.trim() } + ", " + tags.joinToString { it.name.trim() }
+ description = getOppositeLanguage(isEng, rusName, engName) + rating.average.parseAverage() + " " + rating.average +
+ " (голосов: " + rating.votes + ")\n" + otherNames.joinAltNames() + summary
+ }
+
+ private fun Float.parseAverage(): String {
+ return when {
+ this > 9.5 -> "★★★★★"
+ this > 8.5 -> "★★★★✬"
+ this > 7.5 -> "★★★★☆"
+ this > 6.5 -> "★★★✬☆"
+ this > 5.5 -> "★★★☆☆"
+ this > 4.5 -> "★★✬☆☆"
+ this > 3.5 -> "★★☆☆☆"
+ this > 2.5 -> "★✬☆☆☆"
+ this > 1.5 -> "★☆☆☆☆"
+ this > 0.5 -> "✬☆☆☆☆"
+ else -> "☆☆☆☆☆"
+ }
+ }
+
+ private fun parseStatus(isLicensed: Boolean, statusTranslate: String, statusTitle: String): Int = when {
+ isLicensed -> SManga.LICENSED
+ statusTranslate == "Завершён" && statusTitle == "Приостановлен" || statusTranslate == "Заморожен" || statusTranslate == "Заброшен" -> SManga.ON_HIATUS
+ statusTranslate == "Завершён" && statusTitle == "Выпуск прекращён" -> SManga.CANCELLED
+ statusTranslate == "Продолжается" -> SManga.ONGOING
+ statusTranslate == "Выходит" -> SManga.ONGOING
+ statusTranslate == "Завершён" -> SManga.COMPLETED
+ statusTranslate == "Вышло" -> SManga.PUBLISHING_FINISHED
+ else -> when (statusTitle) {
+ "Онгоинг" -> SManga.ONGOING
+ "Анонс" -> SManga.ONGOING
+ "Завершён" -> SManga.COMPLETED
+ "Приостановлен" -> SManga.ON_HIATUS
+ "Выпуск прекращён" -> SManga.CANCELLED
+ else -> SManga.UNKNOWN
+ }
+ }
+
+ private fun List.joinAltNames(): String = when {
+ this.isNotEmpty() -> "Альтернативные названия:\n" + this.joinToString(" / ") + "\n\n"
+ else -> ""
+ }
+}
+
+private fun getSelectedLanguage(isEng: String, rusName: String?, engName: String?, name: String): String = when {
+ isEng == "rus" && rusName.orEmpty().isNotEmpty() -> rusName!!
+ isEng == "eng" && engName.orEmpty().isNotEmpty() -> engName!!
+ else -> name
+}
+
+private fun getOppositeLanguage(isEng: String, rusName: String?, engName: String?): String = when {
+ isEng == "eng" && rusName.orEmpty().isNotEmpty() -> rusName + "\n"
+ isEng == "rus" && engName.orEmpty().isNotEmpty() -> engName + "\n"
+ else -> ""
+}
+
+@Serializable
+class Chapter(
+ val id: Int,
+ @SerialName("branches_count") val branchesCount: Int,
+ val branches: List,
+ val name: String?,
+ val number: String,
+ val volume: String,
+ @SerialName("item_number") val itemNumber: Float?,
+) {
+ @Serializable
+ class Branch(
+ @SerialName("branch_id") val branchId: Int?,
+ @SerialName("created_at") val createdAt: String,
+ val teams: List,
+ val user: User,
+ ) {
+ @Serializable
+ class Team(
+ val name: String,
+ )
+
+ @Serializable
+ class User(
+ val username: String,
+ )
+ }
+
+ private fun first(branchId: Int? = null): Branch? {
+ return runCatching { if (branchId != null) branches.first { it.branchId == branchId } else branches.first() }.getOrNull()
+ }
+
+ private fun getTeamName(branchId: Int? = null): String? {
+ return runCatching { first(branchId)!!.teams.first().name }.getOrNull()
+ }
+
+ private fun getUserName(branchId: Int? = null): String? {
+ return runCatching { first(branchId)!!.user.username }.getOrNull()
+ }
+
+ fun toSChapter(slugUrl: String, branchId: Int? = null, isScanUser: Boolean): SChapter = SChapter.create().apply {
+ val chapterName = "Том $volume. Глава $number"
+ name = if (this@Chapter.name.isNullOrBlank()) chapterName else "$chapterName - ${this@Chapter.name}"
+ val branchStr = if (branchId != null) "&branch_id=$branchId" else ""
+ url = "/$slugUrl/chapter?$branchStr&volume=$volume&number=$number"
+ scanlator = getTeamName(branchId) ?: if (isScanUser) getUserName(branchId) else null
+ date_upload = runCatching { LibGroup.simpleDateFormat.parse(first(branchId)!!.createdAt)!!.time }.getOrDefault(0L)
+ chapter_number = itemNumber ?: -1f
+ }
+}
+
+@Serializable
+class Branch(
+ val id: Int,
+)
+
+@Serializable
+class Pages(
+ val pages: List,
+) {
+ @Serializable
+ class MangaPage(
+ val slug: Int,
+ val url: String,
+ )
+
+ fun toPageList(): List = pages.map { Page(it.slug, it.url) }
+}
+
+@Serializable
+class AuthToken(
+ private val token: Token,
+) {
+ @Serializable
+ class Token(
+ val timestamp: Long,
+ @SerialName("expires_in") val expiresIn: Long,
+ @SerialName("token_type") val tokenType: String,
+ @SerialName("access_token") val accessToken: String,
+ )
+
+ fun isExpired(): Boolean {
+ val currentTime = System.currentTimeMillis()
+ val expiresIn = token.timestamp + (token.expiresIn * 1000)
+ return expiresIn < currentTime
+ }
+
+ fun getToken(): String = "${token.tokenType} ${token.accessToken}"
+}
diff --git a/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibUrlActivity.kt b/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibUrlActivity.kt
index 9f6896587..75f099a9a 100644
--- a/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibUrlActivity.kt
+++ b/lib-multisrc/libgroup/src/eu/kanade/tachiyomi/multisrc/libgroup/LibUrlActivity.kt
@@ -19,7 +19,7 @@ class LibUrlActivity : Activity() {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 0) {
- val titleid = pathSegments[0]
+ val titleid = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${LibGroup.PREFIX_SLUG_SEARCH}$titleid")
diff --git a/src/ru/hentailib/src/eu/kanade/tachiyomi/extension/ru/hentailib/HentaiLib.kt b/src/ru/hentailib/src/eu/kanade/tachiyomi/extension/ru/hentailib/HentaiLib.kt
index aae8a14a0..b16b01055 100644
--- a/src/ru/hentailib/src/eu/kanade/tachiyomi/extension/ru/hentailib/HentaiLib.kt
+++ b/src/ru/hentailib/src/eu/kanade/tachiyomi/extension/ru/hentailib/HentaiLib.kt
@@ -5,17 +5,12 @@ import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import eu.kanade.tachiyomi.multisrc.libgroup.LibGroup
-import eu.kanade.tachiyomi.network.POST
-import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
-import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class HentaiLib : LibGroup("HentaiLib", "https://hentailib.me", "ru") {
- override val id: Long = 6425650164840473547
-
private val preferences: SharedPreferences by lazy {
Injekt.get().getSharedPreferences("source_$id", 0x0000)
}
@@ -23,209 +18,17 @@ class HentaiLib : LibGroup("HentaiLib", "https://hentailib.me", "ru") {
private var domain: String = preferences.getString(DOMAIN_TITLE, DOMAIN_DEFAULT)!!
override val baseUrl: String = domain
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- if (csrfToken.isEmpty()) {
- val tokenResponse = client.newCall(popularMangaRequest(page)).execute()
- val resBody = tokenResponse.body.string()
- csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
- }
- val url = super.searchMangaRequest(page, query, filters).url.newBuilder()
- (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
- when (filter) {
- is TagList -> filter.state.forEach { tag ->
- if (tag.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(
- if (tag.isIncluded()) "tags[include][]" else "tags[exclude][]",
- tag.id,
- )
- }
- }
- else -> {}
- }
- }
- return POST(url.toString(), catalogHeaders())
- }
+ override val siteId: Int = 4 // Important in api calls
// Filters
- private class SearchFilter(name: String, val id: String) : Filter.TriState(name)
-
- private class TagList(tags: List) : Filter.Group("Теги", tags)
-
override fun getFilterList(): FilterList {
val filters = super.getFilterList().toMutableList()
- filters.add(4, TagList(getTagList()))
+ if (filters.size > 7) {
+ filters.removeAt(7) // AgeList
+ }
return FilterList(filters)
}
- private fun getTagList() = listOf(
- SearchFilter("3D", "1"),
- SearchFilter("Defloration", "287"),
- SearchFilter("FPP(Вид от первого лица)", "289"),
- SearchFilter("Footfuck", "5"),
- SearchFilter("Handjob", "6"),
- SearchFilter("Lactation", "7"),
- SearchFilter("Living clothes", "284"),
- SearchFilter("Mind break", "9"),
- SearchFilter("Scat", "13"),
- SearchFilter("Selfcest", "286"),
- SearchFilter("Shemale", "220"),
- SearchFilter("Tomboy", "14"),
- SearchFilter("Unbirth", "283"),
- SearchFilter("X-Ray", "15"),
- SearchFilter("Алкоголь", "16"),
- SearchFilter("Анал", "17"),
- SearchFilter("Андроид", "18"),
- SearchFilter("Анилингус", "19"),
- SearchFilter("Анимация (GIF)", "350"),
- SearchFilter("Арт", "20"),
- SearchFilter("Ахэгао", "2"),
- SearchFilter("БДСМ", "22"),
- SearchFilter("Бакуню", "21"),
- SearchFilter("Бара", "293"),
- SearchFilter("Без проникновения", "336"),
- SearchFilter("Без текста", "23"),
- SearchFilter("Без трусиков", "24"),
- SearchFilter("Без цензуры", "25"),
- SearchFilter("Беременность", "26"),
- SearchFilter("Бикини", "27"),
- SearchFilter("Близнецы", "28"),
- SearchFilter("Боди-арт", "29"),
- SearchFilter("Больница", "30"),
- SearchFilter("Большая грудь", "31"),
- SearchFilter("Большая попка", "32"),
- SearchFilter("Борьба", "33"),
- SearchFilter("Буккакэ", "34"),
- SearchFilter("В бассейне", "35"),
- SearchFilter("В ванной", "36"),
- SearchFilter("В государственном учреждении", "37"),
- SearchFilter("В общественном месте", "38"),
- SearchFilter("В очках", "8"),
- SearchFilter("В первый раз", "39"),
- SearchFilter("В транспорте", "40"),
- SearchFilter("Вампиры", "41"),
- SearchFilter("Вибратор", "42"),
- SearchFilter("Втроём", "43"),
- SearchFilter("Гипноз", "44"),
- SearchFilter("Глубокий минет", "45"),
- SearchFilter("Горячий источник", "46"),
- SearchFilter("Групповой секс", "47"),
- SearchFilter("Гуро", "307"),
- SearchFilter("Гяру и Гангуро", "48"),
- SearchFilter("Двойное проникновение", "49"),
- SearchFilter("Девочки-волшебницы", "50"),
- SearchFilter("Девушка-туалет", "51"),
- SearchFilter("Демон", "52"),
- SearchFilter("Дилдо", "53"),
- SearchFilter("Домохозяйка", "54"),
- SearchFilter("Дыра в стене", "55"),
- SearchFilter("Жестокость", "56"),
- SearchFilter("Золотой дождь", "57"),
- SearchFilter("Зомби", "58"),
- SearchFilter("Зоофилия", "351"),
- SearchFilter("Зрелые женщины", "59"),
- SearchFilter("Избиение", "223"),
- SearchFilter("Измена", "60"),
- SearchFilter("Изнасилование", "61"),
- SearchFilter("Инопланетяне", "62"),
- SearchFilter("Инцест", "63"),
- SearchFilter("Исполнение желаний", "64"),
- SearchFilter("Историческое", "65"),
- SearchFilter("Камера", "66"),
- SearchFilter("Кляп", "288"),
- SearchFilter("Колготки", "67"),
- SearchFilter("Косплей", "68"),
- SearchFilter("Кримпай", "3"),
- SearchFilter("Куннилингус", "69"),
- SearchFilter("Купальники", "70"),
- SearchFilter("ЛГБТ", "343"),
- SearchFilter("Латекс и кожа", "71"),
- SearchFilter("Магия", "72"),
- SearchFilter("Маленькая грудь", "73"),
- SearchFilter("Мастурбация", "74"),
- SearchFilter("Медсестра", "221"),
- SearchFilter("Мейдочка", "75"),
- SearchFilter("Мерзкий дядька", "76"),
- SearchFilter("Милф", "77"),
- SearchFilter("Много девушек", "78"),
- SearchFilter("Много спермы", "79"),
- SearchFilter("Молоко", "80"),
- SearchFilter("Монашка", "353"),
- SearchFilter("Монстродевушки", "81"),
- SearchFilter("Монстры", "82"),
- SearchFilter("Мочеиспускание", "83"),
- SearchFilter("На природе", "84"),
- SearchFilter("Наблюдение", "85"),
- SearchFilter("Насекомые", "285"),
- SearchFilter("Небритая киска", "86"),
- SearchFilter("Небритые подмышки", "87"),
- SearchFilter("Нетораре", "88"),
- SearchFilter("Нэтори", "11"),
- SearchFilter("Обмен телами", "89"),
- SearchFilter("Обычный секс", "90"),
- SearchFilter("Огромная грудь", "91"),
- SearchFilter("Огромный член", "92"),
- SearchFilter("Омораси", "93"),
- SearchFilter("Оральный секс", "94"),
- SearchFilter("Орки", "95"),
- SearchFilter("Остановка времени", "296"),
- SearchFilter("Пайзури", "96"),
- SearchFilter("Парень пассив", "97"),
- SearchFilter("Переодевание", "98"),
- SearchFilter("Пирсинг", "308"),
- SearchFilter("Пляж", "99"),
- SearchFilter("Повседневность", "100"),
- SearchFilter("Подвязки", "282"),
- SearchFilter("Подглядывание", "101"),
- SearchFilter("Подчинение", "102"),
- SearchFilter("Похищение", "103"),
- SearchFilter("Превозмогание", "104"),
- SearchFilter("Принуждение", "105"),
- SearchFilter("Прозрачная одежда", "106"),
- SearchFilter("Проституция", "107"),
- SearchFilter("Психические отклонения", "108"),
- SearchFilter("Публично", "109"),
- SearchFilter("Пытки", "224"),
- SearchFilter("Пьяные", "110"),
- SearchFilter("Рабы", "356"),
- SearchFilter("Рабыни", "111"),
- SearchFilter("С Сюжетом", "337"),
- SearchFilter("Сuminside", "4"),
- SearchFilter("Секс-игрушки", "112"),
- SearchFilter("Сексуально возбуждённая", "113"),
- SearchFilter("Сибари", "114"),
- SearchFilter("Спортивная форма", "117"),
- SearchFilter("Спортивное тело", "335"),
- SearchFilter("Спящие", "118"),
- SearchFilter("Страпон", "119"),
- SearchFilter("Суккуб", "120"),
- SearchFilter("Темнокожие", "121"),
- SearchFilter("Тентакли", "122"),
- SearchFilter("Толстушки", "123"),
- SearchFilter("Трагедия", "124"),
- SearchFilter("Трап", "125"),
- SearchFilter("Ужасы", "126"),
- SearchFilter("Униформа", "127"),
- SearchFilter("Учитель и ученик", "352"),
- SearchFilter("Ушастые", "128"),
- SearchFilter("Фантазии", "129"),
- SearchFilter("Фемдом", "130"),
- SearchFilter("Фестиваль", "131"),
- SearchFilter("Фетиш", "132"),
- SearchFilter("Фистинг", "133"),
- SearchFilter("Фурри", "134"),
- SearchFilter("Футанари", "136"),
- SearchFilter("Футанари имеет парня", "137"),
- SearchFilter("Цельный купальник", "138"),
- SearchFilter("Цундэрэ", "139"),
- SearchFilter("Чикан", "140"),
- SearchFilter("Чулки", "141"),
- SearchFilter("Шлюха", "142"),
- SearchFilter("Эксгибиционизм", "143"),
- SearchFilter("Эльф", "144"),
- SearchFilter("Юные", "145"),
- SearchFilter("Яндэрэ", "146"),
- )
-
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
super.setupPreferenceScreen(screen)
EditTextPreference(screen.context).apply {
@@ -234,7 +37,7 @@ class HentaiLib : LibGroup("HentaiLib", "https://hentailib.me", "ru") {
summary = domain
this.setDefaultValue(DOMAIN_DEFAULT)
dialogTitle = DOMAIN_TITLE
- setOnPreferenceChangeListener { _, newValue ->
+ setOnPreferenceChangeListener { _, _ ->
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
true
@@ -243,8 +46,6 @@ class HentaiLib : LibGroup("HentaiLib", "https://hentailib.me", "ru") {
}
companion object {
- const val PREFIX_SLUG_SEARCH = "slug:"
-
private const val DOMAIN_TITLE = "Домен"
private const val DOMAIN_DEFAULT = "https://hentailib.me"
}
diff --git a/src/ru/mangalib/AndroidManifest.xml b/src/ru/mangalib/AndroidManifest.xml
deleted file mode 100644
index f2f30eed6..000000000
--- a/src/ru/mangalib/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ru/mangalib/src/eu/kanade/tachiyomi/extension/ru/mangalib/MangaLib.kt b/src/ru/mangalib/src/eu/kanade/tachiyomi/extension/ru/mangalib/MangaLib.kt
index 6efa9b2ff..69396a558 100644
--- a/src/ru/mangalib/src/eu/kanade/tachiyomi/extension/ru/mangalib/MangaLib.kt
+++ b/src/ru/mangalib/src/eu/kanade/tachiyomi/extension/ru/mangalib/MangaLib.kt
@@ -3,206 +3,41 @@ package eu.kanade.tachiyomi.extension.ru.mangalib
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
-import androidx.preference.ListPreference
-import androidx.preference.PreferenceScreen
+import androidx.preference.EditTextPreference
import eu.kanade.tachiyomi.multisrc.libgroup.LibGroup
-import eu.kanade.tachiyomi.network.POST
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.source.model.FilterList
-import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaLib : LibGroup("MangaLib", "https://mangalib.me", "ru") {
- override val id: Long = 6111047689498497237
-
private val preferences: SharedPreferences by lazy {
- Injekt.get().getSharedPreferences("source_${id}_2", 0x0000)
+ Injekt.get().getSharedPreferences("source_$id", 0x0000)
}
- private val baseOrig: String = "https://mangalib.me"
- private val baseMirr: String = "https://mangalib.org"
- private var domain: String? = preferences.getString(DOMAIN_PREF, baseOrig)
- override val baseUrl: String = domain.toString()
+ private var domain: String = preferences.getString(DOMAIN_PREF, DOMAIN_DEFAULT)!!
+ override val baseUrl: String = domain
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- if (csrfToken.isEmpty()) {
- val tokenResponse = client.newCall(popularMangaRequest(page)).execute()
- val resBody = tokenResponse.body.string()
- csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
- }
- val url = super.searchMangaRequest(page, query, filters).url.newBuilder()
- (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
- when (filter) {
- is AgeList -> filter.state.forEach { age ->
- if (age.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(
- if (age.isIncluded()) "caution[include][]" else "caution[exclude][]",
- age.id,
- )
- }
- }
- is TagList -> filter.state.forEach { tag ->
- if (tag.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(
- if (tag.isIncluded()) "tags[include][]" else "tags[exclude][]",
- tag.id,
- )
- }
- }
- else -> {}
- }
- }
- return POST(url.toString(), catalogHeaders())
- }
+ override val siteId: Int = 1 // Important in api calls
- // Filters
- private class SearchFilter(name: String, val id: String) : Filter.TriState(name)
-
- private class TagList(tags: List) : Filter.Group("Теги", tags)
- private class AgeList(ages: List) : Filter.Group("Возрастное ограничение", ages)
-
- override fun getFilterList(): FilterList {
- val filters = super.getFilterList().toMutableList()
- filters.add(4, TagList(getTagList()))
- filters.add(7, AgeList(getAgeList()))
- return FilterList(filters)
- }
-
- private fun getTagList() = listOf(
- SearchFilter("Азартные игры", "304"),
- SearchFilter("Алхимия", "225"),
- SearchFilter("Ангелы", "226"),
- SearchFilter("Антигерой", "175"),
- SearchFilter("Антиутопия", "227"),
- SearchFilter("Апокалипсис", "228"),
- SearchFilter("Армия", "229"),
- SearchFilter("Артефакты", "230"),
- SearchFilter("Боги", "215"),
- SearchFilter("Бои на мечах", "231"),
- SearchFilter("Борьба за власть", "231"),
- SearchFilter("Брат и сестра", "233"),
- SearchFilter("Будущее", "234"),
- SearchFilter("Ведьма", "338"),
- SearchFilter("Вестерн", "235"),
- SearchFilter("Видеоигры", "185"),
- SearchFilter("Виртуальная реальность", "195"),
- SearchFilter("Владыка демонов", "236"),
- SearchFilter("Военные", "179"),
- SearchFilter("Война", "237"),
- SearchFilter("Волшебники / маги", "281"),
- SearchFilter("Волшебные существа", "239"),
- SearchFilter("Воспоминания из другого мира", "240"),
- SearchFilter("Выживание", "193"),
- SearchFilter("ГГ женщина", "243"),
- SearchFilter("ГГ имба", "291"),
- SearchFilter("ГГ мужчина", "244"),
- SearchFilter("Геймеры", "241"),
- SearchFilter("Гильдии", "242"),
- SearchFilter("Глупый ГГ", "297"),
- SearchFilter("Гоблины", "245"),
- SearchFilter("Горничные", "169"),
- SearchFilter("Гяру", "178"),
- SearchFilter("Демоны", "151"),
- SearchFilter("Драконы", "246"),
- SearchFilter("Дружба", "247"),
- SearchFilter("Жестокий мир", "249"),
- SearchFilter("Животные компаньоны", "250"),
- SearchFilter("Завоевание мира", "251"),
- SearchFilter("Зверолюди", "162"),
- SearchFilter("Злые духи", "252"),
- SearchFilter("Зомби", "149"),
- SearchFilter("Игровые элементы", "253"),
- SearchFilter("Империи", "254"),
- SearchFilter("Квесты", "255"),
- SearchFilter("Космос", "256"),
- SearchFilter("Кулинария", "152"),
- SearchFilter("Культивация", "160"),
- SearchFilter("Легендарное оружие", "257"),
- SearchFilter("Лоли", "187"),
- SearchFilter("Магическая академия", "258"),
- SearchFilter("Магия", "168"),
- SearchFilter("Мафия", "172"),
- SearchFilter("Медицина", "153"),
- SearchFilter("Месть", "259"),
- SearchFilter("Монстр Девушки", "188"),
- SearchFilter("Монстры", "189"),
- SearchFilter("Музыка", "190"),
- SearchFilter("Навыки / способности", "260"),
- SearchFilter("Насилие / жестокость", "262"),
- SearchFilter("Наёмники", "261"),
- SearchFilter("Нежить", "263"),
- SearchFilter("Ниндая", "180"),
- SearchFilter("Обратный Гарем", "191"),
- SearchFilter("Огнестрельное оружие", "264"),
- SearchFilter("Офисные Работники", "181"),
- SearchFilter("Пародия", "265"),
- SearchFilter("Пираты", "340"),
- SearchFilter("Подземелья", "266"),
- SearchFilter("Политика", "267"),
- SearchFilter("Полиция", "182"),
- SearchFilter("Преступники / Криминал", "186"),
- SearchFilter("Призраки / Духи", "177"),
- SearchFilter("Путешествие во времени", "194"),
- SearchFilter("Разумные расы", "268"),
- SearchFilter("Ранги силы", "248"),
- SearchFilter("Реинкарнация", "148"),
- SearchFilter("Роботы", "269"),
- SearchFilter("Рыцари", "270"),
- SearchFilter("Самураи", "183"),
- SearchFilter("Система", "271"),
- SearchFilter("Скрытие личности", "273"),
- SearchFilter("Спасение мира", "274"),
- SearchFilter("Спортивное тело", "334"),
- SearchFilter("Средневековье", "173"),
- SearchFilter("Стимпанк", "272"),
- SearchFilter("Супергерои", "275"),
- SearchFilter("Традиционные игры", "184"),
- SearchFilter("Умный ГГ", "302"),
- SearchFilter("Учитель / ученик", "276"),
- SearchFilter("Философия", "277"),
- SearchFilter("Хикикомори", "166"),
- SearchFilter("Холодное оружие", "278"),
- SearchFilter("Шантаж", "279"),
- SearchFilter("Эльфы", "216"),
- SearchFilter("Якудза", "164"),
- SearchFilter("Япония", "280"),
-
- )
-
- private fun getAgeList() = listOf(
- SearchFilter("Отсутствует", "0"),
- SearchFilter("16+", "1"),
- SearchFilter("18+", "2"),
- )
-
- companion object {
- const val PREFIX_SLUG_SEARCH = "slug:"
- private const val DOMAIN_PREF = "MangaLibDomain"
- private const val DOMAIN_PREF_Title = "Выбор домена"
- }
-
- override fun setupPreferenceScreen(screen: PreferenceScreen) {
+ override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
super.setupPreferenceScreen(screen)
- ListPreference(screen.context).apply {
+ EditTextPreference(screen.context).apply {
key = DOMAIN_PREF
- title = DOMAIN_PREF_Title
- entries = arrayOf("Основной (mangalib.me)", "Зеркало (mangalib.org)")
- entryValues = arrayOf(baseOrig, baseMirr)
- summary = "%s"
- setDefaultValue(baseOrig)
- setOnPreferenceChangeListener { _, newValue ->
- try {
- val res = preferences.edit().putString(DOMAIN_PREF, newValue as String).commit()
- val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
- Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
- res
- } catch (e: Exception) {
- e.printStackTrace()
- false
- }
+ this.title = DOMAIN_TITLE
+ summary = domain
+ this.setDefaultValue(DOMAIN_DEFAULT)
+ dialogTitle = DOMAIN_TITLE
+ setOnPreferenceChangeListener { _, _ ->
+ val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
+ Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
+ true
}
}.let(screen::addPreference)
}
+
+ companion object {
+ private const val DOMAIN_PREF = "MangaLibDomain"
+ private const val DOMAIN_TITLE = "Домен"
+ private const val DOMAIN_DEFAULT = "https://test-front.mangalib.me"
+ }
}
diff --git a/src/ru/yaoilib/AndroidManifest.xml b/src/ru/yaoilib/AndroidManifest.xml
deleted file mode 100644
index 3c5f85757..000000000
--- a/src/ru/yaoilib/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ru/yaoilib/build.gradle b/src/ru/yaoilib/build.gradle
index fbbcd3017..ee4efd366 100644
--- a/src/ru/yaoilib/build.gradle
+++ b/src/ru/yaoilib/build.gradle
@@ -2,7 +2,7 @@ ext {
extName = 'YaoiLib'
extClass = '.YaoiLib'
themePkg = 'libgroup'
- baseUrl = 'https://v2.slashlib.me'
+ baseUrl = 'https://slashlib.me'
overrideVersionCode = 4
isNsfw = true
}
diff --git a/src/ru/yaoilib/src/eu/kanade/tachiyomi/extension/ru/yaoilib/YaoiLib.kt b/src/ru/yaoilib/src/eu/kanade/tachiyomi/extension/ru/yaoilib/YaoiLib.kt
index 9e67ad104..97e43a929 100644
--- a/src/ru/yaoilib/src/eu/kanade/tachiyomi/extension/ru/yaoilib/YaoiLib.kt
+++ b/src/ru/yaoilib/src/eu/kanade/tachiyomi/extension/ru/yaoilib/YaoiLib.kt
@@ -5,14 +5,10 @@ import android.content.SharedPreferences
import android.widget.Toast
import androidx.preference.EditTextPreference
import eu.kanade.tachiyomi.multisrc.libgroup.LibGroup
-import eu.kanade.tachiyomi.network.POST
-import eu.kanade.tachiyomi.source.model.Filter
-import eu.kanade.tachiyomi.source.model.FilterList
-import okhttp3.Request
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
-class YaoiLib : LibGroup("YaoiLib", "https://v2.slashlib.me", "ru") {
+class YaoiLib : LibGroup("YaoiLib", "https://slashlib.me", "ru") {
private val preferences: SharedPreferences by lazy {
Injekt.get().getSharedPreferences("source_$id", 0x0000)
@@ -21,156 +17,7 @@ class YaoiLib : LibGroup("YaoiLib", "https://v2.slashlib.me", "ru") {
private var domain: String = preferences.getString(DOMAIN_TITLE, DOMAIN_DEFAULT)!!
override val baseUrl: String = domain
- override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
- if (csrfToken.isEmpty()) {
- val tokenResponse = client.newCall(popularMangaRequest(page)).execute()
- val resBody = tokenResponse.body.string()
- csrfToken = "_token\" content=\"(.*)\"".toRegex().find(resBody)!!.groups[1]!!.value
- }
- val url = super.searchMangaRequest(page, query, filters).url.newBuilder()
- (if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
- when (filter) {
- is AgeList -> filter.state.forEach { age ->
- if (age.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(
- if (age.isIncluded()) "caution[include][]" else "caution[exclude][]",
- age.id,
- )
- }
- }
- is TagList -> filter.state.forEach { tag ->
- if (tag.state != Filter.TriState.STATE_IGNORE) {
- url.addQueryParameter(
- if (tag.isIncluded()) "tags[include][]" else "tags[exclude][]",
- tag.id,
- )
- }
- }
- else -> {}
- }
- }
- return POST(url.toString(), catalogHeaders())
- }
-
- // Filters
- private class SearchFilter(name: String, val id: String) : Filter.TriState(name)
-
- private class TagList(tags: List) : Filter.Group("Теги", tags)
- private class AgeList(ages: List) : Filter.Group("Возрастное ограничение", ages)
-
- override fun getFilterList(): FilterList {
- val filters = super.getFilterList().toMutableList()
- filters.add(4, TagList(getTagList()))
- filters.add(7, AgeList(getAgeList()))
- return FilterList(filters)
- }
-
- private fun getTagList() = listOf(
- SearchFilter("Азартные игры", "304"),
- SearchFilter("Алхимия", "225"),
- SearchFilter("Ангелы", "226"),
- SearchFilter("Антигерой", "175"),
- SearchFilter("Антиутопия", "227"),
- SearchFilter("Апокалипсис", "228"),
- SearchFilter("Армия", "229"),
- SearchFilter("Артефакты", "230"),
- SearchFilter("Боги", "215"),
- SearchFilter("Бои на мечах", "231"),
- SearchFilter("Борьба за власть", "231"),
- SearchFilter("Брат и сестра", "233"),
- SearchFilter("Будущее", "234"),
- SearchFilter("Ведьма", "338"),
- SearchFilter("Вестерн", "235"),
- SearchFilter("Видеоигры", "185"),
- SearchFilter("Виртуальная реальность", "195"),
- SearchFilter("Владыка демонов", "236"),
- SearchFilter("Военные", "179"),
- SearchFilter("Война", "237"),
- SearchFilter("Волшебники / маги", "281"),
- SearchFilter("Волшебные существа", "239"),
- SearchFilter("Воспоминания из другого мира", "240"),
- SearchFilter("Выживание", "193"),
- SearchFilter("ГГ женщина", "243"),
- SearchFilter("ГГ имба", "291"),
- SearchFilter("ГГ мужчина", "244"),
- SearchFilter("Геймеры", "241"),
- SearchFilter("Гильдии", "242"),
- SearchFilter("Глупый ГГ", "297"),
- SearchFilter("Гоблины", "245"),
- SearchFilter("Горничные", "169"),
- SearchFilter("Гяру", "178"),
- SearchFilter("Демоны", "151"),
- SearchFilter("Драконы", "246"),
- SearchFilter("Дружба", "247"),
- SearchFilter("Жестокий мир", "249"),
- SearchFilter("Животные компаньоны", "250"),
- SearchFilter("Завоевание мира", "251"),
- SearchFilter("Зверолюди", "162"),
- SearchFilter("Злые духи", "252"),
- SearchFilter("Зомби", "149"),
- SearchFilter("Игровые элементы", "253"),
- SearchFilter("Империи", "254"),
- SearchFilter("Квесты", "255"),
- SearchFilter("Космос", "256"),
- SearchFilter("Кулинария", "152"),
- SearchFilter("Культивация", "160"),
- SearchFilter("Легендарное оружие", "257"),
- SearchFilter("Лоли", "187"),
- SearchFilter("Магическая академия", "258"),
- SearchFilter("Магия", "168"),
- SearchFilter("Мафия", "172"),
- SearchFilter("Медицина", "153"),
- SearchFilter("Месть", "259"),
- SearchFilter("Монстр Девушки", "188"),
- SearchFilter("Монстры", "189"),
- SearchFilter("Музыка", "190"),
- SearchFilter("Навыки / способности", "260"),
- SearchFilter("Насилие / жестокость", "262"),
- SearchFilter("Наёмники", "261"),
- SearchFilter("Нежить", "263"),
- SearchFilter("Ниндая", "180"),
- SearchFilter("Обратный Гарем", "191"),
- SearchFilter("Огнестрельное оружие", "264"),
- SearchFilter("Офисные Работники", "181"),
- SearchFilter("Пародия", "265"),
- SearchFilter("Пираты", "340"),
- SearchFilter("Подземелья", "266"),
- SearchFilter("Политика", "267"),
- SearchFilter("Полиция", "182"),
- SearchFilter("Преступники / Криминал", "186"),
- SearchFilter("Призраки / Духи", "177"),
- SearchFilter("Путешествие во времени", "194"),
- SearchFilter("Разумные расы", "268"),
- SearchFilter("Ранги силы", "248"),
- SearchFilter("Реинкарнация", "148"),
- SearchFilter("Роботы", "269"),
- SearchFilter("Рыцари", "270"),
- SearchFilter("Самураи", "183"),
- SearchFilter("Система", "271"),
- SearchFilter("Скрытие личности", "273"),
- SearchFilter("Спасение мира", "274"),
- SearchFilter("Спортивное тело", "334"),
- SearchFilter("Средневековье", "173"),
- SearchFilter("Стимпанк", "272"),
- SearchFilter("Супергерои", "275"),
- SearchFilter("Традиционные игры", "184"),
- SearchFilter("Умный ГГ", "302"),
- SearchFilter("Учитель / ученик", "276"),
- SearchFilter("Философия", "277"),
- SearchFilter("Хикикомори", "166"),
- SearchFilter("Холодное оружие", "278"),
- SearchFilter("Шантаж", "279"),
- SearchFilter("Эльфы", "216"),
- SearchFilter("Якудза", "164"),
- SearchFilter("Япония", "280"),
-
- )
-
- private fun getAgeList() = listOf(
- SearchFilter("Отсутствует", "0"),
- SearchFilter("16+", "1"),
- SearchFilter("18+", "2"),
- )
+ override val siteId: Int = 2 // Important in api calls
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
super.setupPreferenceScreen(screen)
@@ -180,7 +27,7 @@ class YaoiLib : LibGroup("YaoiLib", "https://v2.slashlib.me", "ru") {
summary = domain
this.setDefaultValue(DOMAIN_DEFAULT)
dialogTitle = DOMAIN_TITLE
- setOnPreferenceChangeListener { _, newValue ->
+ setOnPreferenceChangeListener { _, _ ->
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
true
@@ -189,9 +36,7 @@ class YaoiLib : LibGroup("YaoiLib", "https://v2.slashlib.me", "ru") {
}
companion object {
- const val PREFIX_SLUG_SEARCH = "slug:"
-
private const val DOMAIN_TITLE = "Домен"
- private const val DOMAIN_DEFAULT = "https://v2.slashlib.me"
+ private const val DOMAIN_DEFAULT = "https://test-front.slashlib.me"
}
}