Compare commits

..

No commits in common. "988d1b04af44267b8c934a230cb88e1d746783b9" and "e91f02af0291608767ef2369a70e39001bed3d64" have entirely different histories.

840 changed files with 5774 additions and 8136 deletions

View File

@ -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.

View File

@ -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

View File

@ -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
View File

@ -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

View File

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

View File

@ -37,8 +37,6 @@ abstract class BlogTruyen(
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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)
}

View File

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

View File

@ -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 {

View File

@ -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) }

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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")

View File

@ -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 {

View File

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

View File

@ -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"),

View File

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

View File

@ -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)

View File

@ -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.

View File

@ -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()
}
}

View File

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

View File

@ -67,8 +67,6 @@ constructor(
override val supportsLatest = true
override val client = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")

View File

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

View File

@ -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"),
)
}

View File

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

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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()) }

View File

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

View File

@ -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) }

View File

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

View File

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

View File

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

View File

@ -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()

View File

@ -1,7 +1,7 @@
ext {
extName = 'Cubari'
extClass = '.CubariFactory'
extVersionCode = 25
extVersionCode = 24
}
apply from: "$rootDir/common.gradle"

View File

@ -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()

View File

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

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

View File

@ -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)
}
}

View File

@ -3,7 +3,7 @@ ext {
extClass = '.EternalMangasFactory'
themePkg = 'mangaesp'
baseUrl = 'https://eternalmangas.com'
overrideVersionCode = 2
overrideVersionCode = 1
isNsfw = true
}

View File

@ -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 {

View File

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

View File

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

View File

@ -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()

View File

@ -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

View File

@ -1,7 +1,7 @@
ext {
extName = 'Komga'
extClass = '.KomgaFactory'
extVersionCode = 59
extVersionCode = 58
}
apply from: "$rootDir/common.gradle"

View File

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

View File

@ -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),
)

View File

@ -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()

View File

@ -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()

View File

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

View File

@ -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)

View File

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

View File

@ -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"))

View File

@ -1,7 +1,7 @@
ext {
extName = 'MyReadingManga'
extClass = '.MyReadingMangaFactory'
extVersionCode = 56
extVersionCode = 53
isNsfw = true
}

View File

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

View File

@ -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>

View File

@ -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

View File

@ -1,12 +0,0 @@
ext {
extName = 'NamiComi'
extClass = '.NamiComiFactory'
extVersionCode = 2
isNsfw = false
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(":lib:i18n"))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")

View File

@ -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))
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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,
)

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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?,
)

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

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