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