Compare commits
No commits in common. "988d1b04af44267b8c934a230cb88e1d746783b9" and "e91f02af0291608767ef2369a70e39001bed3d64" have entirely different histories.
988d1b04af
...
e91f02af02
@ -68,7 +68,7 @@ small, just do a normal full clone instead.**
|
||||
1. Do a partial clone.
|
||||
```bash
|
||||
git clone --filter=blob:none --sparse <fork-repo-url>
|
||||
cd extensions-source/
|
||||
cd extensions/
|
||||
```
|
||||
2. Configure sparse checkout.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
3
gradlew
generated
vendored
@ -86,7 +86,8 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 3
|
||||
|
@ -37,8 +37,6 @@ abstract class BlogTruyen(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 4
|
||||
baseVersionCode = 3
|
||||
|
@ -814,8 +814,8 @@ abstract class GalleryAdults(
|
||||
val tags = mutableListOf<Genre>()
|
||||
runBlocking {
|
||||
val jobsPool = mutableListOf<Job>()
|
||||
// Get first 5 pages
|
||||
(1..5).forEach { page ->
|
||||
// Get first 3 pages
|
||||
(1..3).forEach { page ->
|
||||
jobsPool.add(
|
||||
launchIO {
|
||||
runCatching {
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 28
|
||||
baseVersionCode = 26
|
||||
|
@ -177,18 +177,17 @@ abstract class GroupLe(
|
||||
"div#tab-description .manga-description",
|
||||
).text()
|
||||
manga.status = when {
|
||||
(
|
||||
document.html()
|
||||
.contains("Запрещена публикация произведения по копирайту") || document.html()
|
||||
.contains("ЗАПРЕЩЕНА К ПУБЛИКАЦИИ НА ТЕРРИТОРИИ РФ!")
|
||||
) && document.select("div.chapters").isEmpty() -> SManga.LICENSED
|
||||
infoElement.html().contains("<b>Сингл") -> SManga.COMPLETED
|
||||
infoElement.html()
|
||||
.contains("Запрещена публикация произведения по копирайту") || infoElement.html()
|
||||
.contains("ЗАПРЕЩЕНА К ПУБЛИКАЦИИ НА ТЕРРИТОРИИ РФ!") -> SManga.LICENSED
|
||||
infoElement.html().contains("<b>Сингл</b>") -> SManga.COMPLETED
|
||||
else ->
|
||||
when (infoElement.selectFirst("span.badge:contains(выпуск)")?.text()) {
|
||||
"выпуск продолжается" -> SManga.ONGOING
|
||||
"выпуск начат" -> SManga.ONGOING
|
||||
"выпуск завершён" -> if (infoElement.selectFirst("span.badge:contains(переведено)")?.text()?.isNotEmpty() == true) SManga.COMPLETED else SManga.PUBLISHING_FINISHED
|
||||
"выпуск приостановлен" -> SManga.ON_HIATUS
|
||||
when (infoElement.select("p:contains(Перевод:) span").first()?.text()) {
|
||||
"продолжается" -> SManga.ONGOING
|
||||
"начат" -> SManga.ONGOING
|
||||
"переведено" -> SManga.COMPLETED
|
||||
"завершён" -> SManga.COMPLETED
|
||||
"приостановлен" -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
@ -214,9 +213,15 @@ abstract class GroupLe(
|
||||
|
||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
if (document.select(".user-avatar").isEmpty() &&
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||
if ((
|
||||
document.select(".expandable.hide-dn").isNotEmpty() && document.select(".user-avatar")
|
||||
.isEmpty() && document.toString()
|
||||
.contains("current_user_country_code = 'RU'")
|
||||
) || (
|
||||
document.select("img.logo")
|
||||
.first()?.attr("title")
|
||||
?.contains("Allhentai") == true && document.select(".user-avatar").isEmpty()
|
||||
)
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
@ -308,21 +313,19 @@ abstract class GroupLe(
|
||||
|
||||
val html = document.html()
|
||||
|
||||
if (document.select(".user-avatar").isEmpty() &&
|
||||
document.title().run { contains("AllHentai") || contains("MintManga") || contains("МинтМанга") }
|
||||
val readerMark = "rm_h.readerDoInit(["
|
||||
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
|
||||
val readerMark = when {
|
||||
html.contains("rm_h.readerDoInit([") -> "rm_h.readerDoInit(["
|
||||
html.contains("rm_h.readerInit([") -> "rm_h.readerInit(["
|
||||
!response.request.url.toString().contains(baseUrl) -> {
|
||||
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
||||
if (!html.contains(readerMark)) {
|
||||
if (document.select(".input-lg").isNotEmpty() || (
|
||||
document.select(".user-avatar")
|
||||
.isEmpty() && document.select("img.logo").first()?.attr("title")
|
||||
?.contains("Allhentai") == true
|
||||
)
|
||||
) {
|
||||
throw Exception("Для просмотра контента необходима авторизация через WebView\uD83C\uDF0E")
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Дизайн сайта обновлен, для дальнейшей работы необходимо обновление дополнения")
|
||||
if (!response.request.url.toString().contains(baseUrl)) {
|
||||
throw Exception("Не удалось загрузить главу. Url: ${response.request.url}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 6
|
||||
baseVersionCode = 5
|
||||
|
@ -33,7 +33,7 @@ abstract class Iken(
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
private var genres = emptyList<Pair<String, String>>()
|
||||
protected val titleCache by lazy {
|
||||
private val titleCache by lazy {
|
||||
val response = client.newCall(GET("$baseUrl/api/query?perPage=9999", headers)).execute()
|
||||
val data = response.parseAs<SearchResponse>()
|
||||
|
||||
@ -53,9 +53,11 @@ abstract class Iken(
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val slugs = document.select("div:contains(Popular) + div.swiper div.manga-swipe > a")
|
||||
.map { it.absUrl("href").substringAfterLast("/series/") }
|
||||
|
||||
val entries = document.select("aside a:has(img)").mapNotNull {
|
||||
titleCache[it.absUrl("href").substringAfter("series/")]?.toSManga()
|
||||
val entries = slugs.mapNotNull {
|
||||
titleCache[it]?.toSManga()
|
||||
}
|
||||
|
||||
return MangasPage(entries, false)
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 12
|
||||
baseVersionCode = 9
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
@ -206,22 +205,16 @@ abstract class Keyoapp(
|
||||
}
|
||||
|
||||
// Details
|
||||
protected open val descriptionSelector: String = "div:containsOwn(Synopsis) ~ div"
|
||||
protected open val statusSelector: String = "div:has(span:containsOwn(Status)) ~ div"
|
||||
protected open val authorSelector: String = "div:has(span:containsOwn(Author)) ~ div"
|
||||
protected open val artistSelector: String = "div:has(span:containsOwn(Artist)) ~ div"
|
||||
protected open val genreSelector: String = "div:has(span:containsOwn(Type)) ~ div"
|
||||
protected open val dateSelector: String = ".text-xs"
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
title = document.selectFirst("div.grid > h1")!!.text()
|
||||
thumbnail_url = document.getImageUrl("div[class*=photoURL]")
|
||||
description = document.selectFirst(descriptionSelector)?.text()
|
||||
status = document.selectFirst(statusSelector).parseStatus()
|
||||
author = document.selectFirst(authorSelector)?.text()
|
||||
artist = document.selectFirst(artistSelector)?.text()
|
||||
description = document.selectFirst("div.grid > div.overflow-hidden > p")?.text()
|
||||
status = document.selectFirst("div[alt=Status]").parseStatus()
|
||||
author = document.selectFirst("div[alt=Author]")?.text()
|
||||
artist = document.selectFirst("div[alt=Artist]")?.text()
|
||||
genre = buildList {
|
||||
document.selectFirst(genreSelector)?.text()?.replaceFirstChar {
|
||||
document.selectFirst("div[alt='Series Type']")?.text()?.replaceFirstChar {
|
||||
if (it.isLowerCase()) {
|
||||
it.titlecase(
|
||||
Locale.getDefault(),
|
||||
@ -234,7 +227,7 @@ abstract class Keyoapp(
|
||||
}.joinToString()
|
||||
}
|
||||
|
||||
protected fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
private fun Element?.parseStatus(): Int = when (this?.text()?.lowercase()) {
|
||||
"ongoing" -> SManga.ONGOING
|
||||
"dropped" -> SManga.CANCELLED
|
||||
"paused" -> SManga.ON_HIATUS
|
||||
@ -254,7 +247,7 @@ abstract class Keyoapp(
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a[href]")!!.attr("href"))
|
||||
name = element.selectFirst(".text-sm")!!.text()
|
||||
element.selectFirst(dateSelector)?.run {
|
||||
element.selectFirst(".text-xs")?.run {
|
||||
date_upload = text().trim().parseDate()
|
||||
}
|
||||
if (element.select("img[src*=Coin.svg]").isNotEmpty()) {
|
||||
@ -315,12 +308,6 @@ abstract class Keyoapp(
|
||||
protected open fun Element.getImageUrl(selector: String): String? {
|
||||
return this.selectFirst(selector)?.let { element ->
|
||||
IMG_REGEX.find(element.attr("style"))?.groups?.get("url")?.value
|
||||
?.toHttpUrlOrNull()?.let {
|
||||
it.newBuilder()
|
||||
.setQueryParameter("w", "480") // Keyoapp returns the dynamic size of the thumbnail to any size
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -338,6 +325,8 @@ abstract class Keyoapp(
|
||||
|
||||
private fun String.parseRelativeDate(): Long {
|
||||
val now = Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 34
|
||||
baseVersionCode = 32
|
||||
|
@ -34,6 +34,7 @@ import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
@ -64,46 +65,43 @@ abstract class LibGroup(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
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 var bearerToken: String? = null
|
||||
|
||||
private var userId: Int? = null
|
||||
|
||||
abstract val siteId: Int // Important in api calls
|
||||
|
||||
private val apiDomain: String = preferences.getString(API_DOMAIN_PREF, API_DOMAIN_DEFAULT).toString()
|
||||
private val apiDomain: String = "https://api.lib.social"
|
||||
|
||||
override val client by lazy {
|
||||
network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.rateLimitHost(apiDomain.toHttpUrl(), 1)
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 1)
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.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︎ и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
|
||||
}
|
||||
return@addInterceptor response
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(3)
|
||||
.rateLimitHost(apiDomain.toHttpUrl(), 1)
|
||||
.connectTimeout(5, TimeUnit.MINUTES)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(15, TimeUnit.SECONDS)
|
||||
.addInterceptor(::checkForToken)
|
||||
.addInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.code == 419) {
|
||||
throw IOException("HTTP error ${response.code}. Проверьте сайт. Для завершения авторизации необходимо перезапустить приложение с полной остановкой.")
|
||||
}
|
||||
.build()
|
||||
}
|
||||
if (response.code == 404) {
|
||||
throw IOException("HTTP error ${response.code}. Проверьте сайт. Попробуйте авторизоваться через WebView\uD83C\uDF0E︎ и обновите список. Для завершения авторизации может потребоваться перезапустить приложение с полной остановкой.")
|
||||
}
|
||||
return@addInterceptor response
|
||||
}
|
||||
.build()
|
||||
|
||||
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/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 imageHeader() = Headers.Builder().apply {
|
||||
add("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
add("Referer", baseUrl)
|
||||
}.build()
|
||||
|
||||
private var _constants: Constants? = null
|
||||
private fun getConstants(): Constants? {
|
||||
if (_constants == null) {
|
||||
@ -375,25 +373,10 @@ abstract class LibGroup(
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun checkImage(url: String): Boolean {
|
||||
val getUrlHead = Request.Builder().url(url).head().headers(imageHeader()).build()
|
||||
val response = client.newCall(getUrlHead).execute()
|
||||
return response.isSuccessful && (response.header("content-length", "0")?.toInt()!! > 600)
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
if (page.imageUrl != null) {
|
||||
return Observable.just(page.imageUrl)
|
||||
}
|
||||
if (isServer() == "auto") {
|
||||
for (serverApi in IMG_SERVERS.slice(1 until IMG_SERVERS.size)) {
|
||||
val server = getConstants()?.getServer(serverApi, siteId)?.url
|
||||
val imageUrl = "$server${page.url}"
|
||||
if (checkImage(imageUrl)) {
|
||||
return Observable.just(imageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
val server = getConstants()?.getServer(isServer(), siteId)?.url ?: throw Exception("Ошибка получения сервера изображений")
|
||||
return Observable.just("$server${page.url}")
|
||||
}
|
||||
@ -401,7 +384,13 @@ abstract class LibGroup(
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, imageHeader())
|
||||
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<MangasPage> {
|
||||
@ -576,7 +565,6 @@ abstract class LibGroup(
|
||||
companion object {
|
||||
const val PREFIX_SLUG_SEARCH = "slug:"
|
||||
private const val SERVER_PREF = "MangaLibImageServer"
|
||||
private val IMG_SERVERS = arrayOf("auto", "main", "secondary", "compress")
|
||||
|
||||
private const val SORTING_PREF = "MangaLibSorting"
|
||||
private const val SORTING_PREF_TITLE = "Способ выбора переводчиков"
|
||||
@ -590,16 +578,12 @@ abstract class LibGroup(
|
||||
private const val LANGUAGE_PREF = "MangaLibTitleLanguage"
|
||||
private const val LANGUAGE_PREF_TITLE = "Выбор языка на обложке"
|
||||
|
||||
private const val API_DOMAIN_PREF = "MangaLibApiDomain"
|
||||
private const val API_DOMAIN_TITLE = "Выбор домена API"
|
||||
private const val API_DOMAIN_DEFAULT = "https://api.imglib.info"
|
||||
|
||||
private const val TOKEN_STORE = "TokenStore"
|
||||
|
||||
val simpleDateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.US) }
|
||||
}
|
||||
|
||||
private fun isServer(): String = preferences.getString(SERVER_PREF, "compress")!!
|
||||
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)
|
||||
@ -607,18 +591,12 @@ abstract class LibGroup(
|
||||
val serverPref = ListPreference(screen.context).apply {
|
||||
key = SERVER_PREF
|
||||
title = "Сервер изображений"
|
||||
entries = arrayOf("Автовыбор", "Первый", "Второй", "Сжатия")
|
||||
entryValues = IMG_SERVERS
|
||||
summary = "%s \n\n" +
|
||||
"По умолчанию в приложении и на сайте «Сжатия» - самый стабильный и быстрый. \n\n" +
|
||||
"«Автовыбор» - проходит по всем серверам и показывает только загруженную картинку. \nМожет происходить медленно. \n\n" +
|
||||
entries = arrayOf("Первый", "Второй", "Сжатия")
|
||||
entryValues = arrayOf("main", "secondary", "compress")
|
||||
summary = "%s \n\nВыбор приоритетного сервера изображений. \n" +
|
||||
"По умолчанию «Первый». \n\n" +
|
||||
"ⓘВыбор другого сервера помогает при ошибках и медленной загрузки изображений глав."
|
||||
setDefaultValue("compress")
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val warning = "Для смены сервера: Настройки -> Дополнительно -> Очистить кэш глав"
|
||||
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
setDefaultValue("main")
|
||||
}
|
||||
|
||||
val sortingPref = ListPreference(screen.context).apply {
|
||||
@ -651,30 +629,11 @@ abstract class LibGroup(
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val domainApiPref = ListPreference(screen.context).apply {
|
||||
key = API_DOMAIN_PREF
|
||||
title = API_DOMAIN_TITLE
|
||||
entries = arrayOf("Официальное приложение (api.imglib.info)", "Основной (api.lib.social)", "Резервный (api.mangalib.me)", "Резервный 2 (api2.mangalib.me)")
|
||||
entryValues = arrayOf(API_DOMAIN_DEFAULT, "https://api.lib.social", "https://api.mangalib.me", "https://api2.mangalib.me")
|
||||
summary = "%s" +
|
||||
"\n\nВыбор домена API, используемого для работы приложения." +
|
||||
"\n\nПо умолчанию «Официальное приложение»" +
|
||||
"\n\nⓘВы не увидите его нигде глазами, но источник должен начать работать стибильнее."
|
||||
setDefaultValue(API_DOMAIN_DEFAULT)
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val warning = "Для смены домена необходимо перезапустить приложение с полной остановкой."
|
||||
Toast.makeText(screen.context, warning, Toast.LENGTH_LONG).show()
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(serverPref)
|
||||
screen.addPreference(sortingPref)
|
||||
screen.addPreference(screen.editTextPreference(TRANSLATORS_TITLE, TRANSLATORS_DEFAULT, groupTranslates()))
|
||||
screen.addPreference(scanlatorUsername)
|
||||
screen.addPreference(titleLanguagePref)
|
||||
screen.addPreference(domainApiPref)
|
||||
}
|
||||
private fun PreferenceScreen.editTextPreference(title: String, default: String, value: String): androidx.preference.EditTextPreference {
|
||||
return androidx.preference.EditTextPreference(context).apply {
|
||||
|
@ -45,7 +45,7 @@ class Constants(
|
||||
)
|
||||
|
||||
fun getServer(isServers: String?, siteId: Int): ImageServer =
|
||||
if (!isServers.isNullOrBlank() and (isServers != "auto")) {
|
||||
if (!isServers.isNullOrBlank()) {
|
||||
imageServers.first { it.id == isServers && it.siteIds.contains(siteId) }
|
||||
} else {
|
||||
imageServers.first { it.siteIds.contains(siteId) }
|
||||
|
@ -1,5 +0,0 @@
|
||||
plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
Before Width: | Height: | Size: 8.8 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 45 KiB |
@ -1,5 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.machinetranslations
|
||||
|
||||
class MachineTranslationsFactoryUtils
|
||||
|
||||
data class Language(val lang: String, val target: String = lang, val origin: String = "en")
|
@ -46,8 +46,6 @@ abstract class MangaEsp(
|
||||
|
||||
protected open val seriesPath = "/ver"
|
||||
|
||||
protected open val useApiSearch = false
|
||||
|
||||
override val client: OkHttpClient = network.client.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
@ -64,9 +62,7 @@ abstract class MangaEsp(
|
||||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||
.additionalParse()
|
||||
.map { it.toSManga(seriesPath) }
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
@ -76,9 +72,7 @@ abstract class MangaEsp(
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LastUpdatesDto>(response.body.string())
|
||||
|
||||
val mangas = responseData.response
|
||||
.additionalParse()
|
||||
.map { it.toSManga(seriesPath) }
|
||||
val mangas = responseData.response.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
@ -99,33 +93,20 @@ abstract class MangaEsp(
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (useApiSearch) {
|
||||
GET("$apiBaseUrl$apiPath/comics", headers)
|
||||
} else {
|
||||
GET("$baseUrl/comics", headers)
|
||||
}
|
||||
}
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/comics", headers)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
protected open fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
comicsList = if (useApiSearch) {
|
||||
json.decodeFromString<List<SeriesDto>>(response.body.string()).toMutableList()
|
||||
} else {
|
||||
val script = response.asJsoup().select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
|
||||
}.additionalParse().toMutableList()
|
||||
val document = response.asJsoup()
|
||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson).toMutableList()
|
||||
return parseComicsList(page, query, filters)
|
||||
}
|
||||
|
||||
protected open fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
|
||||
return this
|
||||
}
|
||||
|
||||
private var filteredList = mutableListOf<SeriesDto>()
|
||||
|
||||
protected open fun parseComicsList(page: Int, query: String, filterList: FilterList): MangasPage {
|
||||
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 29
|
||||
baseVersionCode = 28
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:randomua"))
|
||||
|
@ -50,7 +50,7 @@ abstract class MangaHub(
|
||||
private var baseApiUrl = "https://api.mghcdn.com"
|
||||
private var baseCdnUrl = "https://imgx.mghcdn.com"
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.setRandomUserAgent(
|
||||
userAgentType = UserAgentType.DESKTOP,
|
||||
filterInclude = listOf("chrome"),
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 2
|
||||
baseVersionCode = 1
|
||||
|
@ -22,8 +22,6 @@ abstract class MangaReader : HttpSource(), ConfigurableSource {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
final override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
||||
|
||||
final override fun popularMangaParse(response: Response) = searchMangaParse(response)
|
||||
|
@ -30,5 +30,3 @@ project_filter_warning=NOTE: Can't be used with other filter!
|
||||
project_filter_name=%s Project List page
|
||||
pref_dynamic_url_title=Automatically update dynamic URLs
|
||||
pref_dynamic_url_summary=Automatically update random numbers in manga URLs.\nHelps mitigating HTTP 404 errors during update and "in library" marks when browsing.\nNote: This setting may require clearing database in advanced settings and migrating all manga to the same source.
|
||||
pref_hide_paid_chapters_title=Hide chapters which require a purchase
|
||||
pref_hide_paid_chapters_summary=Hide chapters which must be purchased using coins.\nYou might want to disable this if you want to be notified of paid chapters so that you can go purchase them.
|
||||
|
@ -1,35 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangathemesia
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
|
||||
class MangaThemesiaPaidChapterHelper(
|
||||
private val hidePaidChaptersPrefKey: String = "pref_hide_paid_chapters",
|
||||
private val lockedChapterSelector: String = "a[data-bs-target='#lockedChapterModal']",
|
||||
) {
|
||||
fun addHidePaidChaptersPreferenceToScreen(screen: PreferenceScreen, intl: Intl) {
|
||||
SwitchPreferenceCompat(screen.context).apply {
|
||||
key = hidePaidChaptersPrefKey
|
||||
title = intl["pref_hide_paid_chapters_title"]
|
||||
summary = intl["pref_hide_paid_chapters_summary"]
|
||||
setDefaultValue(true)
|
||||
}.also(screen::addPreference)
|
||||
}
|
||||
|
||||
fun getHidePaidChaptersPref(preferences: SharedPreferences) = preferences.getBoolean(hidePaidChaptersPrefKey, true)
|
||||
|
||||
fun getChapterListSelectorBasedOnHidePaidChaptersPref(baseChapterListSelector: String, preferences: SharedPreferences): String {
|
||||
if (!getHidePaidChaptersPref(preferences)) {
|
||||
return baseChapterListSelector
|
||||
}
|
||||
|
||||
// Fragile
|
||||
val selectors = baseChapterListSelector.split(", ")
|
||||
|
||||
return selectors
|
||||
.map { "$it:not($lockedChapterSelector):not(:has($lockedChapterSelector))" }
|
||||
.joinToString()
|
||||
}
|
||||
}
|
@ -2,7 +2,7 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 12
|
||||
baseVersionCode = 11
|
||||
|
||||
dependencies {
|
||||
api(project(":lib:i18n"))
|
||||
|
@ -67,8 +67,6 @@ constructor(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 12
|
@ -0,0 +1,428 @@
|
||||
package eu.kanade.tachiyomi.multisrc.nepnep
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Source responds to requests with their full database as a JsonArray, then sorts/filters it client-side
|
||||
* We'll take the database on first requests, then do what we want with it
|
||||
*/
|
||||
abstract class NepNep(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/77.0")
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private lateinit var directory: List<JsonElement>
|
||||
|
||||
// Convenience functions to shorten later code
|
||||
/** Returns value corresponding to given key as a string, or null */
|
||||
private fun JsonElement.getString(key: String): String? {
|
||||
return this.jsonObject[key]!!.jsonPrimitive.contentOrNull
|
||||
}
|
||||
|
||||
/** Returns value corresponding to given key as a JsonArray */
|
||||
private fun JsonElement.getArray(key: String): JsonArray {
|
||||
return this.jsonObject[key]!!.jsonArray
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/search/", headers)
|
||||
}
|
||||
|
||||
// don't use ";" for substringBefore() !
|
||||
private fun directoryFromDocument(document: Document): JsonArray {
|
||||
val str = document.select("script:containsData(MainFunction)").first()!!.data()
|
||||
.substringAfter("vm.Directory = ").substringBefore("vm.GetIntValue").trim()
|
||||
.replace(";", " ")
|
||||
return json.parseToJsonElement(str).jsonArray
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
thumbnailUrl = document.select(".SearchResult > .SearchResultCover img").first()!!.attr("ng-src")
|
||||
directory = directoryFromDocument(document).sortedByDescending { it.getString("v") }
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
private fun parseDirectory(page: Int): MangasPage {
|
||||
val mangas = mutableListOf<SManga>()
|
||||
val endRange = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex }
|
||||
|
||||
for (i in (((page - 1) * 24)..endRange)) {
|
||||
mangas.add(
|
||||
SManga.create().apply {
|
||||
title = directory[i].getString("s")!!
|
||||
url = "/manga/${directory[i].getString("i")}"
|
||||
thumbnail_url = getThumbnailUrl(directory[i].getString("i")!!)
|
||||
},
|
||||
)
|
||||
}
|
||||
return MangasPage(mangas, endRange < directory.lastIndex)
|
||||
}
|
||||
|
||||
private var thumbnailUrl: String? = null
|
||||
|
||||
private fun getThumbnailUrl(id: String): String {
|
||||
if (thumbnailUrl.isNullOrEmpty()) {
|
||||
val response = client.newCall(popularMangaRequest(1)).execute()
|
||||
thumbnailUrl = response.asJsoup().select(".SearchResult > .SearchResultCover img").first()!!.attr("ng-src")
|
||||
}
|
||||
|
||||
return thumbnailUrl!!.replace("{{Result.i}}", id)
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response)
|
||||
}
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(1)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
directory = directoryFromDocument(response.asJsoup()).sortedByDescending { it.getString("lt") }
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
// Search
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (page == 1) {
|
||||
client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, query, filters)
|
||||
}
|
||||
} else {
|
||||
Observable.just(parseDirectory(page))
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(1)
|
||||
|
||||
private fun searchMangaParse(response: Response, query: String, filters: FilterList): MangasPage {
|
||||
val trimmedQuery = query.trim()
|
||||
directory = directoryFromDocument(response.asJsoup())
|
||||
.filter {
|
||||
// Comparing query with display name
|
||||
it.getString("s")!!.contains(trimmedQuery, ignoreCase = true) or
|
||||
// Comparing query with list of alternate names
|
||||
it.getArray("al").any { altName ->
|
||||
altName.jsonPrimitive.content.contains(trimmedQuery, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
|
||||
val genres = mutableListOf<String>()
|
||||
val genresNo = mutableListOf<String>()
|
||||
var sortBy: String
|
||||
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
|
||||
when (filter) {
|
||||
is Sort -> {
|
||||
sortBy = when (filter.state?.index) {
|
||||
1 -> "ls"
|
||||
2 -> "v"
|
||||
else -> "s"
|
||||
}
|
||||
directory = if (filter.state?.ascending != true) {
|
||||
directory.sortedByDescending { it.getString(sortBy) }
|
||||
} else {
|
||||
directory.sortedByDescending { it.getString(sortBy) }.reversed()
|
||||
}
|
||||
}
|
||||
is SelectField -> if (filter.state != 0) {
|
||||
directory = when (filter.name) {
|
||||
"Scan Status" -> directory.filter { it.getString("ss")!!.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Publish Status" -> directory.filter { it.getString("ps")!!.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Type" -> directory.filter { it.getString("t")!!.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Translation" -> directory.filter { it.getString("o")!!.contains("yes", ignoreCase = true) }
|
||||
else -> directory
|
||||
}
|
||||
}
|
||||
is YearField -> if (filter.state.isNotEmpty()) directory = directory.filter { it.getString("y")!!.contains(filter.state) }
|
||||
is AuthorField -> if (filter.state.isNotEmpty()) {
|
||||
directory = directory.filter { e ->
|
||||
e.getArray("a").any {
|
||||
it.jsonPrimitive.content.contains(filter.state, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
is GenreList -> filter.state.forEach { genre ->
|
||||
when (genre.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
|
||||
Filter.TriState.STATE_EXCLUDE -> genresNo.add(genre.name)
|
||||
}
|
||||
}
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
if (genres.isNotEmpty()) {
|
||||
genres.map { genre ->
|
||||
directory = directory.filter { e ->
|
||||
e.getArray("g").any { it.jsonPrimitive.content.contains(genre, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (genresNo.isNotEmpty()) {
|
||||
genresNo.map { genre ->
|
||||
directory = directory.filterNot { e ->
|
||||
e.getArray("g").any { it.jsonPrimitive.content.contains(genre, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return response.asJsoup().select("div.BoxBody > div.row").let { info ->
|
||||
SManga.create().apply {
|
||||
title = info.select("h1").text()
|
||||
author = info.select("li.list-group-item:has(span:contains(Author)) a").first()?.text()
|
||||
status = info.select("li.list-group-item:has(span:contains(Status)) a:contains(scan)").text().toStatus()
|
||||
description = info.select("div.Content").text()
|
||||
thumbnail_url = info.select("img").attr("abs:src")
|
||||
|
||||
val genres = info.select("li.list-group-item:has(span:contains(Genre)) a")
|
||||
.map { element -> element.text() }
|
||||
.toMutableSet()
|
||||
|
||||
// add series type(manga/manhwa/manhua/other) thinggy to genre
|
||||
info.select("li.list-group-item:has(span:contains(Type)) a, a[href*=type\\=]").firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not()) {
|
||||
genres.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
genre = genres.toList().joinToString(", ")
|
||||
|
||||
// add alternative name to manga description
|
||||
val altName = "Alternative Name: "
|
||||
info.select("li.list-group-item:has(span:contains(Alter))").firstOrNull()?.ownText()?.let {
|
||||
if (it.isBlank().not() && it != "N/A") {
|
||||
description = when {
|
||||
description.isNullOrBlank() -> altName + it
|
||||
else -> description + "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toStatus() = when {
|
||||
this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("Complete", ignoreCase = true) -> SManga.COMPLETED
|
||||
this.contains("Cancelled", ignoreCase = true) -> SManga.CANCELLED
|
||||
this.contains("Hiatus", ignoreCase = true) -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters - Mind special cases like decimal chapters (e.g. One Punch Man) and manga with seasons (e.g. The Gamer)
|
||||
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:SS Z", Locale.getDefault())
|
||||
|
||||
private fun chapterURLEncode(e: String): String {
|
||||
var index = ""
|
||||
val t = e.substring(0, 1).toInt()
|
||||
if (1 != t) { index = "-index-$t" }
|
||||
val dgt = if (e.toInt() < 100100) { 4 } else if (e.toInt() < 101000) { 3 } else if (e.toInt() < 110000) { 2 } else { 1 }
|
||||
val n = e.substring(dgt, e.length - 1)
|
||||
var suffix = ""
|
||||
val path = e.substring(e.length - 1).toInt()
|
||||
if (0 != path) { suffix = ".$path" }
|
||||
return "-chapter-$n$suffix$index.html"
|
||||
}
|
||||
|
||||
private val chapterImageRegex = Regex("""^0+""")
|
||||
|
||||
private fun chapterImage(e: String, cleanString: Boolean = false): String {
|
||||
// cleanString will result in an empty string if chapter number is 0, hence the else if below
|
||||
val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it }
|
||||
// If b is not zero, indicates chapter has decimal numbering
|
||||
val b = e.substring(e.length - 1).toInt()
|
||||
return if (b == 0 && a.isNotEmpty()) {
|
||||
a
|
||||
} else if (b == 0 && a.isEmpty()) {
|
||||
"0"
|
||||
} else {
|
||||
"$a.$b"
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val vmChapters = response.asJsoup().select("script:containsData(MainFunction)").first()!!.data()
|
||||
.substringAfter("vm.Chapters = ").substringBefore(";")
|
||||
val array = json.parseToJsonElement(vmChapters).jsonArray
|
||||
val hasDistinctTypes = array.map { it.getString("Type") }.distinct().count() > 1
|
||||
return array.map { json ->
|
||||
val indexChapter = json.getString("Chapter")!!
|
||||
val type = json.getString("Type")
|
||||
SChapter.create().apply {
|
||||
name = json.getString("ChapterName").let { if (it.isNullOrEmpty()) "$type ${chapterImage(indexChapter, true)}" else it }
|
||||
url = "/read-online/" + response.request.url.toString().substringAfter("/manga/") + chapterURLEncode(indexChapter)
|
||||
// only add type info as scanlator if there are differing types among chapter array
|
||||
scanlator = if (hasDistinctTypes) type else null
|
||||
date_upload = try {
|
||||
json.getString("Date").let { dateFormat.parse("$it +0600")?.time } ?: 0
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val script = document.selectFirst("script:containsData(MainFunction)")?.data()
|
||||
?: client.newCall(GET(document.location().removeSuffix(".html"), headers))
|
||||
.execute().asJsoup().selectFirst("script:containsData(MainFunction)")!!.data()
|
||||
val curChapter = json.parseToJsonElement(script!!.substringAfter("vm.CurChapter = ").substringBefore(";")).jsonObject
|
||||
|
||||
val pageTotal = curChapter.getString("Page")!!.toInt()
|
||||
|
||||
val host = "https://" +
|
||||
script
|
||||
.substringAfter("vm.CurPathName = \"", "")
|
||||
.substringBefore("\"")
|
||||
.also {
|
||||
if (it.isEmpty()) {
|
||||
throw Exception("$name is overloaded and blocking Tachiyomi right now. Wait for unblock.")
|
||||
}
|
||||
}
|
||||
val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"")
|
||||
val seasonURI = curChapter.getString("Directory")!!
|
||||
.let { if (it.isEmpty()) "" else "$it/" }
|
||||
val path = "$host/manga/$titleURI/$seasonURI"
|
||||
|
||||
val chNum = chapterImage(curChapter.getString("Chapter")!!)
|
||||
|
||||
return IntRange(1, pageTotal).mapIndexed { i, _ ->
|
||||
val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) }
|
||||
Page(i, "", "$path$chNum-$imageNum.png")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
||||
|
||||
// Filters
|
||||
|
||||
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Selection(2, false))
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
private class YearField : Filter.Text("Years")
|
||||
private class AuthorField : Filter.Text("Author")
|
||||
private class SelectField(name: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
YearField(),
|
||||
AuthorField(),
|
||||
SelectField("Scan Status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
|
||||
SelectField("Publish Status", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
|
||||
SelectField("Type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
|
||||
SelectField("Translation", arrayOf("Any", "Official Only")),
|
||||
Sort(),
|
||||
GenreList(getGenreList()),
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("label.triStateCheckBox input")].map(el => `Filter("${el.getAttribute('name')}", "${el.nextSibling.textContent.trim()}")`).join(',\n')
|
||||
// https://manga4life.com/advanced-search/
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Comedy"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Hentai"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Isekai"),
|
||||
Genre("Josei"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri"),
|
||||
)
|
||||
}
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 3
|
||||
baseVersionCode = 2
|
||||
|
@ -29,8 +29,6 @@ abstract class PizzaReader(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
open val apiUrl by lazy { "$baseUrl$apiPath" }
|
||||
|
||||
protected open val json: Json by injectLazy()
|
||||
|
@ -1,273 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.slimereadtheme
|
||||
|
||||
import app.cash.quickjs.QuickJs
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.ChapterDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.LatestResponseDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.MangaInfoDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PageListDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.PopularMangaDto
|
||||
import eu.kanade.tachiyomi.extension.pt.slimeread.dto.toSMangaList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
abstract class SlimeReadTheme(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val scanId: String = "",
|
||||
) : HttpSource() {
|
||||
|
||||
protected open val apiUrl: String by lazy { getApiUrlFromPage() }
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
protected open val urlInfix: String = "slimeread.com"
|
||||
|
||||
protected open fun getApiUrlFromPage(): String {
|
||||
val initClient = network.cloudflareClient
|
||||
val response = initClient.newCall(GET(baseUrl, headers)).execute()
|
||||
if (!response.isSuccessful) throw Exception("HTTP error ${response.code}")
|
||||
val document = response.asJsoup()
|
||||
val scriptUrl = document.selectFirst("script[src*=pages/_app]")?.attr("abs:src")
|
||||
?: throw Exception("Could not find script URL")
|
||||
val scriptResponse = initClient.newCall(GET(scriptUrl, headers)).execute()
|
||||
if (!scriptResponse.isSuccessful) throw Exception("HTTP error ${scriptResponse.code}")
|
||||
val script = scriptResponse.body.string()
|
||||
val apiUrl = FUNCTION_REGEX.find(script)?.let { result ->
|
||||
val varBlock = result.groupValues[1]
|
||||
val varUrlInfix = result.groupValues[2]
|
||||
|
||||
val block = """${varBlock.replace(varUrlInfix, "\"$urlInfix\"")}.toString()"""
|
||||
|
||||
try {
|
||||
QuickJs.create().use { it.evaluate(block) as String }
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return apiUrl?.let { "https://$it" } ?: throw Exception("Could not find API URL")
|
||||
}
|
||||
|
||||
// ============================== Popular ===============================
|
||||
private var popularMangeCache: MangasPage? = null
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
val url = "$apiUrl/book_search?order=1&status=0".toHttpUrl().newBuilder()
|
||||
.addIfNotBlank("scan_id", scanId)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
popularMangeCache = popularMangeCache?.takeIf { page != 1 }
|
||||
?: super.fetchPopularManga(page).toBlocking().last()
|
||||
return pageableOf(page, popularMangeCache!!)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val items = response.parseAs<List<PopularMangaDto>>()
|
||||
val mangaList = items.toSMangaList()
|
||||
return MangasPage(mangaList, mangaList.isNotEmpty())
|
||||
}
|
||||
|
||||
// =============================== Latest ===============================
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
val url = "$apiUrl/books?page=$page".toHttpUrl().newBuilder()
|
||||
.addIfNotBlank("scan_id", scanId)
|
||||
.build()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val dto = response.parseAs<LatestResponseDto>()
|
||||
val mangaList = dto.data.toSMangaList()
|
||||
val hasNextPage = dto.page < dto.pages
|
||||
return MangasPage(mangaList, hasNextPage)
|
||||
}
|
||||
|
||||
// =============================== Search ===============================
|
||||
|
||||
private var searchMangaCache: MangasPage? = null
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_SEARCH)) { // URL intent handler
|
||||
val id = query.removePrefix(PREFIX_SEARCH)
|
||||
client.newCall(GET("$apiUrl/book/$id", headers))
|
||||
.asObservableSuccess()
|
||||
.map(::searchMangaByIdParse)
|
||||
} else {
|
||||
searchMangaCache = searchMangaCache?.takeIf { page != 1 }
|
||||
?: super.fetchSearchManga(page, query, filters).toBlocking().last()
|
||||
pageableOf(page, searchMangaCache!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaByIdParse(response: Response): MangasPage {
|
||||
val details = mangaDetailsParse(response)
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun getFilterList() = SlimeReadThemeFilters.FILTER_LIST
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val params = SlimeReadThemeFilters.getSearchParameters(filters)
|
||||
|
||||
val url = "$apiUrl/book_search".toHttpUrl().newBuilder()
|
||||
.addIfNotBlank("query", query)
|
||||
.addIfNotBlank("genre[]", params.genre)
|
||||
.addIfNotBlank("status", params.status)
|
||||
.addIfNotBlank("searchMethod", params.searchMethod)
|
||||
.addIfNotBlank("scan_id", scanId)
|
||||
.apply {
|
||||
params.categories.forEach {
|
||||
addQueryParameter("categories[]", it)
|
||||
}
|
||||
}.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
// =========================== Manga Details ============================
|
||||
override fun getMangaUrl(manga: SManga) = baseUrl + manga.url.replace("/book/", "/manga/")
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(apiUrl + manga.url, headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val info = response.parseAs<MangaInfoDto>()
|
||||
thumbnail_url = info.thumbnail_url
|
||||
title = info.name
|
||||
description = info.description
|
||||
genre = info.categories.joinToString()
|
||||
url = "/book/${info.id}"
|
||||
status = when (info.status) {
|
||||
1 -> SManga.ONGOING
|
||||
2 -> SManga.COMPLETED
|
||||
3, 4 -> SManga.CANCELLED
|
||||
5 -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// ============================== Chapters ==============================
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$apiUrl/book_cap_units_all?manga_id=${manga.url.substringAfterLast("/")}", headers)
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val items = response.parseAs<List<ChapterDto>>()
|
||||
val mangaId = response.request.url.queryParameter("manga_id")!!
|
||||
return items.map {
|
||||
SChapter.create().apply {
|
||||
name = "Cap " + parseChapterNumber(it.number)
|
||||
date_upload = parseChapterDate(it.updated_at)
|
||||
chapter_number = it.number
|
||||
scanlator = it.scan?.scan_name
|
||||
url = "/book_cap_units?manga_id=$mangaId&cap=${it.number}"
|
||||
}
|
||||
}.reversed()
|
||||
}
|
||||
|
||||
private fun parseChapterNumber(number: Float): String {
|
||||
val cap = number + 1F
|
||||
return "%.2f".format(cap)
|
||||
.let { if (cap < 10F) "0$it" else it }
|
||||
.replace(",00", "")
|
||||
.replace(",", ".")
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return try { dateFormat.parse(date)!!.time } catch (_: Exception) { 0L }
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String {
|
||||
val url = "$baseUrl${chapter.url}".toHttpUrl()
|
||||
val id = url.queryParameter("manga_id")!!
|
||||
val cap = url.queryParameter("cap")!!.toFloat()
|
||||
val num = parseChapterNumber(cap)
|
||||
return "$baseUrl/ler/$id/cap-$num"
|
||||
}
|
||||
|
||||
// =============================== Pages ================================
|
||||
override fun pageListRequest(chapter: SChapter) = GET(apiUrl + chapter.url, headers)
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val body = response.body.string()
|
||||
val pages = if (body.startsWith("{")) {
|
||||
json.decodeFromString<Map<String, PageListDto>>(body).values.flatMap { it.pages }
|
||||
} else {
|
||||
json.decodeFromString<List<PageListDto>>(body).flatMap { it.pages }
|
||||
}
|
||||
|
||||
return pages.mapIndexed { index, item ->
|
||||
Page(index, "", item.url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
// ============================= Utilities ==============================
|
||||
|
||||
/**
|
||||
* Handles a large manga list and returns a paginated response.
|
||||
* The app can't handle the large JSON list without pagination.
|
||||
*
|
||||
* @param page The page number to retrieve.
|
||||
* @param cache The cached manga page containing the full list of mangas.
|
||||
*/
|
||||
private fun pageableOf(page: Int, cache: MangasPage) = Observable.just(cache).map { mangaPage ->
|
||||
val mangas = mangaPage.mangas
|
||||
val pageSize = 15
|
||||
|
||||
val currentSlice = (page - 1) * pageSize
|
||||
|
||||
val startIndex = min(mangas.size, currentSlice)
|
||||
val endIndex = min(mangas.size, currentSlice + pageSize)
|
||||
|
||||
val slice = mangas.subList(startIndex, endIndex)
|
||||
|
||||
MangasPage(slice, hasNextPage = endIndex < mangas.size)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
json.decodeFromStream(it.body.byteStream())
|
||||
}
|
||||
|
||||
private fun HttpUrl.Builder.addIfNotBlank(query: String, value: String): HttpUrl.Builder {
|
||||
if (value.isNotBlank()) addQueryParameter(query, value)
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PREFIX_SEARCH = "id:"
|
||||
val FUNCTION_REGEX = """(?<script>\[""\.concat\("[^,]+,"\."\)\.concat\((?<infix>[^,]+),":\d+"\)\])""".toRegex(RegexOption.DOT_MATCHES_ALL)
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||
}
|
||||
}
|
@ -118,8 +118,7 @@ abstract class VerComics(
|
||||
protected open val pageListSelector =
|
||||
"div.wp-content p > img:not(noscript img), " +
|
||||
"div.wp-content div#lector > img:not(noscript img), " +
|
||||
"div.wp-content > figure img:not(noscript img)," +
|
||||
"div.wp-content > img, div.wp-content > p img"
|
||||
"div.wp-content > figure img:not(noscript img)"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = document.select(pageListSelector)
|
||||
.mapIndexed { i, img -> Page(i, imageUrl = img.imgAttr()) }
|
||||
|
@ -2,4 +2,4 @@ plugins {
|
||||
id("lib-multisrc")
|
||||
}
|
||||
|
||||
baseVersionCode = 10
|
||||
baseVersionCode = 9
|
||||
|
@ -27,8 +27,6 @@ abstract class ZeistManga(
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient
|
||||
|
||||
protected val json: Json by injectLazy()
|
||||
|
||||
private val intl by lazy { ZeistMangaIntl(lang) }
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Bato.to'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 46
|
||||
extVersionCode = 43
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
@ -41,6 +42,7 @@ import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
open class BatoTo(
|
||||
final override val lang: String,
|
||||
@ -49,11 +51,10 @@ open class BatoTo(
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
.migrateMirrorPref()
|
||||
}
|
||||
|
||||
override val name: String = "Bato.to"
|
||||
override val baseUrl: String get() = mirror
|
||||
override val baseUrl: String by lazy { getMirrorPref()!! }
|
||||
override val id: Long = when (lang) {
|
||||
"zh-Hans" -> 2818874445640189582
|
||||
"zh-Hant" -> 38886079663327225
|
||||
@ -69,9 +70,12 @@ open class BatoTo(
|
||||
entryValues = MIRROR_PREF_ENTRY_VALUES
|
||||
setDefaultValue(MIRROR_PREF_DEFAULT_VALUE)
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
mirror = newValue as String
|
||||
true
|
||||
val selected = newValue as String
|
||||
val index = findIndexOfValue(selected)
|
||||
val entry = entryValues[index] as String
|
||||
preferences.edit().putString("${MIRROR_PREF_KEY}_$lang", entry).commit()
|
||||
}
|
||||
}
|
||||
val altChapterListPref = CheckBoxPreference(screen.context).apply {
|
||||
@ -79,6 +83,11 @@ open class BatoTo(
|
||||
title = ALT_CHAPTER_LIST_PREF_TITLE
|
||||
summary = ALT_CHAPTER_LIST_PREF_SUMMARY
|
||||
setDefaultValue(ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
preferences.edit().putBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", checkValue).commit()
|
||||
}
|
||||
}
|
||||
val removeOfficialPref = CheckBoxPreference(screen.context).apply {
|
||||
key = "${REMOVE_TITLE_VERSION_PREF}_$lang"
|
||||
@ -94,35 +103,18 @@ open class BatoTo(
|
||||
screen.addPreference(removeOfficialPref)
|
||||
}
|
||||
|
||||
private var mirror = ""
|
||||
get() {
|
||||
val current = field
|
||||
if (current.isNotEmpty()) {
|
||||
return current
|
||||
}
|
||||
field = getMirrorPref()!!
|
||||
return field
|
||||
}
|
||||
|
||||
private fun getMirrorPref(): String? = preferences.getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)
|
||||
private fun getAltChapterListPref(): Boolean = preferences.getBoolean("${ALT_CHAPTER_LIST_PREF_KEY}_$lang", ALT_CHAPTER_LIST_PREF_DEFAULT_VALUE)
|
||||
private fun isRemoveTitleVersion(): Boolean {
|
||||
return preferences.getBoolean("${REMOVE_TITLE_VERSION_PREF}_$lang", false)
|
||||
}
|
||||
|
||||
private fun SharedPreferences.migrateMirrorPref(): SharedPreferences {
|
||||
val selectedMirror = getString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE)!!
|
||||
|
||||
if (selectedMirror in DEPRECATED_MIRRORS) {
|
||||
edit().putString("${MIRROR_PREF_KEY}_$lang", MIRROR_PREF_DEFAULT_VALUE).commit()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
override val supportsLatest = true
|
||||
private val json: Json by injectLazy()
|
||||
override val client = network.cloudflareClient
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page", headers)
|
||||
@ -332,28 +324,17 @@ open class BatoTo(
|
||||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
private var titleRegex: Regex =
|
||||
Regex("\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|\\/Official|\\/ Official", RegexOption.IGNORE_CASE)
|
||||
Regex("(?:\\([^()]*\\)|\\{[^{}]*\\}|\\[(?:(?!]).)*]|«[^»]*»|〘[^〙]*〙|「[^」]*」|『[^』]*』|≪[^≫]*≫|﹛[^﹜]*﹜|〖[^〖〗]*〗|𖤍.+?𖤍|《[^》]*》|⌜.+?⌝|⟨[^⟩]*⟩|/.+)")
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.selectFirst("div#mainer div.container-fluid")!!
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
val manga = SManga.create()
|
||||
val workStatus = infoElement.select("div.attr-item:contains(original work) span").text()
|
||||
val uploadStatus = infoElement.select("div.attr-item:contains(upload status) span").text()
|
||||
val originalTitle = infoElement.select("h3").text().removeEntities()
|
||||
val description = buildString {
|
||||
append(infoElement.select("div.limit-html").text())
|
||||
infoElement.selectFirst(".episode-list > .alert-warning")?.also {
|
||||
append("\n\n${it.text()}")
|
||||
}
|
||||
infoElement.selectFirst("h5:containsOwn(Extra Info:) + div")?.also {
|
||||
append("\n\nExtra Info:\n${it.text()}")
|
||||
}
|
||||
document.selectFirst("div.pb-2.alias-set.line-b-f")?.also {
|
||||
append("\n\nAlternative Titles:\n")
|
||||
append(it.text().split('/').joinToString("\n") { "• ${it.trim()}" })
|
||||
}
|
||||
}
|
||||
|
||||
val alternativeTitles = document.select("div.pb-2.alias-set.line-b-f").text()
|
||||
val description = infoElement.select("div.limit-html").text() + "\n" +
|
||||
infoElement.select(".episode-list > .alert-warning").text().trim()
|
||||
val cleanedTitle = if (isRemoveTitleVersion()) {
|
||||
originalTitle.replace(titleRegex, "").trim()
|
||||
} else {
|
||||
@ -365,7 +346,8 @@ open class BatoTo(
|
||||
manga.artist = infoElement.select("div.attr-item:contains(artist) span").text()
|
||||
manga.status = parseStatus(workStatus, uploadStatus)
|
||||
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
|
||||
manga.description = description
|
||||
manga.description = description +
|
||||
if (alternativeTitles.isNotBlank()) "\n\nAlternative Titles:\n$alternativeTitles" else ""
|
||||
manga.thumbnail_url = document.select("div.attr-cover img").attr("abs:src")
|
||||
return manga
|
||||
}
|
||||
@ -1001,7 +983,7 @@ open class BatoTo(
|
||||
private const val MIRROR_PREF_TITLE = "Mirror"
|
||||
private const val REMOVE_TITLE_VERSION_PREF = "REMOVE_TITLE_VERSION"
|
||||
private val MIRROR_PREF_ENTRIES = arrayOf(
|
||||
"zbato.org",
|
||||
"bato.to",
|
||||
"batocomic.com",
|
||||
"batocomic.net",
|
||||
"batocomic.org",
|
||||
@ -1010,6 +992,9 @@ open class BatoTo(
|
||||
"battwo.com",
|
||||
"comiko.net",
|
||||
"comiko.org",
|
||||
"mangatoto.com",
|
||||
"mangatoto.net",
|
||||
"mangatoto.org",
|
||||
"readtoto.com",
|
||||
"readtoto.net",
|
||||
"readtoto.org",
|
||||
@ -1024,17 +1009,11 @@ open class BatoTo(
|
||||
"xbato.org",
|
||||
"zbato.com",
|
||||
"zbato.net",
|
||||
"zbato.org",
|
||||
)
|
||||
private val MIRROR_PREF_ENTRY_VALUES = MIRROR_PREF_ENTRIES.map { "https://$it" }.toTypedArray()
|
||||
private val MIRROR_PREF_DEFAULT_VALUE = MIRROR_PREF_ENTRY_VALUES[0]
|
||||
|
||||
private val DEPRECATED_MIRRORS = listOf(
|
||||
"https://bato.to",
|
||||
"https://mangatoto.com",
|
||||
"https://mangatoto.net",
|
||||
"https://mangatoto.org",
|
||||
)
|
||||
|
||||
private const val ALT_CHAPTER_LIST_PREF_KEY = "ALT_CHAPTER_LIST"
|
||||
private const val ALT_CHAPTER_LIST_PREF_TITLE = "Alternative Chapter List"
|
||||
private const val ALT_CHAPTER_LIST_PREF_SUMMARY = "If checked, uses an alternate chapter list"
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Comick'
|
||||
extClass = '.ComickFactory'
|
||||
extVersionCode = 51
|
||||
extVersionCode = 50
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ abstract class Comick(
|
||||
add("User-Agent", "Tachiyomi ${System.getProperty("http.agent")}")
|
||||
}
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
override val client = network.client.newBuilder()
|
||||
.addNetworkInterceptor(::errorInterceptor)
|
||||
.rateLimit(3, 1, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Cubari'
|
||||
extClass = '.CubariFactory'
|
||||
extVersionCode = 25
|
||||
extVersionCode = 24
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -38,7 +38,7 @@ open class Cubari(override val lang: String) : HttpSource() {
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
override val client = super.client.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val headers = request.headers.newBuilder()
|
||||
|
@ -1,24 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.deviantart.DeviantArtUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
|
||||
<data android:scheme="http"/>
|
||||
<data android:scheme="https"/>
|
||||
|
||||
<data android:host="www.deviantart.com"/>
|
||||
<data android:host="deviantart.com"/>
|
||||
|
||||
<data android:pathPattern="/..*/gallery/..*"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
Before Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 14 KiB |
@ -1,173 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.deviantart
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.parser.Parser
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class DeviantArt : HttpSource() {
|
||||
override val name = "DeviantArt"
|
||||
override val baseUrl = "https://deviantart.com"
|
||||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0")
|
||||
}
|
||||
|
||||
private val backendBaseUrl = "https://backend.deviantart.com"
|
||||
private fun backendBuilder() = backendBaseUrl.toHttpUrl().newBuilder()
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
private fun parseDate(dateStr: String?): Long {
|
||||
return try {
|
||||
dateFormat.parse(dateStr ?: "")!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException(SEARCH_FORMAT_MSG)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val matchGroups = requireNotNull(
|
||||
Regex("""gallery:([\w-]+)(?:/(\d+))?""").matchEntire(query)?.groupValues,
|
||||
) { SEARCH_FORMAT_MSG }
|
||||
val username = matchGroups[1]
|
||||
val folderId = matchGroups[2].ifEmpty { "all" }
|
||||
return GET("$baseUrl/$username/gallery/$folderId", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val manga = mangaDetailsParse(response)
|
||||
return MangasPage(listOf(manga), false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
val subFolderGallery = document.selectFirst("#sub-folder-gallery")
|
||||
val manga = SManga.create().apply {
|
||||
// If manga is sub-gallery then use sub-gallery name, else use gallery name
|
||||
title = subFolderGallery?.selectFirst("._2vMZg + ._2vMZg")?.text()?.substringBeforeLast(" ")
|
||||
?: subFolderGallery?.selectFirst("[aria-haspopup=listbox] > div")!!.ownText()
|
||||
author = document.title().substringBefore(" ")
|
||||
description = subFolderGallery?.selectFirst(".legacy-journal")?.wholeText()
|
||||
thumbnail_url = subFolderGallery?.selectFirst("img[property=contentUrl]")?.absUrl("src")
|
||||
}
|
||||
manga.setUrlWithoutDomain(response.request.url.toString())
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val pathSegments = getMangaUrl(manga).toHttpUrl().pathSegments
|
||||
val username = pathSegments[0]
|
||||
val folderId = pathSegments[2]
|
||||
|
||||
val query = if (folderId == "all") {
|
||||
"gallery:$username"
|
||||
} else {
|
||||
"gallery:$username/$folderId"
|
||||
}
|
||||
|
||||
val url = backendBuilder()
|
||||
.addPathSegment("rss.xml")
|
||||
.addQueryParameter("q", query)
|
||||
.build()
|
||||
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoupXml()
|
||||
val chapterList = parseToChapterList(document).toMutableList()
|
||||
var nextUrl = document.selectFirst("[rel=next]")?.absUrl("href")
|
||||
|
||||
while (nextUrl != null) {
|
||||
val newRequest = GET(nextUrl, headers)
|
||||
val newResponse = client.newCall(newRequest).execute()
|
||||
val newDocument = newResponse.asJsoupXml()
|
||||
val newChapterList = parseToChapterList(newDocument)
|
||||
chapterList.addAll(newChapterList)
|
||||
|
||||
nextUrl = newDocument.selectFirst("[rel=next]")?.absUrl("href")
|
||||
}
|
||||
|
||||
return indexChapterList(chapterList.toList())
|
||||
}
|
||||
|
||||
private fun parseToChapterList(document: Document): List<SChapter> {
|
||||
val items = document.select("item")
|
||||
return items.map {
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(it.selectFirst("link")!!.text())
|
||||
chapter.apply {
|
||||
name = it.selectFirst("title")!!.text()
|
||||
date_upload = parseDate(it.selectFirst("pubDate")?.text())
|
||||
scanlator = it.selectFirst("media|credit")?.text()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun indexChapterList(chapterList: List<SChapter>): List<SChapter> {
|
||||
// DeviantArt allows users to arrange galleries arbitrarily so we will
|
||||
// primitively index the list by checking the first and last dates
|
||||
return if (chapterList.first().date_upload > chapterList.last().date_upload) {
|
||||
chapterList.mapIndexed { i, chapter ->
|
||||
chapter.apply { chapter_number = chapterList.size - i.toFloat() }
|
||||
}
|
||||
} else {
|
||||
chapterList.mapIndexed { i, chapter ->
|
||||
chapter.apply { chapter_number = i.toFloat() + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val imageUrl = document.selectFirst("img[fetchpriority=high]")?.absUrl("src")
|
||||
return listOf(Page(0, imageUrl = imageUrl))
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
private fun Response.asJsoupXml(): Document {
|
||||
return Jsoup.parse(body.string(), request.url.toString(), Parser.xmlParser())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SEARCH_FORMAT_MSG = "Please enter a query in the format of gallery:{username} or gallery:{username}/{folderId}"
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'E-Hentai'
|
||||
extClass = '.EHFactory'
|
||||
extVersionCode = 24
|
||||
extVersionCode = 22
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -48,13 +48,11 @@ abstract class EHentai(
|
||||
private val webViewCookieManager: CookieManager by lazy { CookieManager.getInstance() }
|
||||
private val memberId: String by lazy { getMemberIdPref() }
|
||||
private val passHash: String by lazy { getPassHashPref() }
|
||||
private val igneous: String by lazy { getIgneousPref() }
|
||||
private val forceEh: Boolean by lazy { getForceEhPref() }
|
||||
|
||||
override val baseUrl: String
|
||||
get() = when {
|
||||
System.getenv("CI") == "true" -> "https://e-hentai.org"
|
||||
!forceEh && memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
|
||||
memberId.isNotEmpty() && passHash.isNotEmpty() -> "https://exhentai.org"
|
||||
else -> "https://e-hentai.org"
|
||||
}
|
||||
|
||||
@ -172,18 +170,18 @@ abstract class EHentai(
|
||||
query.isBlank() -> languageTag(enforceLanguageFilter)
|
||||
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
|
||||
}
|
||||
filters.filterIsInstance<TextFilter>().forEach { filter ->
|
||||
if (filter.state.isNotEmpty()) {
|
||||
val splitted = filter.state.split(",").filter(String::isNotBlank)
|
||||
if (splitted.size < 2 && filter.type != "tags") {
|
||||
modifiedQuery += " ${filter.type}:\"${filter.state.replace(" ", "+")}\""
|
||||
filters.filterIsInstance<TextFilter>().forEach { it ->
|
||||
if (it.state.isNotEmpty()) {
|
||||
val splitted = it.state.split(",").filter(String::isNotBlank)
|
||||
if (splitted.size < 2 && it.type != "tags") {
|
||||
modifiedQuery += " ${it.type}:\"${it.state.replace(" ", "+")}\""
|
||||
} else {
|
||||
splitted.forEach { tag ->
|
||||
val trimmed = tag.trim().lowercase()
|
||||
modifiedQuery += if (trimmed.startsWith('-')) {
|
||||
" -${filter.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
|
||||
if (trimmed.startsWith('-')) {
|
||||
modifiedQuery += " -${it.type}:\"${trimmed.removePrefix("-").replace(" ", "+")}\""
|
||||
} else {
|
||||
" ${filter.type}:\"${trimmed.replace(" ", "+")}\""
|
||||
modifiedQuery += " ${it.type}:\"${trimmed.replace(" ", "+")}\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -380,7 +378,7 @@ abstract class EHentai(
|
||||
|
||||
cookies["ipb_pass_hash"] = passHash
|
||||
|
||||
cookies["igneous"] = igneous
|
||||
cookies["igneous"] = ""
|
||||
|
||||
buildCookies(cookies)
|
||||
}
|
||||
@ -400,7 +398,7 @@ abstract class EHentai(
|
||||
.appendQueryParameter(param, value)
|
||||
.toString()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
override val client = network.client.newBuilder()
|
||||
.cookieJar(CookieJar.NO_COOKIES)
|
||||
.addInterceptor { chain ->
|
||||
val newReq = chain
|
||||
@ -416,7 +414,6 @@ abstract class EHentai(
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
EnforceLanguageFilter(getEnforceLanguagePref()),
|
||||
Favorites(),
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
Filter.Header("Separate tags with commas (,)"),
|
||||
@ -438,14 +435,6 @@ abstract class EHentai(
|
||||
}
|
||||
}
|
||||
|
||||
class Favorites : CheckBox("Favorites"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state) {
|
||||
builder.appendPath("favorites.php")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GenreOption(name: String, private val genreId: String) : CheckBox(name, false), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
builder.appendQueryParameter("f_$genreId", if (state) "1" else "0")
|
||||
@ -572,33 +561,21 @@ abstract class EHentai(
|
||||
private const val PASS_HASH_PREF_TITLE = "ipb_pass_hash"
|
||||
private const val PASS_HASH_PREF_SUMMARY = "ipb_pass_hash value"
|
||||
private const val PASS_HASH_PREF_DEFAULT_VALUE = ""
|
||||
|
||||
private const val IGNEOUS_PREF_KEY = "IGNEOUS"
|
||||
private const val IGNEOUS_PREF_TITLE = "igneous"
|
||||
private const val IGNEOUS_PREF_SUMMARY = "igneous value override"
|
||||
private const val IGNEOUS_PREF_DEFAULT_VALUE = ""
|
||||
|
||||
private const val FORCE_EH = "FORCE_EH"
|
||||
private const val FORCE_EH_TITLE = "Force e-hentai"
|
||||
private const val FORCE_EH_SUMMARY = "Force e-hentai to avoid content on exhentai"
|
||||
private const val FORCE_EH_DEFAULT_VALUE = true
|
||||
}
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val forceEhPref = CheckBoxPreference(screen.context).apply {
|
||||
key = FORCE_EH
|
||||
title = FORCE_EH_TITLE
|
||||
summary = FORCE_EH_SUMMARY
|
||||
setDefaultValue(FORCE_EH_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
val enforceLanguagePref = CheckBoxPreference(screen.context).apply {
|
||||
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
|
||||
title = ENFORCE_LANGUAGE_PREF_TITLE
|
||||
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
|
||||
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val checkValue = newValue as Boolean
|
||||
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
|
||||
}
|
||||
}
|
||||
|
||||
val memberIdPref = EditTextPreference(screen.context).apply {
|
||||
@ -616,19 +593,8 @@ abstract class EHentai(
|
||||
|
||||
setDefaultValue(PASS_HASH_PREF_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
val igneousPref = EditTextPreference(screen.context).apply {
|
||||
key = IGNEOUS_PREF_KEY
|
||||
title = IGNEOUS_PREF_TITLE
|
||||
summary = IGNEOUS_PREF_SUMMARY
|
||||
|
||||
setDefaultValue(IGNEOUS_PREF_DEFAULT_VALUE)
|
||||
}
|
||||
|
||||
screen.addPreference(forceEhPref)
|
||||
screen.addPreference(memberIdPref)
|
||||
screen.addPreference(passHashPref)
|
||||
screen.addPreference(igneousPref)
|
||||
screen.addPreference(enforceLanguagePref)
|
||||
}
|
||||
|
||||
@ -663,12 +629,4 @@ abstract class EHentai(
|
||||
private fun getMemberIdPref(): String {
|
||||
return getCookieValue(MEMBER_ID_PREF_TITLE, MEMBER_ID_PREF_DEFAULT_VALUE, MEMBER_ID_PREF_KEY)
|
||||
}
|
||||
|
||||
private fun getIgneousPref(): String {
|
||||
return getCookieValue(IGNEOUS_PREF_TITLE, IGNEOUS_PREF_DEFAULT_VALUE, IGNEOUS_PREF_KEY)
|
||||
}
|
||||
|
||||
private fun getForceEhPref(): Boolean {
|
||||
return preferences.getBoolean(FORCE_EH, FORCE_EH_DEFAULT_VALUE)
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ ext {
|
||||
extClass = '.EternalMangasFactory'
|
||||
themePkg = 'mangaesp'
|
||||
baseUrl = 'https://eternalmangas.com'
|
||||
overrideVersionCode = 2
|
||||
overrideVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,14 @@ package eu.kanade.tachiyomi.extension.all.eternalmangas
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.MangaEsp
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.SeriesDto
|
||||
import eu.kanade.tachiyomi.multisrc.mangaesp.TopSeriesDto
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okhttp3.FormBody
|
||||
@ -24,7 +27,21 @@ open class EternalMangas(
|
||||
"https://eternalmangas.com",
|
||||
lang,
|
||||
) {
|
||||
override val useApiSearch = true
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val body = response.body.string()
|
||||
val responseData = json.decodeFromString<TopSeriesDto>(body)
|
||||
|
||||
val topDaily = responseData.response.topDaily.flatten().map { it.data }
|
||||
val topWeekly = responseData.response.topWeekly.flatten().map { it.data }
|
||||
val topMonthly = responseData.response.topMonthly.flatten().map { it.data }
|
||||
|
||||
val mangas = (topDaily + topWeekly + topMonthly).distinctBy { it.slug }
|
||||
.filter { it.language == internalLang }
|
||||
.map { it.toSManga(seriesPath) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val responseData = json.decodeFromString<LatestUpdatesDto>(response.body.string())
|
||||
@ -32,8 +49,16 @@ open class EternalMangas(
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun List<SeriesDto>.additionalParse(): List<SeriesDto> {
|
||||
return this.filter { it.language == internalLang }.toMutableList()
|
||||
override fun searchMangaParse(response: Response, page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
val script = document.select("script:containsData(self.__next_f.push)").joinToString { it.data() }
|
||||
val jsonString = MANGA_LIST_REGEX.find(script)?.groupValues?.get(1)
|
||||
?: throw Exception(intl["comics_list_error"])
|
||||
val unescapedJson = jsonString.unescape()
|
||||
comicsList = json.decodeFromString<List<SeriesDto>>(unescapedJson)
|
||||
.filter { it.language == internalLang }
|
||||
.toMutableList()
|
||||
return parseComicsList(page, query, filters)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
|
@ -1,22 +1,10 @@
|
||||
package eu.kanade.tachiyomi.extension.all.hentaifox
|
||||
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.GalleryAdults
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.Genre
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.SortOrderFilter
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.imgAttr
|
||||
import eu.kanade.tachiyomi.multisrc.galleryadults.toDate
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
class HentaiFox(
|
||||
@ -112,103 +100,4 @@ class HentaiFox(
|
||||
Filter.Header("HINT: Use double quote (\") for exact match"),
|
||||
) + super.getFilterList().list,
|
||||
)
|
||||
|
||||
private val sidebarPath = "includes/sidebar.php"
|
||||
|
||||
private fun sidebarMangaSelector() = "div.item"
|
||||
|
||||
private fun Element.sidebarMangaTitle() =
|
||||
selectFirst("img")?.attr("alt")
|
||||
|
||||
private fun Element.sidebarMangaUrl() =
|
||||
selectFirst("a")?.attr("abs:href")
|
||||
|
||||
private fun Element.sidebarMangaThumbnail() =
|
||||
selectFirst("img")?.imgAttr()
|
||||
|
||||
private var csrfToken: String? = null
|
||||
|
||||
override fun tagsParser(document: Document): List<Genre> {
|
||||
csrfToken = csrfParser(document)
|
||||
return super.tagsParser(document)
|
||||
}
|
||||
|
||||
private fun csrfParser(document: Document): String {
|
||||
return document.select("[name=csrf-token]").attr("content")
|
||||
}
|
||||
|
||||
private fun setSidebarHeaders(csrfToken: String?): Headers {
|
||||
if (csrfToken == null) {
|
||||
return xhrHeaders
|
||||
}
|
||||
return xhrHeaders.newBuilder()
|
||||
.add("X-Csrf-Token", csrfToken)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// Sidebar mangas should always override any other search, so they should appear first
|
||||
// and only propagate to super when a "normal" search is issued
|
||||
val sortOrderFilter = filters.filterIsInstance<SortOrderFilter>().firstOrNull()
|
||||
|
||||
sortOrderFilter?.let {
|
||||
val selectedCategory = sortOrderFilter.values.get(sortOrderFilter.state)
|
||||
if (sidebarCategoriesFilterStateMap.containsKey(selectedCategory)) {
|
||||
return sidebarRequest(
|
||||
sidebarCategoriesFilterStateMap.getValue(selectedCategory),
|
||||
)
|
||||
}
|
||||
}
|
||||
return super.searchMangaRequest(page, query, filters)
|
||||
}
|
||||
|
||||
private fun sidebarRequest(category: String): Request {
|
||||
val url = "$baseUrl/$sidebarPath"
|
||||
return POST(
|
||||
url,
|
||||
setSidebarHeaders(csrfToken),
|
||||
FormBody.Builder()
|
||||
.add("type", category)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
if (response.request.url.encodedPath.endsWith(sidebarPath)) {
|
||||
val document = response.asJsoup()
|
||||
val mangas = document.select(sidebarMangaSelector())
|
||||
.map {
|
||||
SMangaDto(
|
||||
title = it.sidebarMangaTitle()!!,
|
||||
url = it.sidebarMangaUrl()!!,
|
||||
thumbnail = it.sidebarMangaThumbnail(),
|
||||
lang = LANGUAGE_MULTI,
|
||||
)
|
||||
}
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
title = it.title
|
||||
setUrlWithoutDomain(it.url)
|
||||
thumbnail_url = it.thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
} else {
|
||||
return super.searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSortOrderURIs(): List<Pair<String, String>> {
|
||||
return super.getSortOrderURIs() + sidebarCategoriesFilterStateMap.toList()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val sidebarCategoriesFilterStateMap = mapOf(
|
||||
"Top Rated" to "top_rated",
|
||||
"Most Faved" to "top_faved",
|
||||
"Most Fapped" to "top_fapped",
|
||||
"Most Downloaded" to "top_downloaded",
|
||||
).withDefault { "top_rated" }
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Hitomi'
|
||||
extClass = '.HitomiFactory'
|
||||
extVersionCode = 35
|
||||
extVersionCode = 33
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.extension.all.hitomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -30,7 +29,6 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import okhttp3.internal.http2.StreamResetException
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -44,7 +42,6 @@ import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
class Hitomi(
|
||||
@ -65,10 +62,7 @@ class Hitomi(
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(::jxlContentTypeInterceptor)
|
||||
.apply {
|
||||
interceptors().add(0, ::streamResetRetry)
|
||||
}
|
||||
.addInterceptor(::Intercept)
|
||||
.build()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
@ -714,7 +708,7 @@ class Hitomi(
|
||||
return this.sliceArray(byteArray.indices).contentEquals(byteArray)
|
||||
}
|
||||
|
||||
private fun jxlContentTypeInterceptor(chain: Interceptor.Chain): Response {
|
||||
private fun Intercept(chain: Interceptor.Chain): Response {
|
||||
val response = chain.proceed(chain.request())
|
||||
if (response.headers["Content-Type"] != "application/octet-stream") {
|
||||
return response
|
||||
@ -734,20 +728,6 @@ class Hitomi(
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun streamResetRetry(chain: Interceptor.Chain): Response {
|
||||
return try {
|
||||
chain.proceed(chain.request())
|
||||
} catch (e: StreamResetException) {
|
||||
Log.e(name, "reset", e)
|
||||
if (e.message.orEmpty().contains("INTERNAL_ERROR")) {
|
||||
Thread.sleep(2.seconds.inWholeMilliseconds)
|
||||
chain.proceed(chain.request())
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response) = throw UnsupportedOperationException()
|
||||
override fun popularMangaRequest(page: Int) = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
@ -22,9 +22,9 @@ class Gallery(
|
||||
@Serializable
|
||||
class ImageFile(
|
||||
val hash: String,
|
||||
val haswebp: Int?,
|
||||
val hasavif: Int?,
|
||||
val hasjxl: Int?,
|
||||
val haswebp: Int,
|
||||
val hasavif: Int,
|
||||
val hasjxl: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Komga'
|
||||
extClass = '.KomgaFactory'
|
||||
extVersionCode = 59
|
||||
extVersionCode = 58
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -156,7 +156,6 @@ open class Komga(private val suffix: String = "") : ConfigurableSource, Unmetere
|
||||
1 -> if (type == "series") "metadata.titleSort" else "name"
|
||||
2 -> "createdDate"
|
||||
3 -> "lastModifiedDate"
|
||||
4 -> "random"
|
||||
else -> return@forEach
|
||||
} + "," + if (state.ascending) "asc" else "desc"
|
||||
|
||||
|
@ -19,7 +19,7 @@ internal class TypeSelect : Filter.Select<String>(
|
||||
|
||||
internal class SeriesSort(selection: Selection? = null) : Filter.Sort(
|
||||
"Sort",
|
||||
arrayOf("Relevance", "Alphabetically", "Date added", "Date updated", "Random"),
|
||||
arrayOf("Relevance", "Alphabetically", "Date added", "Date updated"),
|
||||
selection ?: Selection(0, false),
|
||||
)
|
||||
|
||||
|
@ -33,7 +33,7 @@ open class MangaFire(
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
|
||||
|
@ -31,7 +31,7 @@ open class MangaReader(
|
||||
|
||||
override val baseUrl = "https://mangareader.to"
|
||||
|
||||
override val client = super.client.newBuilder()
|
||||
override val client = network.client.newBuilder()
|
||||
.addInterceptor(ImageInterceptor)
|
||||
.build()
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Meitua.top'
|
||||
extClass = '.MeituaTop'
|
||||
extVersionCode = 9
|
||||
extVersionCode = 8
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ class MeituaTop : HttpSource() {
|
||||
override val lang = "all"
|
||||
override val supportsLatest = false
|
||||
|
||||
override val baseUrl = "https://7a.meitu1.mom"
|
||||
override val baseUrl = "https://88188.meitu.lol"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/arttype/0b-$page.html", headers)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'Mitaku'
|
||||
extClass = '.Mitaku'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ class Mitaku : ParsedHttpSource() {
|
||||
// ============================== Popular ===============================
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/category/ero-cosplay/page/$page", headers)
|
||||
|
||||
override fun popularMangaSelector() = "div.cm-primary article"
|
||||
override fun popularMangaSelector() = "div.article-container article"
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
setUrlWithoutDomain(element.selectFirst("a")!!.absUrl("href"))
|
||||
|
@ -1,7 +1,7 @@
|
||||
ext {
|
||||
extName = 'MyReadingManga'
|
||||
extClass = '.MyReadingMangaFactory'
|
||||
extVersionCode = 56
|
||||
extVersionCode = 53
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
@ -14,6 +14,7 @@ import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
@ -28,21 +29,10 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
||||
// Basic Info
|
||||
override val name = "MyReadingManga"
|
||||
final override val baseUrl = "https://myreadingmanga.info"
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
override fun headersBuilder(): Headers.Builder =
|
||||
super.headersBuilder()
|
||||
.set("User-Agent", USER_AGENT)
|
||||
.add("X-Requested-With", randomString((1..20).random()))
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor { chain ->
|
||||
val request = chain.request()
|
||||
val headers = request.headers.newBuilder().apply {
|
||||
removeAll("X-Requested-With")
|
||||
}.build()
|
||||
|
||||
chain.proceed(request.newBuilder().headers(headers).build())
|
||||
}
|
||||
.build()
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Popular - Random
|
||||
@ -318,16 +308,7 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
||||
Filter.Select<String>(displayName, vals.map { it }.toTypedArray(), defaultValue), UriFilter {
|
||||
override fun addToUri(uri: Uri.Builder, uriParam: String) {
|
||||
if (state != 0 || !firstIsUnspecified) {
|
||||
val splitFilter = vals[state].split(",")
|
||||
when {
|
||||
splitFilter.size == 2 -> {
|
||||
val reversedFilter = splitFilter.reversed().joinToString(" | ").trim()
|
||||
uri.appendQueryParameter(uriParam, "$uriValuePrefix:$reversedFilter")
|
||||
}
|
||||
else -> {
|
||||
uri.appendQueryParameter(uriParam, "$uriValuePrefix:${vals[state]}")
|
||||
}
|
||||
}
|
||||
uri.appendQueryParameter(uriParam, "$uriValuePrefix:${vals[state]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -340,11 +321,6 @@ open class MyReadingManga(override val lang: String, private val siteLang: Strin
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Mobile Safari/537.36"
|
||||
}
|
||||
|
||||
private fun randomString(length: Int): String {
|
||||
val charPool = ('a'..'z') + ('A'..'Z')
|
||||
return List(length) { charPool.random() }.joinToString("")
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.230 Mobile Safari/537.36"
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.namicomi.NamiComiUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter
|
||||
android:autoVerify="false"
|
||||
tools:targetApi="23">
|
||||
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:host="namicomi.com" />
|
||||
<data android:scheme="https" />
|
||||
|
||||
<data android:pathPattern="/.*/title/..*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,114 +0,0 @@
|
||||
content=Content
|
||||
content_rating=Content rating
|
||||
content_rating_genre=Content rating: %s
|
||||
content_rating_mature=Mature
|
||||
content_rating_restricted=Restricted
|
||||
content_rating_safe=Safe
|
||||
content_warnings_drugs=Drugs
|
||||
content_warnings_gambling=Gambling
|
||||
content_warnings_gore=Gore
|
||||
content_warnings_mental_disorders=Mental Disorders
|
||||
content_warnings_physical_abuse=Physical Abuse
|
||||
content_warnings_racism=Racism
|
||||
content_warnings_self_harm=Self-harm
|
||||
content_warnings_sexual_abuse=Sexual Abuse
|
||||
content_warnings_verbal_abuse=Verbal Abuse
|
||||
cover_quality=Cover quality
|
||||
cover_quality_low=Low
|
||||
cover_quality_medium=Medium
|
||||
cover_quality_original=Original
|
||||
data_saver=Data saver
|
||||
data_saver_summary=Enables smaller, more compressed images
|
||||
error_payment_required=Payment required. Chapter requires a premium subscription
|
||||
excluded_tags_mode=Excluded tags mode
|
||||
format=Format
|
||||
format_4_koma=4-Koma
|
||||
format_adaptation=Adaptation
|
||||
format_anthology=Anthology
|
||||
format_full_color=Full Color
|
||||
format_oneshot=Oneshot
|
||||
format_silent=Silent
|
||||
genre=Genre
|
||||
genre_action=Action
|
||||
genre_adventure=Adventure
|
||||
genre_boys_love=Boys' Love
|
||||
genre_comedy=Comedy
|
||||
genre_crime=Crime
|
||||
genre_drama=Drama
|
||||
genre_fantasy=Fantasy
|
||||
genre_girls_love=Girls' Love
|
||||
genre_historical=Historical
|
||||
genre_horror=Horror
|
||||
genre_isekai=Isekai
|
||||
genre_mecha=Mecha
|
||||
genre_medical=Medical
|
||||
genre_mystery=Mystery
|
||||
genre_philosophical=Philosophical
|
||||
genre_psychological=Psychological
|
||||
genre_romance=Romance
|
||||
genre_sci_fi=Sci-Fi
|
||||
genre_slice_of_life=Slice of Life
|
||||
genre_sports=Sports
|
||||
genre_superhero=Superhero
|
||||
genre_thriller=Thriller
|
||||
genre_tragedy=Tragedy
|
||||
genre_wuxia=Wuxia
|
||||
has_available_chapters=Has available chapters
|
||||
included_tags_mode=Included tags mode
|
||||
invalid_manga_id=Not a valid title ID
|
||||
mode_and=And
|
||||
mode_or=Or
|
||||
show_locked_chapters=Show locked/paywalled chapters
|
||||
show_locked_chapters_summary=Display chapters that require an account with a premium subscription
|
||||
sort=Sort
|
||||
sort_alphabetic=Alphabetic
|
||||
sort_content_created_at=Content created at
|
||||
sort_number_of_chapters=Chapter count
|
||||
sort_number_of_comments=Comment count
|
||||
sort_number_of_follows=Followers
|
||||
sort_number_of_likes=Likes
|
||||
sort_rating=Rating
|
||||
sort_views=Views
|
||||
sort_year=Year
|
||||
status=Status
|
||||
status_cancelled=Cancelled
|
||||
status_completed=Completed
|
||||
status_hiatus=Hiatus
|
||||
status_ongoing=Ongoing
|
||||
tags_mode=Tags mode
|
||||
theme=Theme
|
||||
theme_aliens=Aliens
|
||||
theme_animals=Animals
|
||||
theme_cooking=Cooking
|
||||
theme_crossdressing=Crossdressing
|
||||
theme_delinquents=Delinquents
|
||||
theme_demons=Demons
|
||||
theme_genderswap=Genderswap
|
||||
theme_ghosts=Ghosts
|
||||
theme_gyaru=Gyaru
|
||||
theme_harem=Harem
|
||||
theme_mafia=Mafia
|
||||
theme_magic=Magic
|
||||
theme_magical_girls=Magical Girls
|
||||
theme_martial_arts=Martial Arts
|
||||
theme_military=Military
|
||||
theme_monster_girls=Monster Girls
|
||||
theme_monsters=Monsters
|
||||
theme_music=Music
|
||||
theme_ninja=Ninja
|
||||
theme_office_workers=Office Workers
|
||||
theme_police=Police
|
||||
theme_post_apocalyptic=Post-Apocalyptic
|
||||
theme_reincarnation=Reincarnation
|
||||
theme_reverse_harem=Reverse Harem
|
||||
theme_samurai=Samurai
|
||||
theme_school_life=School Life
|
||||
theme_supernatural=Supernatural
|
||||
theme_survival=Survival
|
||||
theme_time_travel=Time Travel
|
||||
theme_traditional_games=Traditional Games
|
||||
theme_vampires=Vampires
|
||||
theme_video_games=Video Games
|
||||
theme_villainess=Villainess
|
||||
theme_virtual_reality=Virtual Reality
|
||||
theme_zombies=Zombies
|
@ -1,12 +0,0 @@
|
||||
ext {
|
||||
extName = 'NamiComi'
|
||||
extClass = '.NamiComiFactory'
|
||||
extVersionCode = 2
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
||||
dependencies {
|
||||
implementation(project(":lib:i18n"))
|
||||
}
|
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 11 KiB |
@ -1,359 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ChapterListDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityAccessMapDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityAccessRequestDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityAccessRequestItemDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.MangaDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.MangaListDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.PageListDto
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import okio.IOException
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
abstract class NamiComi(final override val lang: String, private val extLang: String = lang) :
|
||||
ConfigurableSource, HttpSource() {
|
||||
|
||||
override val name = "NamiComi"
|
||||
override val baseUrl = NamiComiConstants.webUrl
|
||||
override val supportsLatest = true
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
private val helper = NamiComiHelper(lang)
|
||||
|
||||
final override fun headersBuilder() = super.headersBuilder().apply {
|
||||
set("Referer", "$baseUrl/")
|
||||
set("Origin", baseUrl)
|
||||
}
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.rateLimit(3)
|
||||
.addNetworkInterceptor { chain ->
|
||||
val response = chain.proceed(chain.request())
|
||||
|
||||
if (response.code == 402) {
|
||||
response.close()
|
||||
throw IOException(helper.intl["error_payment_required"])
|
||||
}
|
||||
|
||||
return@addNetworkInterceptor response
|
||||
}
|
||||
.build()
|
||||
|
||||
private fun sortedMangaRequest(page: Int, orderBy: String): Request {
|
||||
val url = NamiComiConstants.apiSearchUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("order[$orderBy]", "desc")
|
||||
.addQueryParameter("availableTranslatedLanguages[]", extLang)
|
||||
.addQueryParameter("limit", NamiComiConstants.mangaLimit.toString())
|
||||
.addQueryParameter("offset", helper.getMangaListOffset(page))
|
||||
.addCommonIncludeParameters()
|
||||
.build()
|
||||
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
// Popular manga section
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
sortedMangaRequest(page, "views")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage =
|
||||
mangaListParse(response)
|
||||
|
||||
// Latest manga section
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
sortedMangaRequest(page, "publishedAt")
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||
mangaListParse(response)
|
||||
|
||||
private fun mangaListParse(response: Response): MangasPage {
|
||||
if (response.code == 204) {
|
||||
return MangasPage(emptyList(), false)
|
||||
}
|
||||
|
||||
val mangaListDto = response.parseAs<MangaListDto>()
|
||||
val mangaList = mangaListDto.data.map { mangaDataDto ->
|
||||
helper.createManga(
|
||||
mangaDataDto,
|
||||
extLang,
|
||||
preferences.coverQuality,
|
||||
)
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, mangaListDto.meta.hasNextPage)
|
||||
}
|
||||
|
||||
// Search manga section
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (query.startsWith(NamiComiConstants.prefixIdSearch)) {
|
||||
val mangaId = query.removePrefix(NamiComiConstants.prefixIdSearch)
|
||||
|
||||
if (mangaId.isEmpty()) {
|
||||
throw Exception(helper.intl["invalid_manga_id"])
|
||||
}
|
||||
|
||||
// If the query is an ID, return the manga directly
|
||||
val url = NamiComiConstants.apiSearchUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("ids[]", query.removePrefix(NamiComiConstants.prefixIdSearch))
|
||||
.addCommonIncludeParameters()
|
||||
.build()
|
||||
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
val tempUrl = NamiComiConstants.apiSearchUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("limit", NamiComiConstants.mangaLimit.toString())
|
||||
.addQueryParameter("offset", helper.getMangaListOffset(page))
|
||||
.addCommonIncludeParameters()
|
||||
|
||||
val actualQuery = query.replace(NamiComiConstants.whitespaceRegex, " ")
|
||||
if (actualQuery.isNotBlank()) {
|
||||
tempUrl.addQueryParameter("title", actualQuery)
|
||||
}
|
||||
|
||||
val finalUrl = helper.filters.addFiltersToUrl(
|
||||
url = tempUrl,
|
||||
filters = filters.ifEmpty { getFilterList() },
|
||||
extLang = extLang,
|
||||
)
|
||||
|
||||
return GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = popularMangaParse(response)
|
||||
|
||||
// Manga Details section
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String =
|
||||
"$baseUrl/$extLang/title/${manga.url}/${helper.titleToSlug(manga.title)}"
|
||||
|
||||
/**
|
||||
* Get the API endpoint URL for the entry details.
|
||||
*/
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val url = ("${NamiComiConstants.apiMangaUrl}/${manga.url}").toHttpUrl().newBuilder()
|
||||
.addCommonIncludeParameters()
|
||||
.build()
|
||||
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val manga = response.parseAs<MangaDto>()
|
||||
|
||||
return helper.createManga(
|
||||
manga.data!!,
|
||||
extLang,
|
||||
preferences.coverQuality,
|
||||
)
|
||||
}
|
||||
|
||||
// Chapter list section
|
||||
|
||||
/**
|
||||
* Get the API endpoint URL for the first page of chapter list.
|
||||
*/
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return paginatedChapterListRequest(manga.url, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Required because the chapter list API endpoint is paginated.
|
||||
*/
|
||||
private fun paginatedChapterListRequest(mangaId: String, offset: Int): Request {
|
||||
val url = NamiComiConstants.apiChapterUrl.toHttpUrl().newBuilder()
|
||||
.addQueryParameter("titleId", mangaId)
|
||||
.addQueryParameter("includes[]", NamiComiConstants.organization)
|
||||
.addQueryParameter("limit", "200")
|
||||
.addQueryParameter("offset", offset.toString())
|
||||
.addQueryParameter("translatedLanguages[]", extLang)
|
||||
.addQueryParameter("order[volume]", "desc")
|
||||
.addQueryParameter("order[chapter]", "desc")
|
||||
.toString()
|
||||
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests information about gated chapters (requiring payment & login).
|
||||
*/
|
||||
private fun accessibleChapterListRequest(chapterIds: List<String>): Request {
|
||||
return POST(
|
||||
NamiComiConstants.apiGatingCheckUrl,
|
||||
headers,
|
||||
chapterIds
|
||||
.map { EntityAccessRequestItemDto(it, NamiComiConstants.chapter) }
|
||||
.let { helper.json.encodeToString(EntityAccessRequestDto(it)) }
|
||||
.toRequestBody(),
|
||||
CacheControl.FORCE_NETWORK,
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
if (response.code == 204) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val mangaId = response.request.url.queryParameter("titleId")!!
|
||||
|
||||
val chapterListResponse = response.parseAs<ChapterListDto>()
|
||||
val chapterListResults = chapterListResponse.data.toMutableList()
|
||||
var offset = chapterListResponse.meta.offset
|
||||
var hasNextPage = chapterListResponse.meta.hasNextPage
|
||||
|
||||
// Max results that can be returned is 200 so need to make more API
|
||||
// calls if the chapter list response has a next page.
|
||||
while (hasNextPage) {
|
||||
offset += chapterListResponse.meta.limit
|
||||
|
||||
val newRequest = paginatedChapterListRequest(mangaId, offset)
|
||||
val newResponse = client.newCall(newRequest).execute()
|
||||
val newChapterList = newResponse.parseAs<ChapterListDto>()
|
||||
chapterListResults.addAll(newChapterList.data)
|
||||
|
||||
hasNextPage = newChapterList.meta.hasNextPage
|
||||
}
|
||||
|
||||
// If there are no chapters, don't attempt to check gating
|
||||
if (chapterListResults.isEmpty()) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Split chapter access checks into chunks of 200 chapters
|
||||
val chapterListResultsChunks = chapterListResults.map { it.id }.chunked(200)
|
||||
val accessibleChapterMap: MutableMap<String, Boolean> = mutableMapOf()
|
||||
|
||||
for (chapterIds in chapterListResultsChunks) {
|
||||
val gatingCheckRequest = accessibleChapterListRequest(chapterIds)
|
||||
val gatingCheckResponse = client.newCall(gatingCheckRequest).execute()
|
||||
accessibleChapterMap += gatingCheckResponse.parseAs<EntityAccessMapDto>()
|
||||
.data?.attributes?.map ?: emptyMap()
|
||||
}
|
||||
|
||||
return chapterListResults.mapNotNull {
|
||||
val isAccessible = accessibleChapterMap[it.id]!!
|
||||
when {
|
||||
// Chapter can be viewed
|
||||
isAccessible -> helper.createChapter(it)
|
||||
// Chapter cannot be viewed and user wants to see locked chapters
|
||||
preferences.showLockedChapters -> {
|
||||
helper.createChapter(it).apply {
|
||||
name = "${NamiComiConstants.lockSymbol} $name"
|
||||
}
|
||||
}
|
||||
// Ignore locked chapters otherwise
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String =
|
||||
"$baseUrl/$extLang/chapter/${chapter.url}"
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val chapterId = chapter.url
|
||||
val url = "${NamiComiConstants.apiUrl}/images/chapter/$chapterId?newQualities=true"
|
||||
return GET(url, headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val chapterId = response.request.url.pathSegments.last()
|
||||
val pageListDataDto = response.parseAs<PageListDto>().data ?: return emptyList()
|
||||
|
||||
val hash = pageListDataDto.hash
|
||||
val prefix = "${pageListDataDto.baseUrl}/chapter/$chapterId/$hash"
|
||||
|
||||
val urls = if (preferences.useDataSaver) {
|
||||
pageListDataDto.low.map { prefix + "/low/${it.filename}" }
|
||||
} else {
|
||||
pageListDataDto.source.map { prefix + "/source/${it.filename}" }
|
||||
}
|
||||
|
||||
return urls.mapIndexed { index, url ->
|
||||
Page(index, url, url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val coverQualityPref = ListPreference(screen.context).apply {
|
||||
key = NamiComiConstants.getCoverQualityPreferenceKey(extLang)
|
||||
title = helper.intl["cover_quality"]
|
||||
entries = NamiComiConstants.getCoverQualityPreferenceEntries(helper.intl)
|
||||
entryValues = NamiComiConstants.getCoverQualityPreferenceEntryValues()
|
||||
setDefaultValue(NamiComiConstants.getCoverQualityPreferenceDefaultValue())
|
||||
summary = "%s"
|
||||
}
|
||||
|
||||
val dataSaverPref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = NamiComiConstants.getDataSaverPreferenceKey(extLang)
|
||||
title = helper.intl["data_saver"]
|
||||
summary = helper.intl["data_saver_summary"]
|
||||
setDefaultValue(false)
|
||||
}
|
||||
|
||||
val showLockedChaptersPref = SwitchPreferenceCompat(screen.context).apply {
|
||||
key = NamiComiConstants.getShowLockedChaptersPreferenceKey(extLang)
|
||||
title = helper.intl["show_locked_chapters"]
|
||||
summary = helper.intl["show_locked_chapters_summary"]
|
||||
setDefaultValue(false)
|
||||
}
|
||||
|
||||
screen.addPreference(coverQualityPref)
|
||||
screen.addPreference(dataSaverPref)
|
||||
screen.addPreference(showLockedChaptersPref)
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList =
|
||||
helper.filters.getFilterList(helper.intl)
|
||||
|
||||
private fun HttpUrl.Builder.addCommonIncludeParameters() =
|
||||
this.addQueryParameter("includes[]", NamiComiConstants.coverArt)
|
||||
.addQueryParameter("includes[]", NamiComiConstants.organization)
|
||||
.addQueryParameter("includes[]", NamiComiConstants.tag)
|
||||
.addQueryParameter("includes[]", NamiComiConstants.primaryTag)
|
||||
.addQueryParameter("includes[]", NamiComiConstants.secondaryTag)
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T = use {
|
||||
helper.json.decodeFromString(body.string())
|
||||
}
|
||||
|
||||
private val SharedPreferences.coverQuality
|
||||
get() = getString(NamiComiConstants.getCoverQualityPreferenceKey(extLang), "")
|
||||
|
||||
private val SharedPreferences.useDataSaver
|
||||
get() = getBoolean(NamiComiConstants.getDataSaverPreferenceKey(extLang), false)
|
||||
|
||||
private val SharedPreferences.showLockedChapters
|
||||
get() = getBoolean(NamiComiConstants.getShowLockedChaptersPreferenceKey(extLang), false)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
object NamiComiConstants {
|
||||
const val mangaLimit = 20
|
||||
|
||||
val whitespaceRegex = "\\s".toRegex()
|
||||
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
const val lockSymbol = "🔒"
|
||||
|
||||
// Language codes used for translations
|
||||
const val english = "en"
|
||||
|
||||
// JSON discriminators
|
||||
const val chapter = "chapter"
|
||||
const val manga = "title"
|
||||
const val coverArt = "cover_art"
|
||||
const val organization = "organization"
|
||||
const val tag = "tag"
|
||||
const val primaryTag = "primary_tag"
|
||||
const val secondaryTag = "secondary_tag"
|
||||
const val imageData = "image_data"
|
||||
const val entityAccessMap = "entity_access_map"
|
||||
|
||||
// URLs & API endpoints
|
||||
const val webUrl = "https://namicomi.com"
|
||||
const val cdnUrl = "https://uploads.namicomi.com"
|
||||
const val apiUrl = "https://api.namicomi.com"
|
||||
const val apiMangaUrl = "$apiUrl/title"
|
||||
const val apiSearchUrl = "$apiMangaUrl/search"
|
||||
const val apiChapterUrl = "$apiUrl/chapter"
|
||||
const val apiGatingCheckUrl = "$apiUrl/gating/check"
|
||||
|
||||
// Search prefix for title ids
|
||||
const val prefixIdSearch = "id:"
|
||||
|
||||
// Preferences
|
||||
private const val coverQualityPref = "thumbnailQuality"
|
||||
fun getCoverQualityPreferenceKey(extLang: String): String = "${coverQualityPref}_$extLang"
|
||||
fun getCoverQualityPreferenceEntries(intl: Intl) =
|
||||
arrayOf(intl["cover_quality_original"], intl["cover_quality_medium"], intl["cover_quality_low"])
|
||||
fun getCoverQualityPreferenceEntryValues() = arrayOf("", ".512.jpg", ".256.jpg")
|
||||
fun getCoverQualityPreferenceDefaultValue() = getCoverQualityPreferenceEntryValues()[0]
|
||||
|
||||
private const val dataSaverPref = "dataSaver"
|
||||
fun getDataSaverPreferenceKey(extLang: String): String = "${dataSaverPref}_$extLang"
|
||||
|
||||
private const val showLockedChaptersPref = "showLockedChapters"
|
||||
fun getShowLockedChaptersPreferenceKey(extLang: String): String = "${showLockedChaptersPref}_$extLang"
|
||||
|
||||
// Tag types
|
||||
private const val tagGroupContent = "content-warnings"
|
||||
private const val tagGroupFormat = "format"
|
||||
private const val tagGroupGenre = "genre"
|
||||
private const val tagGroupTheme = "theme"
|
||||
val tagGroupsOrder = arrayOf(tagGroupContent, tagGroupFormat, tagGroupGenre, tagGroupTheme)
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class NamiComiFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
NamiComiEnglish(),
|
||||
NamiComiArabic(),
|
||||
NamiComiBulgarian(),
|
||||
NamiComiCatalan(),
|
||||
NamiComiChineseSimplified(),
|
||||
NamiComiChineseTraditional(),
|
||||
NamiComiCroatian(),
|
||||
NamiComiCzech(),
|
||||
NamiComiDanish(),
|
||||
NamiComiDutch(),
|
||||
NamiComiEstonian(),
|
||||
NamiComiFilipino(),
|
||||
NamiComiFinnish(),
|
||||
NamiComiFrench(),
|
||||
NamiComiGerman(),
|
||||
NamiComiGreek(),
|
||||
NamiComiHebrew(),
|
||||
NamiComiHindi(),
|
||||
NamiComiHungarian(),
|
||||
NamiComiIcelandic(),
|
||||
NamiComiIrish(),
|
||||
NamiComiIndonesian(),
|
||||
NamiComiItalian(),
|
||||
NamiComiJapanese(),
|
||||
NamiComiKorean(),
|
||||
NamiComiLithuanian(),
|
||||
NamiComiMalay(),
|
||||
NamiComiNepali(),
|
||||
NamiComiNorwegian(),
|
||||
NamiComiPanjabi(),
|
||||
NamiComiPersian(),
|
||||
NamiComiPolish(),
|
||||
NamiComiPortugueseBrazil(),
|
||||
NamiComiPortuguesePortugal(),
|
||||
NamiComiRussian(),
|
||||
NamiComiSlovak(),
|
||||
NamiComiSlovenian(),
|
||||
NamiComiSpanishLatinAmerica(),
|
||||
NamiComiSpanishSpain(),
|
||||
NamiComiSwedish(),
|
||||
NamiComiThai(),
|
||||
NamiComiTurkish(),
|
||||
NamiComiUkrainian(),
|
||||
)
|
||||
}
|
||||
|
||||
class NamiComiArabic : NamiComi("ar")
|
||||
class NamiComiBulgarian : NamiComi("bg")
|
||||
class NamiComiCatalan : NamiComi("ca")
|
||||
class NamiComiChineseSimplified : NamiComi("zh-Hans", "zh-hans")
|
||||
class NamiComiChineseTraditional : NamiComi("zh-Hant", "zh-hant")
|
||||
class NamiComiCroatian : NamiComi("hr")
|
||||
class NamiComiCzech : NamiComi("cs")
|
||||
class NamiComiDanish : NamiComi("da")
|
||||
class NamiComiDutch : NamiComi("nl")
|
||||
class NamiComiEnglish : NamiComi("en")
|
||||
class NamiComiEstonian : NamiComi("et")
|
||||
class NamiComiFilipino : NamiComi("fil")
|
||||
class NamiComiFinnish : NamiComi("fi")
|
||||
class NamiComiFrench : NamiComi("fr")
|
||||
class NamiComiGerman : NamiComi("de")
|
||||
class NamiComiGreek : NamiComi("el")
|
||||
class NamiComiHebrew : NamiComi("he")
|
||||
class NamiComiHindi : NamiComi("hi")
|
||||
class NamiComiHungarian : NamiComi("hu")
|
||||
class NamiComiIcelandic : NamiComi("is")
|
||||
class NamiComiIrish : NamiComi("ga")
|
||||
class NamiComiIndonesian : NamiComi("id")
|
||||
class NamiComiItalian : NamiComi("it")
|
||||
class NamiComiJapanese : NamiComi("ja")
|
||||
class NamiComiKorean : NamiComi("ko")
|
||||
class NamiComiLithuanian : NamiComi("lt")
|
||||
class NamiComiMalay : NamiComi("ms")
|
||||
class NamiComiNepali : NamiComi("ne")
|
||||
class NamiComiNorwegian : NamiComi("no")
|
||||
class NamiComiPanjabi : NamiComi("pa")
|
||||
class NamiComiPersian : NamiComi("fa")
|
||||
class NamiComiPolish : NamiComi("pl")
|
||||
class NamiComiPortugueseBrazil : NamiComi("pt-BR", "pt-br")
|
||||
class NamiComiPortuguesePortugal : NamiComi("pt", "pt-pt")
|
||||
class NamiComiRussian : NamiComi("ru")
|
||||
class NamiComiSlovak : NamiComi("sk")
|
||||
class NamiComiSlovenian : NamiComi("sl")
|
||||
class NamiComiSpanishLatinAmerica : NamiComi("es-419")
|
||||
class NamiComiSpanishSpain : NamiComi("es", "es-es")
|
||||
class NamiComiSwedish : NamiComi("sv")
|
||||
class NamiComiThai : NamiComi("th")
|
||||
class NamiComiTurkish : NamiComi("tr")
|
||||
class NamiComiUkrainian : NamiComi("uk")
|
@ -1,289 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ContentRatingDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.StatusDto
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
|
||||
class NamiComiFilters {
|
||||
|
||||
internal fun getFilterList(intl: Intl): FilterList = FilterList(
|
||||
HasAvailableChaptersFilter(intl),
|
||||
ContentRatingList(intl, getContentRatings(intl)),
|
||||
StatusList(intl, getStatus(intl)),
|
||||
SortFilter(intl, getSortables(intl)),
|
||||
TagsFilter(intl, getTagFilters(intl)),
|
||||
TagList(intl["content"], getContents(intl)),
|
||||
TagList(intl["format"], getFormats(intl)),
|
||||
TagList(intl["genre"], getGenres(intl)),
|
||||
TagList(intl["theme"], getThemes(intl)),
|
||||
)
|
||||
|
||||
private interface UrlQueryFilter {
|
||||
fun addQueryParameter(url: HttpUrl.Builder, extLang: String)
|
||||
}
|
||||
|
||||
private class HasAvailableChaptersFilter(intl: Intl) :
|
||||
Filter.CheckBox(intl["has_available_chapters"]),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
if (state) {
|
||||
url.addQueryParameter("hasAvailableChapters", "true")
|
||||
url.addQueryParameter("availableTranslatedLanguages[]", extLang)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ContentRating(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class ContentRatingList(intl: Intl, contentRating: List<ContentRating>) :
|
||||
Filter.Group<ContentRating>(intl["content_rating"], contentRating),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
state.filter(ContentRating::state)
|
||||
.forEach { url.addQueryParameter("contentRatings[]", it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentRatings(intl: Intl) = listOf(
|
||||
ContentRating(intl["content_rating_safe"], ContentRatingDto.SAFE.value),
|
||||
ContentRating(intl["content_rating_restricted"], ContentRatingDto.RESTRICTED.value),
|
||||
ContentRating(intl["content_rating_mature"], ContentRatingDto.MATURE.value),
|
||||
)
|
||||
|
||||
private class Status(name: String, val value: String) : Filter.CheckBox(name)
|
||||
private class StatusList(intl: Intl, status: List<Status>) :
|
||||
Filter.Group<Status>(intl["status"], status),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
state.filter(Status::state)
|
||||
.forEach { url.addQueryParameter("publicationStatuses[]", it.value) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getStatus(intl: Intl) = listOf(
|
||||
Status(intl["status_ongoing"], StatusDto.ONGOING.value),
|
||||
Status(intl["status_completed"], StatusDto.COMPLETED.value),
|
||||
Status(intl["status_hiatus"], StatusDto.HIATUS.value),
|
||||
Status(intl["status_cancelled"], StatusDto.CANCELLED.value),
|
||||
)
|
||||
|
||||
data class Sortable(val title: String, val value: String) {
|
||||
override fun toString(): String = title
|
||||
}
|
||||
|
||||
private fun getSortables(intl: Intl) = arrayOf(
|
||||
Sortable(intl["sort_alphabetic"], "title"),
|
||||
Sortable(intl["sort_number_of_chapters"], "chapterCount"),
|
||||
Sortable(intl["sort_number_of_follows"], "followCount"),
|
||||
Sortable(intl["sort_number_of_likes"], "reactions"),
|
||||
Sortable(intl["sort_number_of_comments"], "commentCount"),
|
||||
Sortable(intl["sort_content_created_at"], "publishedAt"),
|
||||
Sortable(intl["sort_views"], "views"),
|
||||
Sortable(intl["sort_year"], "year"),
|
||||
Sortable(intl["sort_rating"], "rating"),
|
||||
)
|
||||
|
||||
class SortFilter(intl: Intl, private val sortables: Array<Sortable>) :
|
||||
Filter.Sort(
|
||||
intl["sort"],
|
||||
sortables.map(Sortable::title).toTypedArray(),
|
||||
Selection(5, false),
|
||||
),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
if (state != null) {
|
||||
val query = sortables[state!!.index].value
|
||||
val value = if (state!!.ascending) "asc" else "desc"
|
||||
|
||||
url.addQueryParameter("order[$query]", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Tag(val id: String, name: String) : Filter.TriState(name)
|
||||
|
||||
private class TagList(collection: String, tags: List<Tag>) :
|
||||
Filter.Group<Tag>(collection, tags),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
state.forEach { tag ->
|
||||
if (tag.isIncluded()) {
|
||||
url.addQueryParameter("includedTags[]", tag.id)
|
||||
} else if (tag.isExcluded()) {
|
||||
url.addQueryParameter("excludedTags[]", tag.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContents(intl: Intl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("drugs", intl["content_warnings_drugs"]),
|
||||
Tag("gambling", intl["content_warnings_gambling"]),
|
||||
Tag("gore", intl["content_warnings_gore"]),
|
||||
Tag("mental-disorders", intl["content_warnings_mental_disorders"]),
|
||||
Tag("physical-abuse", intl["content_warnings_physical_abuse"]),
|
||||
Tag("racism", intl["content_warnings_racism"]),
|
||||
Tag("self-harm", intl["content_warnings_self_harm"]),
|
||||
Tag("sexual-abuse", intl["content_warnings_sexual_abuse"]),
|
||||
Tag("verbal-abuse", intl["content_warnings_verbal_abuse"]),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
private fun getFormats(intl: Intl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("4-koma", intl["format_4_koma"]),
|
||||
Tag("adaptation", intl["format_adaptation"]),
|
||||
Tag("anthology", intl["format_anthology"]),
|
||||
Tag("full-color", intl["format_full_color"]),
|
||||
Tag("oneshot", intl["format_oneshot"]),
|
||||
Tag("silent", intl["format_silent"]),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
private fun getGenres(intl: Intl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("action", intl["genre_action"]),
|
||||
Tag("adventure", intl["genre_adventure"]),
|
||||
Tag("boys-love", intl["genre_boys_love"]),
|
||||
Tag("comedy", intl["genre_comedy"]),
|
||||
Tag("crime", intl["genre_crime"]),
|
||||
Tag("drama", intl["genre_drama"]),
|
||||
Tag("fantasy", intl["genre_fantasy"]),
|
||||
Tag("girls-love", intl["genre_girls_love"]),
|
||||
Tag("historical", intl["genre_historical"]),
|
||||
Tag("horror", intl["genre_horror"]),
|
||||
Tag("isekai", intl["genre_isekai"]),
|
||||
Tag("mecha", intl["genre_mecha"]),
|
||||
Tag("medical", intl["genre_medical"]),
|
||||
Tag("mystery", intl["genre_mystery"]),
|
||||
Tag("philosophical", intl["genre_philosophical"]),
|
||||
Tag("psychological", intl["genre_psychological"]),
|
||||
Tag("romance", intl["genre_romance"]),
|
||||
Tag("sci-fi", intl["genre_sci_fi"]),
|
||||
Tag("slice-of-life", intl["genre_slice_of_life"]),
|
||||
Tag("sports", intl["genre_sports"]),
|
||||
Tag("superhero", intl["genre_superhero"]),
|
||||
Tag("thriller", intl["genre_thriller"]),
|
||||
Tag("tragedy", intl["genre_tragedy"]),
|
||||
Tag("wuxia", intl["genre_wuxia"]),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
private fun getThemes(intl: Intl): List<Tag> {
|
||||
val tags = listOf(
|
||||
Tag("aliens", intl["theme_aliens"]),
|
||||
Tag("animals", intl["theme_animals"]),
|
||||
Tag("cooking", intl["theme_cooking"]),
|
||||
Tag("crossdressing", intl["theme_crossdressing"]),
|
||||
Tag("delinquents", intl["theme_delinquents"]),
|
||||
Tag("demons", intl["theme_demons"]),
|
||||
Tag("genderswap", intl["theme_genderswap"]),
|
||||
Tag("ghosts", intl["theme_ghosts"]),
|
||||
Tag("gyaru", intl["theme_gyaru"]),
|
||||
Tag("harem", intl["theme_harem"]),
|
||||
Tag("mafia", intl["theme_mafia"]),
|
||||
Tag("magic", intl["theme_magic"]),
|
||||
Tag("magical-girls", intl["theme_magical_girls"]),
|
||||
Tag("martial-arts", intl["theme_martial_arts"]),
|
||||
Tag("military", intl["theme_military"]),
|
||||
Tag("monster-girls", intl["theme_monster_girls"]),
|
||||
Tag("monsters", intl["theme_monsters"]),
|
||||
Tag("music", intl["theme_music"]),
|
||||
Tag("ninja", intl["theme_ninja"]),
|
||||
Tag("office-workers", intl["theme_office_workers"]),
|
||||
Tag("police", intl["theme_police"]),
|
||||
Tag("post-apocalyptic", intl["theme_post_apocalyptic"]),
|
||||
Tag("reincarnation", intl["theme_reincarnation"]),
|
||||
Tag("reverse-harem", intl["theme_reverse_harem"]),
|
||||
Tag("samurai", intl["theme_samurai"]),
|
||||
Tag("school-life", intl["theme_school_life"]),
|
||||
Tag("supernatural", intl["theme_supernatural"]),
|
||||
Tag("survival", intl["theme_survival"]),
|
||||
Tag("time-travel", intl["theme_time_travel"]),
|
||||
Tag("traditional-games", intl["theme_traditional_games"]),
|
||||
Tag("vampires", intl["theme_vampires"]),
|
||||
Tag("video-games", intl["theme_video_games"]),
|
||||
Tag("villainess", intl["theme_villainess"]),
|
||||
Tag("virtual-reality", intl["theme_virtual_reality"]),
|
||||
Tag("zombies", intl["theme_zombies"]),
|
||||
)
|
||||
|
||||
return tags.sortIfTranslated(intl)
|
||||
}
|
||||
|
||||
// Tags taken from: https://api.namicomi.com/title/tags
|
||||
internal fun getTags(intl: Intl): List<Tag> {
|
||||
return getContents(intl) + getFormats(intl) + getGenres(intl) + getThemes(intl)
|
||||
}
|
||||
|
||||
private data class TagMode(val title: String, val value: String) {
|
||||
override fun toString(): String = title
|
||||
}
|
||||
|
||||
private fun getTagModes(intl: Intl) = arrayOf(
|
||||
TagMode(intl["mode_and"], "and"),
|
||||
TagMode(intl["mode_or"], "or"),
|
||||
)
|
||||
|
||||
private class TagInclusionMode(intl: Intl, modes: Array<TagMode>) :
|
||||
Filter.Select<TagMode>(intl["included_tags_mode"], modes, 0),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
url.addQueryParameter("includedTagsMode", values[state].value)
|
||||
}
|
||||
}
|
||||
|
||||
private class TagExclusionMode(intl: Intl, modes: Array<TagMode>) :
|
||||
Filter.Select<TagMode>(intl["excluded_tags_mode"], modes, 1),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
url.addQueryParameter("excludedTagsMode", values[state].value)
|
||||
}
|
||||
}
|
||||
|
||||
private class TagsFilter(intl: Intl, innerFilters: FilterList) :
|
||||
Filter.Group<Filter<*>>(intl["tags_mode"], innerFilters),
|
||||
UrlQueryFilter {
|
||||
|
||||
override fun addQueryParameter(url: HttpUrl.Builder, extLang: String) {
|
||||
state.filterIsInstance<UrlQueryFilter>()
|
||||
.forEach { filter -> filter.addQueryParameter(url, extLang) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTagFilters(intl: Intl): FilterList = FilterList(
|
||||
TagInclusionMode(intl, getTagModes(intl)),
|
||||
TagExclusionMode(intl, getTagModes(intl)),
|
||||
)
|
||||
|
||||
internal fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList, extLang: String): HttpUrl {
|
||||
filters.filterIsInstance<UrlQueryFilter>()
|
||||
.forEach { filter -> filter.addQueryParameter(url, extLang) }
|
||||
|
||||
return url.build()
|
||||
}
|
||||
|
||||
private fun List<Tag>.sortIfTranslated(intl: Intl): List<Tag> = apply {
|
||||
if (intl.chosenLanguage == NamiComiConstants.english) {
|
||||
return this
|
||||
}
|
||||
|
||||
return sortedWith(compareBy(intl.collator, Tag::name))
|
||||
}
|
||||
}
|
@ -1,182 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.AbstractTagDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ChapterDataDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.ContentRatingDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.CoverArtDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.EntityDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.MangaDataDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.OrganizationDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.StatusDto
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.dto.UnknownEntity
|
||||
import eu.kanade.tachiyomi.lib.i18n.Intl
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.plus
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
import java.util.Locale
|
||||
|
||||
class NamiComiHelper(lang: String) {
|
||||
|
||||
val filters = NamiComiFilters()
|
||||
|
||||
val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule += SerializersModule {
|
||||
polymorphic(EntityDto::class) {
|
||||
defaultDeserializer { UnknownEntity.serializer() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val intl = Intl(
|
||||
language = lang,
|
||||
baseLanguage = NamiComiConstants.english,
|
||||
availableLanguages = setOf(NamiComiConstants.english),
|
||||
classLoader = this::class.java.classLoader!!,
|
||||
createMessageFileName = { lang -> Intl.createDefaultMessageFileName(lang) },
|
||||
)
|
||||
|
||||
/**
|
||||
* Get the manga offset pages are 1 based, so subtract 1
|
||||
*/
|
||||
fun getMangaListOffset(page: Int): String = (NamiComiConstants.mangaLimit * (page - 1)).toString()
|
||||
|
||||
private fun getPublicationStatus(mangaDataDto: MangaDataDto): Int {
|
||||
return when (mangaDataDto.attributes!!.publicationStatus) {
|
||||
StatusDto.ONGOING -> SManga.ONGOING
|
||||
StatusDto.CANCELLED -> SManga.CANCELLED
|
||||
StatusDto.COMPLETED -> SManga.COMPLETED
|
||||
StatusDto.HIATUS -> SManga.ON_HIATUS
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(dateAsString: String): Long =
|
||||
NamiComiConstants.dateFormatter.parse(dateAsString)?.time ?: 0
|
||||
|
||||
/**
|
||||
* Create an [SManga] from the JSON element with all attributes filled.
|
||||
*/
|
||||
fun createManga(
|
||||
mangaDataDto: MangaDataDto,
|
||||
lang: String,
|
||||
coverSuffix: String?,
|
||||
): SManga {
|
||||
val attr = mangaDataDto.attributes!!
|
||||
|
||||
// Things that will go with the genre tags but aren't actually genre
|
||||
val extLocale = Locale.forLanguageTag(lang)
|
||||
|
||||
val nonGenres = listOfNotNull(
|
||||
attr.contentRating
|
||||
.takeIf { it != ContentRatingDto.SAFE }
|
||||
?.let { intl.format("content_rating_genre", intl["content_rating_${it.name.lowercase()}"]) },
|
||||
attr.originalLanguage
|
||||
?.let { Locale.forLanguageTag(it) }
|
||||
?.getDisplayName(extLocale)
|
||||
?.replaceFirstChar { it.uppercase(extLocale) },
|
||||
)
|
||||
|
||||
val organization = mangaDataDto.relationships
|
||||
.filterIsInstance<OrganizationDto>()
|
||||
.mapNotNull { it.attributes?.name }
|
||||
.distinct()
|
||||
|
||||
val coverFileName = mangaDataDto.relationships
|
||||
.filterIsInstance<CoverArtDto>()
|
||||
.firstOrNull()
|
||||
?.attributes?.fileName
|
||||
|
||||
val tags = filters.getTags(intl).associate { it.id to it.name }
|
||||
|
||||
val genresMap = mangaDataDto.relationships
|
||||
.filterIsInstance<AbstractTagDto>()
|
||||
.groupBy({ it.attributes!!.group }) { tagDto -> tags[tagDto.id] }
|
||||
.mapValues { it.value.filterNotNull().sortedWith(intl.collator) }
|
||||
|
||||
val genreList = NamiComiConstants.tagGroupsOrder.flatMap { genresMap[it].orEmpty() } + nonGenres
|
||||
|
||||
val desc = (attr.description[lang] ?: attr.description["en"])
|
||||
.orEmpty()
|
||||
|
||||
return SManga.create().apply {
|
||||
initialized = true
|
||||
url = mangaDataDto.id
|
||||
description = desc
|
||||
author = organization.joinToString()
|
||||
status = getPublicationStatus(mangaDataDto)
|
||||
genre = genreList
|
||||
.filter(String::isNotEmpty)
|
||||
.joinToString()
|
||||
|
||||
mangaDataDto.attributes.title.let { titleMap ->
|
||||
title = titleMap[lang] ?: titleMap.values.first()
|
||||
}
|
||||
|
||||
coverFileName?.let {
|
||||
thumbnail_url = when (!coverSuffix.isNullOrEmpty()) {
|
||||
true -> "${NamiComiConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName$coverSuffix"
|
||||
else -> "${NamiComiConstants.cdnUrl}/covers/${mangaDataDto.id}/$coverFileName"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the [SChapter] from the JSON element.
|
||||
*/
|
||||
fun createChapter(chapterDataDto: ChapterDataDto): SChapter {
|
||||
val attr = chapterDataDto.attributes!!
|
||||
val chapterName = mutableListOf<String>()
|
||||
|
||||
attr.volume?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
chapterName.add("Vol.$it")
|
||||
}
|
||||
}
|
||||
|
||||
attr.chapter?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
chapterName.add("Ch.$it")
|
||||
}
|
||||
}
|
||||
|
||||
attr.name?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
if (chapterName.isNotEmpty()) {
|
||||
chapterName.add("-")
|
||||
}
|
||||
chapterName.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
return SChapter.create().apply {
|
||||
url = chapterDataDto.id
|
||||
name = chapterName.joinToString(" ")
|
||||
date_upload = parseDate(attr.publishAt)
|
||||
}
|
||||
}
|
||||
|
||||
fun titleToSlug(title: String) = title.trim()
|
||||
.lowercase(Locale.US)
|
||||
.replace(titleSpecialCharactersRegex, "-")
|
||||
.replace(trailingHyphenRegex, "")
|
||||
.split("-")
|
||||
.reduce { accumulator, element ->
|
||||
val currentSlug = "$accumulator-$element"
|
||||
if (currentSlug.length > 100) {
|
||||
accumulator
|
||||
} else {
|
||||
currentSlug
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val titleSpecialCharactersRegex = "[^a-z0-9]+".toRegex()
|
||||
val trailingHyphenRegex = "-+$".toRegex()
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Springboard that accepts https://namicomi.com/xx/title/yyy intents and redirects them to
|
||||
* the main tachiyomi process. The idea is to not install the intent filter unless
|
||||
* you have this extension installed, but still let the main tachiyomi app control
|
||||
* things.
|
||||
*/
|
||||
class NamiComiUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
||||
// Supported path: /en/title/12345
|
||||
if (pathSegments != null && pathSegments.size > 2) {
|
||||
val titleId = pathSegments[2]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", NamiComiConstants.prefixIdSearch + titleId)
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("NamiComiUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "This URL cannot be handled by the Namicomi extension.", Toast.LENGTH_SHORT).show()
|
||||
Log.e("NamiComiUrlActivity", "Could not parse URI from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias ChapterListDto = PaginatedResponseDto<ChapterDataDto>
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.chapter)
|
||||
class ChapterDataDto(override val attributes: ChapterAttributesDto? = null) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
class ChapterAttributesDto(
|
||||
val name: String?,
|
||||
val volume: String?,
|
||||
val chapter: String?,
|
||||
val pages: Int,
|
||||
val publishAt: String,
|
||||
) : AttributesDto
|
@ -1,15 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.coverArt)
|
||||
class CoverArtDto(override val attributes: CoverArtAttributesDto? = null) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
class CoverArtAttributesDto(
|
||||
val fileName: String? = null,
|
||||
val locale: String? = null,
|
||||
) : AttributesDto
|
@ -1,30 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias EntityAccessMapDto = ResponseDto<EntityAccessMapDataDto>
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.entityAccessMap)
|
||||
class EntityAccessMapDataDto(
|
||||
override val attributes: EntityAccessMapAttributesDto? = null,
|
||||
) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
class EntityAccessMapAttributesDto(
|
||||
// Map of entity IDs to whether the user has access to them
|
||||
val map: Map<String, Boolean>,
|
||||
) : AttributesDto
|
||||
|
||||
@Serializable
|
||||
class EntityAccessRequestDto(
|
||||
val entities: List<EntityAccessRequestItemDto>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class EntityAccessRequestItemDto(
|
||||
val entityId: String,
|
||||
val entityType: String,
|
||||
)
|
@ -1,16 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class EntityDto {
|
||||
val id: String = ""
|
||||
val relationships: List<EntityDto> = emptyList()
|
||||
abstract val attributes: AttributesDto?
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed interface AttributesDto
|
||||
|
||||
@Serializable
|
||||
class UnknownEntity(override val attributes: AttributesDto? = null) : EntityDto()
|
@ -1,70 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias MangaListDto = PaginatedResponseDto<MangaDataDto>
|
||||
|
||||
typealias MangaDto = ResponseDto<MangaDataDto>
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.manga)
|
||||
class MangaDataDto(override val attributes: MangaAttributesDto? = null) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
class MangaAttributesDto(
|
||||
// Title and description are maps of language codes to localized strings
|
||||
val title: Map<String, String>,
|
||||
val description: Map<String, String>,
|
||||
val slug: String,
|
||||
val originalLanguage: String?,
|
||||
val year: Int?,
|
||||
val contentRating: ContentRatingDto? = null,
|
||||
val publicationStatus: StatusDto? = null,
|
||||
) : AttributesDto
|
||||
|
||||
@Serializable
|
||||
enum class ContentRatingDto(val value: String) {
|
||||
@SerialName("safe")
|
||||
SAFE("safe"),
|
||||
|
||||
@SerialName("restricted")
|
||||
RESTRICTED("restricted"),
|
||||
|
||||
@SerialName("mature")
|
||||
MATURE("mature"),
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class StatusDto(val value: String) {
|
||||
@SerialName("ongoing")
|
||||
ONGOING("ongoing"),
|
||||
|
||||
@SerialName("completed")
|
||||
COMPLETED("completed"),
|
||||
|
||||
@SerialName("hiatus")
|
||||
HIATUS("hiatus"),
|
||||
|
||||
@SerialName("cancelled")
|
||||
CANCELLED("cancelled"),
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class AbstractTagDto(override val attributes: TagAttributesDto? = null) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.tag)
|
||||
class TagDto : AbstractTagDto()
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.primaryTag)
|
||||
class PrimaryTagDto : AbstractTagDto()
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.secondaryTag)
|
||||
class SecondaryTagDto : AbstractTagDto()
|
||||
|
||||
@Serializable
|
||||
class TagAttributesDto(val group: String) : AttributesDto
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.organization)
|
||||
class OrganizationDto(override val attributes: OrganizationAttributesDto? = null) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
class OrganizationAttributesDto(val name: String) : AttributesDto
|
@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.namicomi.NamiComiConstants
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
typealias PageListDto = ResponseDto<PageListDataDto>
|
||||
|
||||
@Serializable
|
||||
@SerialName(NamiComiConstants.imageData)
|
||||
class PageListDataDto(
|
||||
override val attributes: AttributesDto? = null,
|
||||
val baseUrl: String,
|
||||
val hash: String,
|
||||
val source: List<PageImageDto>,
|
||||
val high: List<PageImageDto>,
|
||||
val medium: List<PageImageDto>,
|
||||
val low: List<PageImageDto>,
|
||||
) : EntityDto()
|
||||
|
||||
@Serializable
|
||||
class PageImageDto(
|
||||
val size: Int?,
|
||||
val filename: String,
|
||||
val resolution: String?,
|
||||
)
|
@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.namicomi.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class PaginatedResponseDto<T : EntityDto>(
|
||||
val result: String,
|
||||
val data: List<T> = emptyList(),
|
||||
val meta: PaginationStateDto,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ResponseDto<T : EntityDto>(
|
||||
val result: String,
|
||||
val type: String,
|
||||
val data: T? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PaginationStateDto(
|
||||
val limit: Int = 0,
|
||||
val offset: Int = 0,
|
||||
val total: Int = 0,
|
||||
) {
|
||||
val hasNextPage: Boolean
|
||||
get() = limit + offset < total
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
ext {
|
||||
extName = 'DeviantArt'
|
||||
extClass = '.DeviantArt'
|
||||
extName = 'NETCOMICS'
|
||||
extClass = '.NetcomicsFactory'
|
||||
extVersionCode = 3
|
||||
isNsfw = true
|
||||
}
|
BIN
src/all/netcomics/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
src/all/netcomics/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/all/netcomics/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/all/netcomics/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 26 KiB |