|
@ -1,21 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<application android:icon="@mipmap/ic_launcher">
|
|
||||||
<activity
|
|
||||||
android:name=".all.ninenineninehentai.AnimeHUrlActivity"
|
|
||||||
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="animeh.to"/>
|
|
||||||
<data android:scheme="https"/>
|
|
||||||
<data android:pathPattern="/hchapter/..*"/>
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
|
@ -1,8 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'AnimeH'
|
|
||||||
extClass = '.AnimeHFactory'
|
|
||||||
extVersionCode = 7
|
|
||||||
isNsfw = true
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 47 KiB |
|
@ -1,334 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
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.ninenineninehentai.Url.Companion.toAbsUrl
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
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 eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import kotlinx.serialization.decodeFromString
|
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
|
||||||
import okhttp3.Response
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
open class AnimeH(
|
|
||||||
final override val lang: String,
|
|
||||||
private val siteLang: String = lang,
|
|
||||||
) : HttpSource(), ConfigurableSource {
|
|
||||||
|
|
||||||
override val name = "AnimeH"
|
|
||||||
|
|
||||||
override val baseUrl = "https://animeh.to"
|
|
||||||
|
|
||||||
private val apiUrl = "https://api.animeh.to/api"
|
|
||||||
|
|
||||||
override val supportsLatest = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.addInterceptor { chain ->
|
|
||||||
val request = chain.request()
|
|
||||||
val url = request.url
|
|
||||||
|
|
||||||
if (url.host != "127.0.0.1") {
|
|
||||||
return@addInterceptor chain.proceed(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newRequest = request.newBuilder()
|
|
||||||
.url(
|
|
||||||
url.newBuilder()
|
|
||||||
.host(preference.cdnUrl)
|
|
||||||
.build(),
|
|
||||||
).build()
|
|
||||||
|
|
||||||
return@addInterceptor chain.proceed(newRequest)
|
|
||||||
}
|
|
||||||
.rateLimit(1)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private val preference by lazy {
|
|
||||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun headersBuilder() = super.headersBuilder()
|
|
||||||
.set("Referer", "$baseUrl/")
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int): Request {
|
|
||||||
val payload = GraphQL(
|
|
||||||
PopularVariables(size, page, 1, siteLang),
|
|
||||||
POPULAR_QUERY,
|
|
||||||
).toJsonRequestBody()
|
|
||||||
|
|
||||||
return POST(apiUrl, headers, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaParse(response: Response) = browseMangaParse<PopularResponse>(response)
|
|
||||||
|
|
||||||
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(page, "", FilterList())
|
|
||||||
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return if (query.startsWith(SEARCH_PREFIX)) {
|
|
||||||
val mangaId = query.substringAfter(SEARCH_PREFIX)
|
|
||||||
client.newCall(mangaFromIDRequest(mangaId))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map(::searchMangaFromIDParse)
|
|
||||||
} else {
|
|
||||||
super.fetchSearchManga(page, query, filters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val payload = GraphQL(
|
|
||||||
SearchVariables(
|
|
||||||
size = size,
|
|
||||||
page = page,
|
|
||||||
search = SearchPayload(
|
|
||||||
query = query.trim().takeUnless { it.isEmpty() },
|
|
||||||
language = siteLang,
|
|
||||||
sortBy = filters.firstInstanceOrNull<SortFilter>()?.selected,
|
|
||||||
format = filters.firstInstanceOrNull<FormatFilter>()?.selected,
|
|
||||||
tags = filters.firstInstanceOrNull<IncludedTagFilter>()?.tags,
|
|
||||||
excludeTags = filters.firstInstanceOrNull<ExcludedTagFilter>()?.tags,
|
|
||||||
pagesRangeStart = filters.firstInstanceOrNull<MinPageFilter>()?.value,
|
|
||||||
pagesRangeEnd = filters.firstInstanceOrNull<MaxPageFilter>()?.value,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SEARCH_QUERY,
|
|
||||||
).toJsonRequestBody()
|
|
||||||
|
|
||||||
return POST(apiUrl, headers, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response) = browseMangaParse<SearchResponse>(response)
|
|
||||||
override fun getFilterList() = getFilters()
|
|
||||||
|
|
||||||
private fun mangaFromIDRequest(id: String): Request {
|
|
||||||
val payload = GraphQL(
|
|
||||||
IdVariables(id),
|
|
||||||
DETAILS_QUERY,
|
|
||||||
).toJsonRequestBody()
|
|
||||||
|
|
||||||
return POST(apiUrl, headers, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchMangaFromIDParse(response: Response): MangasPage {
|
|
||||||
val res = response.parseAs<ApiDetailsResponse>()
|
|
||||||
|
|
||||||
val manga = res.data.details
|
|
||||||
.takeIf { it.language == siteLang || lang == "all" }
|
|
||||||
?.let { manga ->
|
|
||||||
preference.dateMap = preference.dateMap.also { dateMap ->
|
|
||||||
manga.uploadDate?.let { dateMap[manga.id] = it }
|
|
||||||
}
|
|
||||||
manga.toSManga(preference.shortTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(listOfNotNull(manga), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
|
||||||
return mangaFromIDRequest(manga.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(response: Response): SManga {
|
|
||||||
val res = response.parseAs<ApiDetailsResponse>()
|
|
||||||
val manga = res.data.details
|
|
||||||
|
|
||||||
preference.dateMap = preference.dateMap.also { dateMap ->
|
|
||||||
manga.uploadDate?.let { dateMap[manga.id] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga.toSManga(preference.shortTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMangaUrl(manga: SManga) = "$baseUrl/hchapter/${manga.url}"
|
|
||||||
|
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
val group = manga.description
|
|
||||||
?.substringAfter("Group:", "")
|
|
||||||
?.substringBefore("\n")
|
|
||||||
?.trim()
|
|
||||||
?.takeUnless { it.isEmpty() }
|
|
||||||
|
|
||||||
return Observable.just(
|
|
||||||
listOf(
|
|
||||||
SChapter.create().apply {
|
|
||||||
name = "Chapter"
|
|
||||||
url = manga.url
|
|
||||||
date_upload = preference.dateMap[manga.url].parseDate()
|
|
||||||
scanlator = group
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/hchapter/${chapter.url}"
|
|
||||||
|
|
||||||
override fun pageListRequest(chapter: SChapter): Request {
|
|
||||||
val payload = GraphQL(
|
|
||||||
IdVariables(chapter.url),
|
|
||||||
PAGES_QUERY,
|
|
||||||
).toJsonRequestBody()
|
|
||||||
|
|
||||||
return POST(apiUrl, headers, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(response: Response): List<Page> {
|
|
||||||
val res = response.parseAs<ApiPageListResponse>()
|
|
||||||
|
|
||||||
val pages = res.data.chapter.pages?.firstOrNull()
|
|
||||||
?: return emptyList()
|
|
||||||
|
|
||||||
val cdnUrl = "https://${getUpdatedCdn(res.data.chapter.id)}/"
|
|
||||||
val cdn = pages.urlPart.toAbsUrl(cdnUrl)
|
|
||||||
|
|
||||||
val selectedImages = when (preference.getString(PREF_IMG_QUALITY_KEY, "original")) {
|
|
||||||
"medium" -> pages.qualityMedium?.mapIndexed { i, it ->
|
|
||||||
it ?: pages.qualityOriginal[i]
|
|
||||||
}
|
|
||||||
else -> pages.qualityOriginal
|
|
||||||
} ?: pages.qualityOriginal
|
|
||||||
|
|
||||||
return selectedImages.mapIndexed { index, image ->
|
|
||||||
Page(index, "", "$cdn/${image.url}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUpdatedCdn(chapterId: String): String {
|
|
||||||
val url = "$baseUrl/hchapter/$chapterId"
|
|
||||||
val document = client.newCall(GET(url, headers))
|
|
||||||
.execute().use { it.asJsoup() }
|
|
||||||
|
|
||||||
val cdnHost = document.selectFirst("meta[property=og:image]")
|
|
||||||
?.attr("content")
|
|
||||||
?.toHttpUrlOrNull()
|
|
||||||
?.host
|
|
||||||
|
|
||||||
return cdnHost?.also {
|
|
||||||
preference.cdnUrl = it
|
|
||||||
} ?: preference.cdnUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> String.parseAs(): T =
|
|
||||||
json.decodeFromString(this)
|
|
||||||
|
|
||||||
private inline fun <reified T> Response.parseAs(): T =
|
|
||||||
use { body.string() }.parseAs()
|
|
||||||
|
|
||||||
private inline fun <reified T> List<*>.firstInstanceOrNull(): T? =
|
|
||||||
filterIsInstance<T>().firstOrNull()
|
|
||||||
|
|
||||||
private inline fun <reified T : Any> T.toJsonRequestBody(): RequestBody =
|
|
||||||
json.encodeToString(this)
|
|
||||||
.toRequestBody(JSON_MEDIA_TYPE)
|
|
||||||
|
|
||||||
private fun String?.parseDate(): Long {
|
|
||||||
return runCatching {
|
|
||||||
dateFormat.parse(this!!.trim())!!.time
|
|
||||||
}.getOrDefault(0L)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T : BrowseResponse> browseMangaParse(response: Response): MangasPage {
|
|
||||||
val res = response.parseAs<Data<T>>()
|
|
||||||
val mangas = res.data.chapters.edges
|
|
||||||
val dateMap = preference.dateMap
|
|
||||||
val useShortTitle = preference.shortTitle
|
|
||||||
val entries = mangas.map { manga ->
|
|
||||||
manga.uploadDate?.let { dateMap[manga.id] = it }
|
|
||||||
manga.toSManga(useShortTitle)
|
|
||||||
}
|
|
||||||
preference.dateMap = dateMap
|
|
||||||
val hasNextPage = mangas.size == size
|
|
||||||
|
|
||||||
return MangasPage(entries, hasNextPage)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
|
||||||
ListPreference(screen.context).apply {
|
|
||||||
key = PREF_IMG_QUALITY_KEY
|
|
||||||
title = "Default Image Quality"
|
|
||||||
entries = arrayOf("Original", "Medium")
|
|
||||||
entryValues = arrayOf("original", "medium")
|
|
||||||
setDefaultValue("original")
|
|
||||||
summary = "%s"
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
|
|
||||||
SwitchPreferenceCompat(screen.context).apply {
|
|
||||||
key = PREF_SHORT_TITLE
|
|
||||||
title = "Display Short Titles"
|
|
||||||
summaryOff = "Showing Long Titles"
|
|
||||||
summaryOn = "Showing short Titles"
|
|
||||||
setDefaultValue(false)
|
|
||||||
}.also(screen::addPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var SharedPreferences.dateMap: MutableMap<String, String>
|
|
||||||
get() {
|
|
||||||
val jsonMap = getString(PREF_DATE_MAP_KEY, "{}")!!
|
|
||||||
val dateMap = runCatching { jsonMap.parseAs<MutableMap<String, String>>() }
|
|
||||||
return dateMap.getOrDefault(mutableMapOf())
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
|
||||||
set(dateMap) {
|
|
||||||
edit()
|
|
||||||
.putString(PREF_DATE_MAP_KEY, json.encodeToString(dateMap))
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var SharedPreferences.cdnUrl: String
|
|
||||||
get() = getString(PREF_CDN_URL, DEFAULT_CDN) ?: DEFAULT_CDN
|
|
||||||
|
|
||||||
@SuppressLint("ApplySharedPref")
|
|
||||||
set(cdnUrl) {
|
|
||||||
edit().putString(PREF_CDN_URL, cdnUrl).commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val SharedPreferences.shortTitle get() = getBoolean(PREF_SHORT_TITLE, false)
|
|
||||||
|
|
||||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val size = 20
|
|
||||||
const val SEARCH_PREFIX = "id:"
|
|
||||||
|
|
||||||
private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaTypeOrNull()
|
|
||||||
private val dateFormat by lazy {
|
|
||||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH)
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val PREF_DATE_MAP_KEY = "pref_date_map"
|
|
||||||
private const val PREF_CDN_URL = "pref_cdn_url"
|
|
||||||
private const val PREF_IMG_QUALITY_KEY = "pref_image_quality"
|
|
||||||
private const val PREF_SHORT_TITLE = "pref_short_title"
|
|
||||||
|
|
||||||
private const val DEFAULT_CDN = "edge.fast4speed.rsvp"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,169 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
typealias ApiDetailsResponse = Data<DetailsResponse>
|
|
||||||
|
|
||||||
typealias ApiPageListResponse = Data<PageList>
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Data<T>(val data: T)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Edges<T>(val edges: List<T>)
|
|
||||||
|
|
||||||
interface BrowseResponse {
|
|
||||||
val chapters: Edges<ChapterResponse>
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PopularResponse(
|
|
||||||
@SerialName("queryPopularChapters") override val chapters: Edges<ChapterResponse>,
|
|
||||||
) : BrowseResponse
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchResponse(
|
|
||||||
@SerialName("queryChapters") override val chapters: Edges<ChapterResponse>,
|
|
||||||
) : BrowseResponse
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class DetailsResponse(
|
|
||||||
@SerialName("queryChapter") val details: ChapterResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class ChapterResponse(
|
|
||||||
@SerialName("_id") val id: String,
|
|
||||||
val name: String,
|
|
||||||
val uploadDate: String? = null,
|
|
||||||
val format: String? = null,
|
|
||||||
val description: String? = null,
|
|
||||||
val language: String? = null,
|
|
||||||
val pages: Int? = null,
|
|
||||||
@SerialName("firstPics") val cover: List<Url>? = emptyList(),
|
|
||||||
val tags: List<Tag>? = emptyList(),
|
|
||||||
) {
|
|
||||||
fun toSManga(shortTitle: Boolean) = SManga.create().apply {
|
|
||||||
url = id
|
|
||||||
title = if (shortTitle) name.replace(shortenTitleRegex, "").trim() else name
|
|
||||||
thumbnail_url = cover?.firstOrNull()?.absUrl
|
|
||||||
author = this@ChapterResponse.author
|
|
||||||
artist = author
|
|
||||||
genre = genres
|
|
||||||
description = buildString {
|
|
||||||
if (!this@ChapterResponse.description.isNullOrEmpty()) append(this@ChapterResponse.description.trim(), "\n\n")
|
|
||||||
if (formatParsed != null) append("Format: ${formatParsed}\n")
|
|
||||||
if (languageParsed != null) append("Language: $languageParsed\n")
|
|
||||||
if (group != null) append("Group: $group\n")
|
|
||||||
if (characters != null) append("Character(s): $characters\n")
|
|
||||||
if (parody != null) append("Parody: $parody\n")
|
|
||||||
if (magazine != null) append("Magazine: $magazine\n")
|
|
||||||
if (pages != null) append("Pages: $pages\n")
|
|
||||||
}
|
|
||||||
status = SManga.COMPLETED
|
|
||||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
|
||||||
initialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val formatParsed = when (format) {
|
|
||||||
"artistcg" -> "ArtistCG"
|
|
||||||
"gamecg" -> "GameCG"
|
|
||||||
"imageset" -> "ImageSet"
|
|
||||||
else -> format?.capitalize()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val languageParsed = when (language) {
|
|
||||||
"en" -> "English"
|
|
||||||
"jp" -> "Japanese"
|
|
||||||
"cn" -> "Chinese"
|
|
||||||
"es" -> "Spanish"
|
|
||||||
else -> language
|
|
||||||
}
|
|
||||||
|
|
||||||
private val author = tags?.firstOrNull { it.tagType == "artist" }?.tagName?.capitalize()
|
|
||||||
|
|
||||||
private val group = tags?.filter { it.tagType == "group" }
|
|
||||||
?.joinToString { it.tagName.capitalize() }
|
|
||||||
?.takeUnless { it.isEmpty() }
|
|
||||||
|
|
||||||
private val characters = tags?.filter { it.tagType == "character" }
|
|
||||||
?.joinToString { it.tagName.capitalize() }
|
|
||||||
?.takeUnless { it.isEmpty() }
|
|
||||||
|
|
||||||
private val parody = tags?.filter { it.tagType == "parody" }
|
|
||||||
?.joinToString { it.tagName.capitalize() }
|
|
||||||
?.takeUnless { it.isEmpty() }
|
|
||||||
|
|
||||||
private val magazine = tags?.filter { it.tagType == "magazine" }
|
|
||||||
?.joinToString { it.tagName.capitalize() }
|
|
||||||
?.takeUnless { it.isEmpty() }
|
|
||||||
|
|
||||||
private val genres = tags?.filterNot { it.tagType in filterTags }
|
|
||||||
?.joinToString { it.tagName.capitalize() }
|
|
||||||
?.takeUnless { it.isEmpty() }
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val filterTags = listOf("artist", "group", "character", "parody", "magazine")
|
|
||||||
private val shortenTitleRegex = Regex("""(\[[^]]*]|[({][^)}]*[)}])""")
|
|
||||||
|
|
||||||
private fun String.capitalize(): String {
|
|
||||||
return this.trim().split(" ").joinToString(" ") { word ->
|
|
||||||
word.replaceFirstChar {
|
|
||||||
if (it.isLowerCase()) {
|
|
||||||
it.titlecase(
|
|
||||||
Locale.getDefault(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
it.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Url(val url: String) {
|
|
||||||
val absUrl get() = url.toAbsUrl()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun String.toAbsUrl(baseUrl: String = loUrl): String {
|
|
||||||
return if (this.matches(urlRegex)) {
|
|
||||||
this
|
|
||||||
} else {
|
|
||||||
baseUrl + this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val loUrl = "https://127.0.0.1/"
|
|
||||||
private val urlRegex = Regex("^https?://.*")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Tag(
|
|
||||||
val tagName: String,
|
|
||||||
val tagType: String? = "genre",
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PageList(
|
|
||||||
@SerialName("queryChapter") val chapter: PageUrl,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PageUrl(
|
|
||||||
@SerialName("_id") val id: String,
|
|
||||||
@SerialName("pictureUrls") val pages: List<Pages?>? = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class Pages(
|
|
||||||
@SerialName("picCdn") val urlPart: String,
|
|
||||||
@SerialName("pics") val qualityOriginal: List<Url>,
|
|
||||||
@SerialName("picsM") val qualityMedium: List<Url?>? = emptyList(),
|
|
||||||
)
|
|
|
@ -1,19 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.SourceFactory
|
|
||||||
|
|
||||||
class AnimeHFactory : SourceFactory {
|
|
||||||
override fun createSources() = listOf(
|
|
||||||
AnimeHAll(),
|
|
||||||
AnimeHEn(),
|
|
||||||
AnimeHJa(),
|
|
||||||
AnimeHZh(),
|
|
||||||
AnimeHEs(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
class AnimeHAll : AnimeH("all") { override val id = 5098173700376022513 }
|
|
||||||
class AnimeHEn : AnimeH("en") { override val id = 4370122548313941497 }
|
|
||||||
class AnimeHJa : AnimeH("ja", "jp") { override val id = 8948948503520127713 }
|
|
||||||
class AnimeHZh : AnimeH("zh", "cn") { override val id = 3874510362699054213 }
|
|
||||||
class AnimeHEs : AnimeH("es") { override val id = 2790053117909987291 }
|
|
|
@ -1,69 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.model.Filter
|
|
||||||
import eu.kanade.tachiyomi.source.model.FilterList
|
|
||||||
|
|
||||||
abstract class SelectFilter(
|
|
||||||
displayName: String,
|
|
||||||
private val options: Array<Pair<String, String>>,
|
|
||||||
) : Filter.Select<String>(
|
|
||||||
displayName,
|
|
||||||
options.map { it.first }.toTypedArray(),
|
|
||||||
) {
|
|
||||||
val selected get() = options[state].second.takeUnless { it.isEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class TextFilter(name: String) : Filter.Text(name)
|
|
||||||
|
|
||||||
abstract class TagFilter(name: String) : TextFilter(name) {
|
|
||||||
val tags get() = state.split(",")
|
|
||||||
.map { it.trim().lowercase() }
|
|
||||||
.filter { it.isNotEmpty() }
|
|
||||||
.takeUnless { it.isEmpty() }
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract class PageFilter(name: String) : TextFilter(name) {
|
|
||||||
val value get() = state.trim().toIntOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortFilter : SelectFilter(
|
|
||||||
"Sort By",
|
|
||||||
arrayOf(
|
|
||||||
Pair("Update", ""),
|
|
||||||
Pair("Popular", "Popular"),
|
|
||||||
Pair("Name Ascending", "Name_ASC"),
|
|
||||||
Pair("Name Descending", "Name_DESC"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class FormatFilter : SelectFilter(
|
|
||||||
"Format",
|
|
||||||
arrayOf(
|
|
||||||
Pair("", ""),
|
|
||||||
Pair("Manga", "manga"),
|
|
||||||
Pair("Doujinshi", "doujinshi"),
|
|
||||||
Pair("ArtistCG", "artistcg"),
|
|
||||||
Pair("GameCG", "gamecg"),
|
|
||||||
Pair("ImageSet", "imageset"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
class MinPageFilter : PageFilter("Minimum Pages")
|
|
||||||
|
|
||||||
class MaxPageFilter : PageFilter("Maximum Pages")
|
|
||||||
|
|
||||||
class IncludedTagFilter : TagFilter("Include Tags")
|
|
||||||
|
|
||||||
class ExcludedTagFilter : TagFilter("Exclude Tags")
|
|
||||||
|
|
||||||
fun getFilters() = FilterList(
|
|
||||||
SortFilter(),
|
|
||||||
FormatFilter(),
|
|
||||||
Filter.Separator(),
|
|
||||||
MinPageFilter(),
|
|
||||||
MaxPageFilter(),
|
|
||||||
Filter.Separator(),
|
|
||||||
IncludedTagFilter(),
|
|
||||||
ExcludedTagFilter(),
|
|
||||||
Filter.Header("comma (,) separated tag/parody/character/artist/group"),
|
|
||||||
)
|
|
|
@ -1,39 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GraphQL<T>(
|
|
||||||
val variables: T,
|
|
||||||
val query: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class PopularVariables(
|
|
||||||
val size: Int,
|
|
||||||
val page: Int,
|
|
||||||
val dateRange: Int,
|
|
||||||
val language: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchVariables(
|
|
||||||
val size: Int,
|
|
||||||
val page: Int,
|
|
||||||
val search: SearchPayload,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SearchPayload(
|
|
||||||
val query: String?,
|
|
||||||
val language: String,
|
|
||||||
val sortBy: String?,
|
|
||||||
val format: String?,
|
|
||||||
val tags: List<String>?,
|
|
||||||
val excludeTags: List<String>?,
|
|
||||||
val pagesRangeStart: Int?,
|
|
||||||
val pagesRangeEnd: Int?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class IdVariables(val id: String)
|
|
|
@ -1,106 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
private fun buildQuery(queryAction: () -> String): String {
|
|
||||||
return queryAction()
|
|
||||||
.trimIndent()
|
|
||||||
.replace("%", "$")
|
|
||||||
}
|
|
||||||
|
|
||||||
val POPULAR_QUERY: String = buildQuery {
|
|
||||||
"""
|
|
||||||
query(
|
|
||||||
%size: Int
|
|
||||||
%language: String
|
|
||||||
%dateRange: Int
|
|
||||||
%page: Int
|
|
||||||
) {
|
|
||||||
queryPopularChapters(
|
|
||||||
size: %size
|
|
||||||
language: %language
|
|
||||||
dateRange: %dateRange
|
|
||||||
page: %page
|
|
||||||
) {
|
|
||||||
edges {
|
|
||||||
_id
|
|
||||||
name
|
|
||||||
uploadDate
|
|
||||||
format
|
|
||||||
description
|
|
||||||
language
|
|
||||||
pages
|
|
||||||
firstPics
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
val SEARCH_QUERY: String = buildQuery {
|
|
||||||
"""
|
|
||||||
query(
|
|
||||||
%search: SearchInput
|
|
||||||
%size: Int
|
|
||||||
%page: Int
|
|
||||||
) {
|
|
||||||
queryChapters(
|
|
||||||
limit: %size
|
|
||||||
search: %search
|
|
||||||
page: %page
|
|
||||||
) {
|
|
||||||
edges {
|
|
||||||
_id
|
|
||||||
name
|
|
||||||
uploadDate
|
|
||||||
format
|
|
||||||
description
|
|
||||||
language
|
|
||||||
pages
|
|
||||||
firstPics
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
val DETAILS_QUERY: String = buildQuery {
|
|
||||||
"""
|
|
||||||
query(
|
|
||||||
%id: String
|
|
||||||
) {
|
|
||||||
queryChapter(
|
|
||||||
chapterId: %id
|
|
||||||
) {
|
|
||||||
_id
|
|
||||||
name
|
|
||||||
uploadDate
|
|
||||||
format
|
|
||||||
description
|
|
||||||
language
|
|
||||||
pages
|
|
||||||
firstPics
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
|
|
||||||
val PAGES_QUERY: String = buildQuery {
|
|
||||||
"""
|
|
||||||
query(
|
|
||||||
%id: String
|
|
||||||
) {
|
|
||||||
queryChapter(
|
|
||||||
chapterId: %id
|
|
||||||
) {
|
|
||||||
_id
|
|
||||||
pictureUrls {
|
|
||||||
picCdn
|
|
||||||
pics
|
|
||||||
picsM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.all.ninenineninehentai
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
class AnimeHUrlActivity : 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", "${AnimeH.SEARCH_PREFIX}$id")
|
|
||||||
putExtra("filter", packageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
startActivity(mainIntent)
|
|
||||||
} catch (e: ActivityNotFoundException) {
|
|
||||||
Log.e("AnimeHUrlActivity", e.toString())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("AnimeHUrlActivity", "could not parse uri from intent $intent")
|
|
||||||
}
|
|
||||||
|
|
||||||
finish()
|
|
||||||
exitProcess(0)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'ReadAllComics'
|
|
||||||
extClass = '.ReadAllComics'
|
|
||||||
extVersionCode = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,193 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.en.readallcomicscom
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
|
||||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
|
||||||
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.ParsedHttpSource
|
|
||||||
import eu.kanade.tachiyomi.util.asJsoup
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
|
||||||
import okhttp3.Interceptor
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import org.jsoup.Jsoup
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.nodes.Element
|
|
||||||
import org.jsoup.select.Elements
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
class ReadAllComics : ParsedHttpSource() {
|
|
||||||
|
|
||||||
override val name = "ReadAllComics"
|
|
||||||
|
|
||||||
override val baseUrl = "https://readallcomics.com"
|
|
||||||
|
|
||||||
override val lang = "en"
|
|
||||||
|
|
||||||
override val supportsLatest = false
|
|
||||||
|
|
||||||
private lateinit var searchPageElements: Elements
|
|
||||||
|
|
||||||
override val client = network.cloudflareClient.newBuilder()
|
|
||||||
.addInterceptor(::archivedCategoryInterceptor)
|
|
||||||
.rateLimit(2)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
private fun archivedCategoryInterceptor(chain: Interceptor.Chain): Response {
|
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
|
|
||||||
val document = Jsoup.parse(
|
|
||||||
response.peekBody(Long.MAX_VALUE).string(),
|
|
||||||
request.url.toString(),
|
|
||||||
)
|
|
||||||
|
|
||||||
val newUrl = document.selectFirst(".description-archive > p > span > a")
|
|
||||||
?.attr("href")?.toHttpUrlOrNull()
|
|
||||||
?: return response
|
|
||||||
|
|
||||||
if (newUrl.pathSegments.contains("category")) {
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
return chain.proceed(
|
|
||||||
request.newBuilder()
|
|
||||||
.url(newUrl)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) =
|
|
||||||
GET("$baseUrl${if (page > 1)"/page/$page/" else ""}", headers)
|
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga {
|
|
||||||
val manga = SManga.create().apply {
|
|
||||||
val category = element.classNames()
|
|
||||||
.firstOrNull { it.startsWith("category-") }!!
|
|
||||||
.substringAfter("category-")
|
|
||||||
|
|
||||||
url = "/category/$category/"
|
|
||||||
title = category.replace("-", " ").capitalizeEachWord()
|
|
||||||
thumbnail_url = element.select("img").attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
return manga
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun popularMangaSelector() = "#post-area > div"
|
|
||||||
override fun popularMangaNextPageSelector() = "div.pagenavi > a.next"
|
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
|
||||||
return if (page == 1) {
|
|
||||||
client.newCall(searchMangaRequest(page, query, filters))
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { searchMangaParse(it) }
|
|
||||||
} else {
|
|
||||||
Observable.just(searchPageParse(page))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
|
||||||
val url = baseUrl.toHttpUrl().newBuilder().apply {
|
|
||||||
addQueryParameter("story", query)
|
|
||||||
addQueryParameter("s", "")
|
|
||||||
addQueryParameter("type", "comic")
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
return GET(url, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
|
||||||
searchPageElements = response.asJsoup().select(searchMangaSelector())
|
|
||||||
|
|
||||||
return searchPageParse(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun searchPageParse(page: Int): MangasPage {
|
|
||||||
val mangas = mutableListOf<SManga>()
|
|
||||||
val endRange = ((page * 24) - 1).let { if (it <= searchPageElements.lastIndex) it else searchPageElements.lastIndex }
|
|
||||||
|
|
||||||
for (i in (((page - 1) * 24)..endRange)) {
|
|
||||||
mangas.add(
|
|
||||||
searchMangaFromElement(searchPageElements[i]),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MangasPage(mangas, endRange < searchPageElements.lastIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaFromElement(element: Element) = SManga.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
title = element.text()
|
|
||||||
thumbnail_url = "https://fakeimg.pl/200x300/?text=No%20Cover%0AOn%20Search&font_size=62"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun searchMangaSelector() = ".categories a"
|
|
||||||
override fun searchMangaNextPageSelector() = null
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
|
||||||
title = document.selectFirst("h1")!!.text()
|
|
||||||
genre = document.select("p strong").joinToString { it.text() }
|
|
||||||
author = document.select("p > strong").last()?.text()
|
|
||||||
description = buildString {
|
|
||||||
document.select(".b > strong").forEach { element ->
|
|
||||||
val vol = element.previousElementSibling()
|
|
||||||
if (isNotBlank()) {
|
|
||||||
append("\n\n")
|
|
||||||
}
|
|
||||||
if (vol?.tagName() == "span") {
|
|
||||||
append(vol.text(), "\n")
|
|
||||||
}
|
|
||||||
append(element.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thumbnail_url = document.select("p img").attr("abs:src")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun chapterListSelector() = ".list-story a"
|
|
||||||
|
|
||||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
|
||||||
setUrlWithoutDomain(element.attr("href"))
|
|
||||||
name = element.attr("title")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun pageListParse(document: Document): List<Page> {
|
|
||||||
return document.select("body img:not(body div[id=\"logo\"] img)").mapIndexed { idx, element ->
|
|
||||||
Page(idx, "", element.attr("abs:src"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.capitalizeEachWord(): String {
|
|
||||||
val result = StringBuilder(length)
|
|
||||||
var capitalize = true
|
|
||||||
for (char in this) {
|
|
||||||
result.append(
|
|
||||||
if (capitalize) {
|
|
||||||
char.uppercase()
|
|
||||||
} else {
|
|
||||||
char.lowercase()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
capitalize = char.isWhitespace()
|
|
||||||
}
|
|
||||||
return result.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun imageUrlParse(document: Document) =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesRequest(page: Int) =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesFromElement(element: Element) =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesSelector() =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
override fun latestUpdatesNextPageSelector() =
|
|
||||||
throw UnsupportedOperationException()
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
ext {
|
|
||||||
extName = 'Qinqin Manhua'
|
|
||||||
extClass = '.Qinqin'
|
|
||||||
themePkg = 'sinmh'
|
|
||||||
baseUrl = 'http://www.acgwd.com'
|
|
||||||
overrideVersionCode = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
|
Before Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.9 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 11 KiB |
|
@ -1,30 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.extension.zh.qinqin
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import eu.kanade.tachiyomi.multisrc.sinmh.SinMH
|
|
||||||
import eu.kanade.tachiyomi.network.GET
|
|
||||||
import org.jsoup.nodes.Document
|
|
||||||
import org.jsoup.select.Elements
|
|
||||||
import javax.crypto.Cipher
|
|
||||||
import javax.crypto.spec.IvParameterSpec
|
|
||||||
import javax.crypto.spec.SecretKeySpec
|
|
||||||
|
|
||||||
class Qinqin : SinMH("亲亲漫画", "http://www.acgwd.com") {
|
|
||||||
|
|
||||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/list/post/?page=$page", headers)
|
|
||||||
|
|
||||||
override fun mangaDetailsParse(document: Document) = mangaDetailsParseDMZJStyle(document, hasBreadcrumb = true)
|
|
||||||
|
|
||||||
override fun Elements.sectionsDescending() = this
|
|
||||||
|
|
||||||
// https://www.acgud.com/js/jmzz20191018.js
|
|
||||||
override fun parsePageImages(chapterImages: String): List<String> {
|
|
||||||
val key = SecretKeySpec("cxNB23W8xzKJV26O".toByteArray(), "AES")
|
|
||||||
val iv = IvParameterSpec("opb4x7z21vg1f3gI".toByteArray())
|
|
||||||
val result = Cipher.getInstance("AES/CBC/PKCS7Padding").run {
|
|
||||||
init(Cipher.DECRYPT_MODE, key, iv)
|
|
||||||
doFinal(Base64.decode(chapterImages, Base64.DEFAULT))
|
|
||||||
}
|
|
||||||
return super.parsePageImages(String(result))
|
|
||||||
}
|
|
||||||
}
|
|