Remove sources (#5846)
|
@ -1,8 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Pururin'
|
|
||||||
extClass = '.PururinFactory'
|
|
||||||
extVersionCode = 9
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 9.0 KiB |
|
@ -1,264 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pururin
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import eu.kanade.tachiyomi.source.model.Page
|
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.FormBody
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
|
|
||||||
abstract class Pururin(
|
|
||||||
override val lang: String = "all",
|
|
||||||
private val searchLang: Pair<String, String>? = null,
|
|
||||||
private val langPath: String = "",
|
|
||||||
) : ParsedHttpSource() {
|
|
||||||
override val name = "Pururin"
|
|
||||||
|
|
||||||
final override val baseUrl = "https://pururin.to"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/browse$langPath?sort=most-popular&page=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = "a.card"
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
title = element.attr("title")
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href"))
|
|
||||||
thumbnail_url = element.select("img").attr("abs:src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaNextPageSelector(): String = ".page-item [rel=next]"
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request {
|
|
||||||
return GET("$baseUrl/browse$langPath?page=$page", headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun latestUpdatesSelector(): String = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
// Search
|
|
||||||
|
|
||||||
private fun List<Pair<String, String>>.toValue(): String {
|
|
||||||
return "[${this.joinToString(",") { "{\"id\":${it.first},\"name\":\"${it.second}\"}" }}]"
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parsePageRange(query: String, minPages: Int = 1, maxPages: Int = 9999): Pair<Int, Int> {
|
|
||||||
val num = query.filter(Char::isDigit).toIntOrNull() ?: -1
|
|
||||||
fun limitedNum(number: Int = num): Int = number.coerceIn(minPages, maxPages)
|
|
||||||
|
|
||||||
if (num < 0) return minPages to maxPages
|
|
||||||
return when (query.firstOrNull()) {
|
|
||||||
'<' -> 1 to if (query[1] == '=') limitedNum() else limitedNum(num + 1)
|
|
||||||
'>' -> limitedNum(if (query[1] == '=') num else num + 1) to maxPages
|
|
||||||
'=' -> when (query[1]) {
|
|
||||||
'>' -> limitedNum() to maxPages
|
|
||||||
'<' -> 1 to limitedNum(maxPages)
|
|
||||||
else -> limitedNum() to limitedNum()
|
|
||||||
}
|
|
||||||
else -> limitedNum() to limitedNum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
class Tag(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun findTagByNameSubstring(tags: List<Tag>, substring: String): Pair<String, String>? {
|
|
||||||
val tag = tags.find { it.name.contains(substring, ignoreCase = true) }
|
|
||||||
return tag?.let { Pair(tag.id.toString(), tag.name) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun tagSearch(tag: String, type: String): Pair<String, String>? {
|
|
||||||
val requestBody = FormBody.Builder()
|
|
||||||
.add("text", tag)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val request = Request.Builder()
|
|
||||||
.url("$baseUrl/api/get/tags/search")
|
|
||||||
.headers(headers)
|
|
||||||
.post(requestBody)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val response = client.newCall(request).execute()
|
|
||||||
return findTagByNameSubstring(response.parseAs<List<Tag>>(), type)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val includeTags = mutableListOf<Pair<String, String>>()
|
|
||||||
val excludeTags = mutableListOf<Pair<String, String>>()
|
|
||||||
var pagesMin = 1
|
|
||||||
var pagesMax = 9999
|
|
||||||
var sortBy = "newest"
|
|
||||||
|
|
||||||
if (searchLang != null) includeTags.add(searchLang)
|
|
||||||
|
|
||||||
filters.forEach {
|
|
||||||
when (it) {
|
|
||||||
is SelectFilter -> sortBy = it.getValue()
|
|
||||||
|
|
||||||
is TypeFilter -> {
|
|
||||||
val (_, inactiveFilters) = it.state.partition { stIt -> stIt.state }
|
|
||||||
excludeTags += inactiveFilters.map { fil -> Pair(fil.value, "${fil.name} [Category]") }
|
|
||||||
}
|
|
||||||
|
|
||||||
is PageFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
val (min, max) = parsePageRange(it.state)
|
|
||||||
pagesMin = min
|
|
||||||
pagesMax = max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is TextFilter -> {
|
|
||||||
if (it.state.isNotEmpty()) {
|
|
||||||
it.state.split(",").filter(String::isNotBlank).map { tag ->
|
|
||||||
val trimmed = tag.trim()
|
|
||||||
if (trimmed.startsWith('-')) {
|
|
||||||
tagSearch(trimmed.lowercase().removePrefix("-"), it.type)?.let { tagInfo ->
|
|
||||||
excludeTags.add(tagInfo)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tagSearch(trimmed.lowercase(), it.type)?.let { tagInfo ->
|
|
||||||
includeTags.add(tagInfo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Searching with just one tag usually gives wrong results
|
|
||||||
if (query.isEmpty()) {
|
|
||||||
when {
|
|
||||||
excludeTags.size == 1 && includeTags.isEmpty() -> excludeTags.addAll(excludeTags)
|
|
||||||
includeTags.size == 1 && excludeTags.isEmpty() -> {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("browse")
|
|
||||||
addPathSegment("tags")
|
|
||||||
addPathSegment("content")
|
|
||||||
addPathSegment(includeTags[0].first)
|
|
||||||
addQueryParameter("sort", sortBy)
|
|
||||||
addQueryParameter("start_page", pagesMin.toString())
|
|
||||||
addQueryParameter("last_page", pagesMax.toString())
|
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addPathSegment("search")
|
|
||||||
addQueryParameter("q", query)
|
|
||||||
addQueryParameter("sort", sortBy)
|
|
||||||
addQueryParameter("start_page", pagesMin.toString())
|
|
||||||
addQueryParameter("last_page", pagesMax.toString())
|
|
||||||
if (includeTags.isNotEmpty()) addQueryParameter("included_tags", includeTags.toValue())
|
|
||||||
if (excludeTags.isNotEmpty()) addQueryParameter("excluded_tags", excludeTags.toValue())
|
|
||||||
if (page > 1) addQueryParameter("page", page.toString())
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector(): String = popularMangaSelector()
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
|
||||||
|
|
||||||
override fun searchMangaNextPageSelector(): String = popularMangaNextPageSelector()
|
|
||||||
|
|
||||||
// Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
document.select(".box-gallery").let { e ->
|
|
||||||
initialized = true
|
|
||||||
title = e.select(".title").text()
|
|
||||||
author = e.select("a[href*=/circle/]").text().ifEmpty { e.select("[itemprop=author]").text() }
|
|
||||||
artist = e.select("[itemprop=author]").text()
|
|
||||||
genre = e.select("a[href*=/content/]").text()
|
|
||||||
description = e.select(".box-gallery .table-info tr")
|
|
||||||
.filter { tr ->
|
|
||||||
tr.select("td").none { it.text().contains("content", ignoreCase = true) || it.text().contains("ratings", ignoreCase = true) }
|
|
||||||
}
|
|
||||||
.joinToString("\n") { tr ->
|
|
||||||
tr.select("td")
|
|
||||||
.joinToString(": ") { it.text() }
|
|
||||||
}
|
|
||||||
thumbnail_url = e.select("img").attr("abs:src")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapters
|
|
||||||
|
|
||||||
override fun chapterListSelector(): String = ".table-collection tbody tr a"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element): SChapter {
|
|
||||||
return SChapter.create().apply {
|
|
||||||
name = element.text()
|
|
||||||
setUrlWithoutDomain(element.attr("abs:href"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
return response.asJsoup().select(chapterListSelector())
|
|
||||||
.map { chapterFromElement(it) }
|
|
||||||
.reversed()
|
|
||||||
.let { list ->
|
|
||||||
list.ifEmpty {
|
|
||||||
listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(response.request.url.toString())
|
|
||||||
name = "Chapter"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pages
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select(".gallery-preview a img")
|
|
||||||
.mapIndexed { i, img ->
|
|
||||||
Page(i, "", (if (img.hasAttr("abs:src")) img.attr("abs:src") else img.attr("abs:data-src")).replace("t.", "."))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T {
|
|
||||||
return json.decodeFromString(body.string())
|
|
||||||
}
|
|
||||||
override fun getFilterList() = getFilters()
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pururin
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class PururinFactory : SourceFactory {
|
|
||||||
override fun createSources(): List<Source> = listOf(
|
|
||||||
PururinAll(),
|
|
||||||
PururinEN(),
|
|
||||||
PururinJA(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PururinAll : Pururin()
|
|
||||||
class PururinEN : Pururin(
|
|
||||||
"en",
|
|
||||||
Pair("13010", "english"),
|
|
||||||
"/tags/language/13010/english",
|
|
||||||
)
|
|
||||||
class PururinJA : Pururin(
|
|
||||||
"ja",
|
|
||||||
Pair("13011", "japanese"),
|
|
||||||
"/tags/language/13011/japanese",
|
|
||||||
)
|
|
|
@ -1,57 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.pururin
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
|
|
||||||
fun getFilters(): FilterList {
|
|
||||||
return FilterList(
|
|
||||||
SelectFilter("Sort by", getSortsList),
|
|
||||||
TypeFilter("Types"),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Separate tags with commas (,)"),
|
|
||||||
Filter.Header("Prepend with dash (-) to exclude"),
|
|
||||||
TextFilter("Tags", "[Content]"),
|
|
||||||
TextFilter("Artists", "[Artist]"),
|
|
||||||
TextFilter("Circles", "[Circle]"),
|
|
||||||
TextFilter("Parodies", "[Parody]"),
|
|
||||||
TextFilter("Languages", "[Language]"),
|
|
||||||
TextFilter("Scanlators", "[Scanlator]"),
|
|
||||||
TextFilter("Conventions", "[Convention]"),
|
|
||||||
TextFilter("Collections", "[Collections]"),
|
|
||||||
TextFilter("Categories", "[Category]"),
|
|
||||||
TextFilter("Uploaders", "[Uploader]"),
|
|
||||||
Filter.Separator(),
|
|
||||||
Filter.Header("Filter by pages, for example: (>20)"),
|
|
||||||
PageFilter("Pages"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
internal class TypeFilter(name: String) :
|
|
||||||
Filter.Group<CheckBoxFilter>(
|
|
||||||
name,
|
|
||||||
listOf(
|
|
||||||
Pair("Artbook", "17783"),
|
|
||||||
Pair("Artist CG", "13004"),
|
|
||||||
Pair("Doujinshi", "13003"),
|
|
||||||
Pair("Game CG", "13008"),
|
|
||||||
Pair("Manga", "13004"),
|
|
||||||
Pair("Webtoon", "27939"),
|
|
||||||
).map { CheckBoxFilter(it.first, it.second, true) },
|
|
||||||
)
|
|
||||||
|
|
||||||
internal open class CheckBoxFilter(name: String, val value: String, state: Boolean) : Filter.CheckBox(name, state)
|
|
||||||
|
|
||||||
internal open class PageFilter(name: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class TextFilter(name: String, val type: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
internal open class SelectFilter(name: String, val vals: List<Pair<String, String>>, state: Int = 0) :
|
|
||||||
Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
|
|
||||||
fun getValue() = vals[state].second
|
|
||||||
}
|
|
||||||
private val getSortsList: List<Pair<String, String>> = listOf(
|
|
||||||
Pair("Newest", "newest"),
|
|
||||||
Pair("Most Popular", "most-popular"),
|
|
||||||
Pair("Highest Rated", "highest-rated"),
|
|
||||||
Pair("Most Viewed", "most-viewed"),
|
|
||||||
Pair("Title", "title"),
|
|
||||||
)
|
|
|
@ -1,10 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Radiant Scans'
|
|
||||||
extClass = '.RadiantScans'
|
|
||||||
themePkg = 'mangathemesia'
|
|
||||||
baseUrl = 'https://radiantscans.com'
|
|
||||||
overrideVersionCode = 9
|
|
||||||
isNsfw = false
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 6.9 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 19 KiB |
|
@ -1,46 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.luminousscans
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.mangathemesia.MangaThemesiaAlt
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
import okhttp3.Request
|
|
||||||
|
|
||||||
class RadiantScans : MangaThemesiaAlt(
|
|
||||||
"Radiant Scans",
|
|
||||||
"https://radiantscans.com",
|
|
||||||
"en",
|
|
||||||
mangaUrlDirectory = "/series",
|
|
||||||
randomUrlPrefKey = "pref_permanent_manga_url_2_en",
|
|
||||||
) {
|
|
||||||
// Luminous Scans -> Radiant Scans
|
|
||||||
override val id = 1019556752273106311
|
|
||||||
|
|
||||||
init {
|
|
||||||
// remove legacy preferences
|
|
||||||
preferences.run {
|
|
||||||
if (contains("pref_url_map")) {
|
|
||||||
edit().remove("pref_url_map").apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val client = super.client.newBuilder()
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val request = super.searchMangaRequest(page, query, filters)
|
|
||||||
if (query.isBlank()) return request
|
|
||||||
|
|
||||||
val url = request.url.newBuilder()
|
|
||||||
.addPathSegment("page/$page/")
|
|
||||||
.removeAllQueryParameters("page")
|
|
||||||
.removeAllQueryParameters("title")
|
|
||||||
.addQueryParameter("s", query)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return request.newBuilder()
|
|
||||||
.url(url)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Manga Tx.to'
|
|
||||||
extClass = '.MangaTxTo'
|
|
||||||
themePkg = 'madara'
|
|
||||||
baseUrl = 'https://mangatx.to'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = false
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 42 KiB |
|
@ -1,14 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.mangatxto
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
|
|
||||||
class MangaTxTo : Madara(
|
|
||||||
"Manga Tx.to",
|
|
||||||
"https://mangatx.to",
|
|
||||||
"en",
|
|
||||||
) {
|
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
|
||||||
override val useNewChapterEndpoint = false
|
|
||||||
|
|
||||||
override val mangaSubString = "manhua"
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<activity
|
|
||||||
android:name=".en.ninehentai.NineHentaiUrlActivity"
|
|
||||||
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:host="9hentai.com"
|
|
||||||
android:pathPattern="/g/..*"
|
|
||||||
android:scheme="https" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,8 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'NineHentai'
|
|
||||||
extClass = '.NineHentai'
|
|
||||||
extVersionCode = 3
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,379 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.ninehentai
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
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.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
|
||||||
import kotlinx.serialization.json.decodeFromJsonElement
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.put
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import okio.Buffer
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import rx.Observable
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.util.Calendar
|
|
||||||
|
|
||||||
class NineHentai : HttpSource() {
|
|
||||||
|
|
||||||
override val baseUrl = "https://9hentai.com"
|
|
||||||
|
|
||||||
override val name = "NineHentai"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
override val client: OkHttpClient = network.cloudflareClient
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
// Builds request for /api/getBooks endpoint
|
|
||||||
private fun buildSearchRequest(
|
|
||||||
searchText: String = "",
|
|
||||||
page: Int,
|
|
||||||
sort: Int = 0,
|
|
||||||
range: List<Int> = listOf(0, 2000),
|
|
||||||
includedTags: List<Tag> = listOf(),
|
|
||||||
excludedTags: List<Tag> = listOf(),
|
|
||||||
): Request {
|
|
||||||
val searchRequest = SearchRequest(
|
|
||||||
text = searchText,
|
|
||||||
page = page - 1, // Source starts counting from 0, not 1
|
|
||||||
sort = sort,
|
|
||||||
pages = Range(range),
|
|
||||||
tag = Items(
|
|
||||||
items = TagArrays(
|
|
||||||
included = includedTags,
|
|
||||||
excluded = excludedTags,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
val jsonString = json.encodeToString(SearchRequestPayload(search = searchRequest))
|
|
||||||
return POST("$baseUrl$SEARCH_URL", headers, jsonString.toRequestBody(MEDIA_TYPE))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseSearchResponse(response: Response): MangasPage {
|
|
||||||
return response.use {
|
|
||||||
val page = json.decodeFromString<SearchRequestPayload>(it.request.bodyString).search.page
|
|
||||||
json.decodeFromString<SearchResponse>(it.body.string()).let { searchResponse ->
|
|
||||||
MangasPage(
|
|
||||||
searchResponse.results.map {
|
|
||||||
SManga.create().apply {
|
|
||||||
url = "/g/${it.id}"
|
|
||||||
title = it.title
|
|
||||||
// Cover is the compressed first page (cover might change if page count changes)
|
|
||||||
thumbnail_url = "${it.image_server}${it.id}/1.jpg?${it.total_page}"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
searchResponse.totalCount - 1 > page,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Builds request for /api/getBookById endpoint
|
|
||||||
private fun buildDetailRequest(id: Int): Request {
|
|
||||||
val jsonString = buildJsonObject { put("id", id) }.toString()
|
|
||||||
return POST("$baseUrl$MANGA_URL", headers, jsonString.toRequestBody(MEDIA_TYPE))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Popular
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request = buildSearchRequest(page = page, sort = 1)
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response): MangasPage = parseSearchResponse(response)
|
|
||||||
|
|
||||||
// Latest
|
|
||||||
override fun latestUpdatesRequest(page: Int): Request = buildSearchRequest(page = page)
|
|
||||||
|
|
||||||
override fun latestUpdatesParse(response: Response): MangasPage = parseSearchResponse(response)
|
|
||||||
|
|
||||||
// Search
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
if (query.startsWith("id:")) {
|
|
||||||
val id = query.substringAfter("id:").toInt()
|
|
||||||
return client.newCall(buildDetailRequest(id))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { response ->
|
|
||||||
fetchSingleManga(response)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
|
||||||
var sort = 0
|
|
||||||
val range = mutableListOf(0, 2000)
|
|
||||||
val includedTags = mutableListOf<Tag>()
|
|
||||||
val excludedTags = mutableListOf<Tag>()
|
|
||||||
for (filter in filterList) {
|
|
||||||
when (filter) {
|
|
||||||
is SortFilter -> {
|
|
||||||
sort = filter.state
|
|
||||||
}
|
|
||||||
is MinPagesFilter -> {
|
|
||||||
try {
|
|
||||||
range[0] = filter.state.toInt()
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
// Suppress and retain default value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is MaxPagesFilter -> {
|
|
||||||
try {
|
|
||||||
range[1] = filter.state.toInt()
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
// Suppress and retain default value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is IncludedFilter -> {
|
|
||||||
includedTags += getTags(filter.state, 1)
|
|
||||||
}
|
|
||||||
is ExcludedFilter -> {
|
|
||||||
excludedTags += getTags(filter.state, 1)
|
|
||||||
}
|
|
||||||
is GroupFilter -> {
|
|
||||||
includedTags += getTags(filter.state, 2)
|
|
||||||
}
|
|
||||||
is ParodyFilter -> {
|
|
||||||
includedTags += getTags(filter.state, 3)
|
|
||||||
}
|
|
||||||
is ArtistFilter -> {
|
|
||||||
includedTags += getTags(filter.state, 4)
|
|
||||||
}
|
|
||||||
is CharacterFilter -> {
|
|
||||||
includedTags += getTags(filter.state, 5)
|
|
||||||
}
|
|
||||||
is CategoryFilter -> {
|
|
||||||
includedTags += getTags(filter.state, 6)
|
|
||||||
}
|
|
||||||
else -> { /* Do nothing */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buildSearchRequest(
|
|
||||||
searchText = query,
|
|
||||||
page = page,
|
|
||||||
sort = sort,
|
|
||||||
range = range,
|
|
||||||
includedTags = includedTags,
|
|
||||||
excludedTags = excludedTags,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage = parseSearchResponse(response)
|
|
||||||
|
|
||||||
// Manga Details
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
return SManga.create().apply {
|
|
||||||
response.asJsoup().selectFirst("div#bigcontainer")!!.let { info ->
|
|
||||||
title = info.select("h1").text()
|
|
||||||
thumbnail_url = info.selectFirst("div#cover v-lazy-image")!!.attr("abs:src")
|
|
||||||
status = SManga.COMPLETED
|
|
||||||
artist = info.selectTextOrNull("div.field-name:contains(Artist:) a.tag")
|
|
||||||
author = info.selectTextOrNull("div.field-name:contains(Group:) a.tag") ?: "Unknown circle"
|
|
||||||
genre = info.selectTextOrNull("div.field-name:contains(Tag:) a.tag")
|
|
||||||
// Additional details
|
|
||||||
description = listOf(
|
|
||||||
Pair("Alternative Title", info.selectTextOrNull("h2")),
|
|
||||||
Pair("Pages", info.selectTextOrNull("div#info > div:contains(pages)")),
|
|
||||||
Pair("Parody", info.selectTextOrNull("div.field-name:contains(Parody:) a.tag")),
|
|
||||||
Pair("Category", info.selectTextOrNull("div.field-name:contains(Category:) a.tag")),
|
|
||||||
Pair("Language", info.selectTextOrNull("div.field-name:contains(Language:) a.tag")),
|
|
||||||
).filterNot { it.second.isNullOrEmpty() }.joinToString("\n\n") { "${it.first}: ${it.second}" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensures no exceptions are thrown when scraping additional details
|
|
||||||
private fun Element.selectTextOrNull(selector: String): String? {
|
|
||||||
val list = this.select(selector)
|
|
||||||
return if (list.isEmpty()) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
list.joinToString(", ") { it.text() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chapter
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response): List<SChapter> {
|
|
||||||
val time = response.asJsoup().select("div#info div time").text()
|
|
||||||
return listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = "Chapter"
|
|
||||||
date_upload = parseChapterDate(time)
|
|
||||||
url = response.request.url.encodedPath
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun parseChapterDate(date: String): Long {
|
|
||||||
val dateStringSplit = date.split(" ")
|
|
||||||
val value = dateStringSplit[0].toInt()
|
|
||||||
|
|
||||||
return when (dateStringSplit[1].removeSuffix("s")) {
|
|
||||||
"sec" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.SECOND, value * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
"min" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.MINUTE, value * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
"hour" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
"day" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.DATE, value * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
"week" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.DATE, value * 7 * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
"month" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.MONTH, value * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
"year" -> Calendar.getInstance().apply {
|
|
||||||
add(Calendar.YEAR, value * -1)
|
|
||||||
}.timeInMillis
|
|
||||||
else -> {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page List
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val mangaId = chapter.url.substringAfter("/g/").toInt()
|
|
||||||
return buildDetailRequest(mangaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val resultsObj = json.parseToJsonElement(response.body.string()).jsonObject["results"]!!
|
|
||||||
val manga = json.decodeFromJsonElement<Manga>(resultsObj)
|
|
||||||
val imageUrl = manga.image_server + manga.id
|
|
||||||
var totalPages = manga.total_page
|
|
||||||
|
|
||||||
client.newCall(
|
|
||||||
GET(
|
|
||||||
"$imageUrl/preview/${totalPages}t.jpg",
|
|
||||||
headersBuilder().build(),
|
|
||||||
),
|
|
||||||
).execute().code.let { code ->
|
|
||||||
if (code == 404) totalPages--
|
|
||||||
}
|
|
||||||
|
|
||||||
return (1..totalPages).map {
|
|
||||||
Page(it - 1, "", "$imageUrl/$it.jpg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getTags(queries: String, type: Int): List<Tag> {
|
|
||||||
return queries.split(",").map(String::trim)
|
|
||||||
.filterNot(String::isBlank).mapNotNull { query ->
|
|
||||||
val jsonString = buildJsonObject {
|
|
||||||
put("tag_name", query)
|
|
||||||
put("tag_type", type)
|
|
||||||
}.toString()
|
|
||||||
lookupTags(jsonString)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Based on HentaiHand ext
|
|
||||||
private fun lookupTags(request: String): Tag? {
|
|
||||||
return client.newCall(POST("$baseUrl$TAG_URL", headers, request.toRequestBody(MEDIA_TYPE)))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map { response ->
|
|
||||||
// Returns the first matched tag, or null if there are no results
|
|
||||||
val tagList = json.parseToJsonElement(response.body.string()).jsonObject["results"]!!.jsonArray.map {
|
|
||||||
json.decodeFromJsonElement<Tag>(it)
|
|
||||||
}
|
|
||||||
if (tagList.isEmpty()) {
|
|
||||||
return@map null
|
|
||||||
} else {
|
|
||||||
tagList.first()
|
|
||||||
}
|
|
||||||
}.toBlocking().first()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchSingleManga(response: Response): MangasPage {
|
|
||||||
val resultsObj = json.parseToJsonElement(response.body.string()).jsonObject["results"]!!
|
|
||||||
val manga = json.decodeFromJsonElement<Manga>(resultsObj)
|
|
||||||
val list = listOf(
|
|
||||||
SManga.create().apply {
|
|
||||||
setUrlWithoutDomain("/g/${manga.id}")
|
|
||||||
title = manga.title
|
|
||||||
thumbnail_url = "${manga.image_server + manga.id}/cover.jpg"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return MangasPage(list, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filters
|
|
||||||
|
|
||||||
private class SortFilter : Filter.Select<String>(
|
|
||||||
"Sort by",
|
|
||||||
arrayOf("Newest", "Popular Right now", "Most Fapped", "Most Viewed", "By Title"),
|
|
||||||
)
|
|
||||||
|
|
||||||
private class MinPagesFilter : Filter.Text("Minimum Pages")
|
|
||||||
private class MaxPagesFilter : Filter.Text("Maximum Pages")
|
|
||||||
private class IncludedFilter : Filter.Text("Included Tags")
|
|
||||||
private class ExcludedFilter : Filter.Text("Excluded Tags")
|
|
||||||
private class ArtistFilter : Filter.Text("Artist")
|
|
||||||
private class GroupFilter : Filter.Text("Group")
|
|
||||||
private class ParodyFilter : Filter.Text("Parody")
|
|
||||||
private class CharacterFilter : Filter.Text("Character")
|
|
||||||
private class CategoryFilter : Filter.Text("Category")
|
|
||||||
|
|
||||||
override fun getFilterList() = FilterList(
|
|
||||||
Filter.Header("Search by id with \"id:\" in front of query"),
|
|
||||||
Filter.Separator(),
|
|
||||||
SortFilter(),
|
|
||||||
MinPagesFilter(),
|
|
||||||
MaxPagesFilter(),
|
|
||||||
IncludedFilter(),
|
|
||||||
ExcludedFilter(),
|
|
||||||
ArtistFilter(),
|
|
||||||
GroupFilter(),
|
|
||||||
ParodyFilter(),
|
|
||||||
CharacterFilter(),
|
|
||||||
CategoryFilter(),
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
private val Request.bodyString: String
|
|
||||||
get() {
|
|
||||||
val requestCopy = newBuilder().build()
|
|
||||||
val buffer = Buffer()
|
|
||||||
|
|
||||||
return runCatching { buffer.apply { requestCopy.body!!.writeTo(this) }.readUtf8() }
|
|
||||||
.getOrNull() ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
private const val SEARCH_URL = "/api/getBook"
|
|
||||||
private const val MANGA_URL = "/api/getBookByID"
|
|
||||||
private const val TAG_URL = "/api/getTag"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.ninehentai
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Manga(
|
|
||||||
val id: Int,
|
|
||||||
val title: String,
|
|
||||||
val image_server: String,
|
|
||||||
val total_page: Int,
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
The basic search request JSON object looks like this:
|
|
||||||
{
|
|
||||||
"search": {
|
|
||||||
"text": "",
|
|
||||||
"page": 1,
|
|
||||||
"sort": 1,
|
|
||||||
"pages": {
|
|
||||||
"range": [0, 2000]
|
|
||||||
},
|
|
||||||
"tag": {
|
|
||||||
"items": {
|
|
||||||
"included": [],
|
|
||||||
"excluded": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Sort = 0, Newest
|
|
||||||
Sort = 1, Popular right now
|
|
||||||
Sort = 2, Most Fapped
|
|
||||||
Sort = 3, Most Viewed
|
|
||||||
Sort = 4, By title
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchRequest(
|
|
||||||
val text: String,
|
|
||||||
val page: Int,
|
|
||||||
val sort: Int,
|
|
||||||
val pages: Range,
|
|
||||||
val tag: Items,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchRequestPayload(
|
|
||||||
val search: SearchRequest,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchResponse(
|
|
||||||
@SerialName("total_count") val totalCount: Int,
|
|
||||||
val results: List<Manga>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Range(
|
|
||||||
val range: List<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Items(
|
|
||||||
val items: TagArrays,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TagArrays(
|
|
||||||
val included: List<Tag>,
|
|
||||||
val excluded: List<Tag>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Tag(
|
|
||||||
val id: Int,
|
|
||||||
val name: String,
|
|
||||||
val description: String? = null,
|
|
||||||
val type: Int = 1,
|
|
||||||
)
|
|
|
@ -1,38 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.ninehentai
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Springboard that accepts https://9hentai.com/g/xxxxxx intents and redirects them to
|
|
||||||
* the main Tachiyomi process.
|
|
||||||
*/
|
|
||||||
class NineHentaiUrlActivity : Activity() {
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
val pathSegments = intent?.data?.pathSegments
|
|
||||||
if (pathSegments != null && pathSegments.size > 1) {
|
|
||||||
val id = pathSegments[1]
|
|
||||||
val mainIntent = Intent().apply {
|
|
||||||
action = "eu.kanade.tachiyomi.SEARCH"
|
|
||||||
putExtra("query", "id:$id")
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("NineHentaiUrlActivity", e.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("NineHentaiUrlActivity", "could not parse uri from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'ReaderGen'
|
|
||||||
extClass = '.ReaderGen'
|
|
||||||
themePkg = 'madara'
|
|
||||||
baseUrl = 'https://fr.readergen.fr'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 11 KiB |
|
@ -1,7 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.fr.readergen
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
|
|
||||||
class ReaderGen : Madara("ReaderGen", "https://fr.readergen.fr", "fr") {
|
|
||||||
override val useNewChapterEndpoint = true
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Zanman Manga'
|
|
||||||
extClass = '.ZanmanManga'
|
|
||||||
themePkg = 'madara'
|
|
||||||
baseUrl = 'https://zamanmanga.com'
|
|
||||||
overrideVersionCode = 0
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 47 KiB |
|
@ -1,25 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.tr.zanmanmanga
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class ZanmanManga : Madara(
|
|
||||||
"Zanman Manga",
|
|
||||||
"https://zamanmanga.com",
|
|
||||||
"tr",
|
|
||||||
dateFormat = SimpleDateFormat("d MMMM yyyy", Locale("tr")),
|
|
||||||
) {
|
|
||||||
override val mangaDetailsSelectorAuthor = "div.manga-authors > a"
|
|
||||||
override val mangaDetailsSelectorDescription = "div.manga-summary"
|
|
||||||
override val mangaDetailsSelectorThumbnail = "head meta[property='og:image']" // Same as browse
|
|
||||||
|
|
||||||
override val useLoadMoreRequest = LoadMoreStrategy.Never
|
|
||||||
override val useNewChapterEndpoint = true
|
|
||||||
|
|
||||||
override fun imageFromElement(element: Element): String? {
|
|
||||||
return super.imageFromElement(element)?.takeIf { it.isNotEmpty() }
|
|
||||||
?: element.attr("content") // Thumbnail from <head>
|
|
||||||
}
|
|
||||||
}
|
|