added dummy extension
@ -1,7 +1,7 @@
|
||||
// used both in common.gradle and themesources library
|
||||
dependencies {
|
||||
// Lib 1.2, but using specific commit so we don't need to bump up the version
|
||||
compileOnly "com.github.tachiyomiorg:extensions-lib:a596412"
|
||||
compileOnly "com.github.jmir1:extensions-lib:e6cc2ea"
|
||||
|
||||
// These are provided by the app itself
|
||||
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
@ -1,185 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.comicake
|
||||
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.extensions.BuildConfig
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
abstract class ComiCake(
|
||||
override val name: String,
|
||||
final override val baseUrl: String,
|
||||
override val lang: String,
|
||||
readerEndpoint: String = COMICAKE_DEFAULT_READER_ENDPOINT,
|
||||
apiEndpoint: String = COMICAKE_DEFAULT_API_ENDPOINT
|
||||
) : HttpSource() {
|
||||
|
||||
override val versionId = 1
|
||||
override val supportsLatest = true
|
||||
private val readerBase = baseUrl + readerEndpoint
|
||||
private var apiBase = baseUrl + apiEndpoint
|
||||
|
||||
private val userAgent = "Mozilla/5.0 (" +
|
||||
"Android ${Build.VERSION.RELEASE}; Mobile) " +
|
||||
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", userAgent)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$apiBase/comics.json?ordering=-created_at&page=$page") // Not actually popular, just latest added to system
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val res = response.body()!!.string()
|
||||
return getMangasPageFromComicsResponse(res)
|
||||
}
|
||||
|
||||
private fun getMangasPageFromComicsResponse(json: String, nested: Boolean = false): MangasPage {
|
||||
val response = JSONObject(json)
|
||||
val results = response.getJSONArray("results")
|
||||
val mangas = ArrayList<SManga>()
|
||||
val ids = mutableListOf<Int>()
|
||||
|
||||
for (i in 0 until results.length()) {
|
||||
val obj = results.getJSONObject(i)
|
||||
if (nested) {
|
||||
val nestedComic = obj.getJSONObject("comic")
|
||||
val id = nestedComic.getInt("id")
|
||||
if (ids.contains(id))
|
||||
continue
|
||||
ids.add(id)
|
||||
val manga = SManga.create()
|
||||
manga.url = id.toString()
|
||||
manga.title = nestedComic.getString("name")
|
||||
mangas.add(manga)
|
||||
} else {
|
||||
val id = obj.getInt("id")
|
||||
if (ids.contains(id))
|
||||
continue
|
||||
ids.add(id)
|
||||
mangas.add(parseComicJson(obj))
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, !(response.getString("next").isNullOrEmpty() || response.getString("next") == "null"))
|
||||
}
|
||||
|
||||
// Shenanigans to allow "open in webview" to show a webpage instead of JSON
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(apiMangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mangaDetailsParse(response).apply { initialized = true }
|
||||
}
|
||||
}
|
||||
|
||||
private fun apiMangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$apiBase/comics/${manga.url}.json")
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(manga.description!!.substringAfterLast("\n"))
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val comicJson = JSONObject(response.body()!!.string())
|
||||
return parseComicJson(comicJson, true)
|
||||
}
|
||||
|
||||
private fun parseComicJson(obj: JSONObject, human: Boolean = false) = SManga.create().apply {
|
||||
url = if (human) {
|
||||
"$readerBase/series/${obj.getString("slug")}/"
|
||||
} else {
|
||||
obj.getInt("id").toString() // Yeah, I know... Feel free to improve on this
|
||||
}
|
||||
title = obj.getString("name")
|
||||
thumbnail_url = obj.getString("cover")
|
||||
author = parseListNames(obj.getJSONArray("author"))
|
||||
artist = parseListNames(obj.getJSONArray("artist"))
|
||||
description = obj.getString("description") +
|
||||
"\n\n${readerBase}series/${obj.getString("slug")}/" // webpage for "open in webview"
|
||||
genre = parseListNames(obj.getJSONArray("tags"))
|
||||
status = SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun parseListNames(arr: JSONArray): String {
|
||||
val hold = ArrayList<String>(arr.length())
|
||||
for (i in 0 until arr.length())
|
||||
hold.add(arr.getJSONObject(i).getString("name"))
|
||||
return hold.sorted().joinToString(", ")
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
// TODO filters
|
||||
return GET("$apiBase/comics.json?page=$page&search=$query")
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val res = response.body()!!.string()
|
||||
return getMangasPageFromComicsResponse(res)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$apiBase/chapters.json?page=$page&expand=comic")
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val res = response.body()!!.string()
|
||||
return getMangasPageFromComicsResponse(res, true)
|
||||
}
|
||||
|
||||
private fun parseChapterJson(obj: JSONObject) = SChapter.create().apply {
|
||||
name = obj.getString("title") // title will always have content, vs. name that's an optional field
|
||||
chapter_number = (obj.getInt("chapter") + (obj.getInt("subchapter") / 10.0)).toFloat()
|
||||
date_upload = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZ", Locale.getDefault()).parse(obj.getString("published_at"))?.time ?: 0L
|
||||
// TODO scanlator field by adding team to expandable in CC (low priority given the use case of CC)
|
||||
url = obj.getString("manifest")
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET("$apiBase/chapters.json?comic=${manga.url}&ordering=-volume%2C-chapter%2C-subchapter&n=1000", headers) // There's no pagination in Tachiyomi for chapters so we get 1k chapters
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapterJson = JSONObject(response.body()!!.string())
|
||||
val results = chapterJson.getJSONArray("results")
|
||||
val ret = ArrayList<SChapter>()
|
||||
for (i in 0 until results.length()) {
|
||||
ret.add(parseChapterJson(results.getJSONObject(i)))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val webPub = JSONObject(response.body()!!.string())
|
||||
val readingOrder = webPub.getJSONArray("readingOrder")
|
||||
val ret = ArrayList<Page>()
|
||||
for (i in 0 until readingOrder.length()) {
|
||||
val pageUrl = readingOrder.getJSONObject(i).getString("href")
|
||||
ret.add(Page(i, "", pageUrl))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("This method should not be called!")
|
||||
|
||||
companion object {
|
||||
private const val COMICAKE_DEFAULT_API_ENDPOINT = "/api" // Highly unlikely to change
|
||||
private const val COMICAKE_DEFAULT_READER_ENDPOINT = "/r/" // Can change based on CC config
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.comicake
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class ComiCakeGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "comicake"
|
||||
|
||||
override val themeClass = "ComiCake"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("LetItGo Scans", "https://reader.letitgo.scans.today", "en"),
|
||||
SingleLang("WhimSubs", "https://whimsubs.xyz", "en")
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
ComiCakeGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,438 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.eromuse
|
||||
|
||||
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 okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
@ExperimentalStdlibApi
|
||||
open class EroMuse(override val name: String, override val baseUrl: String) : HttpSource() {
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
/**
|
||||
* Browse, search, and latest all run through an ArrayDeque of requests that acts as a stack we push and pop to/from
|
||||
* For the fetch functions, we only need to worry about pushing the first page to the stack because subsequent pages
|
||||
* get pushed to the stack during parseManga(). Page 1's URL must include page=1 if the next page would be page=2,
|
||||
* if page 2 is path_to/2, nothing special needs to be done.
|
||||
*/
|
||||
|
||||
// the stack - shouldn't need to touch these except for visibility
|
||||
protected data class StackItem(val url: String, val pageType: Int)
|
||||
private lateinit var stackItem: StackItem
|
||||
protected val pageStack = ArrayDeque<StackItem>()
|
||||
companion object {
|
||||
const val VARIOUS_AUTHORS = 0
|
||||
const val AUTHOR = 1
|
||||
const val SEARCH_RESULTS_OR_BASE = 2
|
||||
}
|
||||
protected lateinit var currentSortingMode: String
|
||||
|
||||
private val albums = getAlbumList()
|
||||
|
||||
// might need to override for new sources
|
||||
private val nextPageSelector = ".pagination span.current + span a"
|
||||
protected open val albumSelector = "a.c-tile:has(img):not(:has(.members-only))"
|
||||
protected open val topLevelPathSegment = "comics/album"
|
||||
private val pageQueryRegex = Regex("""page=\d+""")
|
||||
|
||||
private fun Document.nextPageOrNull(): String? {
|
||||
val url = this.location()
|
||||
return this.select(nextPageSelector).firstOrNull()?.text()?.toIntOrNull()?.let { int ->
|
||||
if (url.contains(pageQueryRegex)) {
|
||||
url.replace(pageQueryRegex, "page=$int")
|
||||
} else {
|
||||
val httpUrl = HttpUrl.parse(url)!!
|
||||
val builder = if (httpUrl.pathSegments().last().toIntOrNull() is Int) {
|
||||
httpUrl.newBuilder().removePathSegment(httpUrl.pathSegments().lastIndex)
|
||||
} else {
|
||||
httpUrl.newBuilder()
|
||||
}
|
||||
builder.addPathSegment(int.toString()).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Document.addNextPageToStack() {
|
||||
this.nextPageOrNull()?.let { pageStack.add(StackItem(it, stackItem.pageType)) }
|
||||
}
|
||||
|
||||
protected fun Element.imgAttr(): String = if (this.hasAttr("data-src")) this.attr("abs:data-src") else this.attr("abs:src")
|
||||
|
||||
private fun mangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
title = element.text()
|
||||
thumbnail_url = element.select("img").firstOrNull()?.imgAttr()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getAlbumType(url: String, default: Int = AUTHOR): Int {
|
||||
return albums.filter { it.third != SEARCH_RESULTS_OR_BASE && url.contains(it.second, true) }
|
||||
.getOrElse(0) { Triple(null, null, default) }.third
|
||||
}
|
||||
|
||||
protected fun parseManga(document: Document): MangasPage {
|
||||
fun internalParse(internalDocument: Document): List<SManga> {
|
||||
val authorDocument = if (stackItem.pageType == VARIOUS_AUTHORS) {
|
||||
internalDocument.select(albumSelector)?.let {
|
||||
elements ->
|
||||
elements.reversed().map { pageStack.addLast(StackItem(it.attr("abs:href"), AUTHOR)) }
|
||||
}
|
||||
client.newCall(stackRequest()).execute().asJsoup()
|
||||
} else {
|
||||
internalDocument
|
||||
}
|
||||
authorDocument.addNextPageToStack()
|
||||
return authorDocument.select(albumSelector).map { mangaFromElement(it) }
|
||||
}
|
||||
|
||||
if (stackItem.pageType in listOf(VARIOUS_AUTHORS, SEARCH_RESULTS_OR_BASE)) document.addNextPageToStack()
|
||||
val mangas = when (stackItem.pageType) {
|
||||
VARIOUS_AUTHORS -> {
|
||||
document.select(albumSelector)?.let {
|
||||
elements ->
|
||||
elements.reversed().map { pageStack.addLast(StackItem(it.attr("abs:href"), AUTHOR)) }
|
||||
}
|
||||
internalParse(document)
|
||||
}
|
||||
AUTHOR -> {
|
||||
internalParse(document)
|
||||
}
|
||||
SEARCH_RESULTS_OR_BASE -> {
|
||||
val searchMangas = mutableListOf<SManga>()
|
||||
document.select(albumSelector)
|
||||
.map { element ->
|
||||
val url = element.attr("abs:href")
|
||||
val depth = url.removePrefix("$baseUrl/$topLevelPathSegment/").split("/").count()
|
||||
|
||||
when (getAlbumType(url)) {
|
||||
VARIOUS_AUTHORS -> {
|
||||
when (depth) {
|
||||
1 -> { // eg. /comics/album/Fakku-Comics
|
||||
pageStack.addLast(StackItem(url, VARIOUS_AUTHORS))
|
||||
if (searchMangas.isEmpty()) searchMangas += internalParse(client.newCall(stackRequest()).execute().asJsoup()) else null
|
||||
}
|
||||
2 -> { // eg. /comics/album/Fakku-Comics/Bosshi
|
||||
pageStack.addLast(StackItem(url, AUTHOR))
|
||||
if (searchMangas.isEmpty()) searchMangas += internalParse(client.newCall(stackRequest()).execute().asJsoup()) else null
|
||||
}
|
||||
else -> {
|
||||
// eg. 3 -> /comics/album/Fakku-Comics/Bosshi/After-Summer-After
|
||||
// eg. 5 -> /comics/album/Various-Authors/Firollian/Reward/Reward-22/ElfAlfie
|
||||
// eg. 6 -> /comics/album/Various-Authors/Firollian/Area69/Area69-no_1/SamusAran/001_Dialogue
|
||||
searchMangas.add(mangaFromElement(element))
|
||||
}
|
||||
}
|
||||
}
|
||||
AUTHOR -> {
|
||||
if (depth == 1) { // eg. /comics/album/ShadBase-Comics
|
||||
pageStack.addLast(StackItem(url, AUTHOR))
|
||||
if (searchMangas.isEmpty()) searchMangas += internalParse(client.newCall(stackRequest()).execute().asJsoup()) else null
|
||||
} else {
|
||||
// eg. 2 -> /comics/album/ShadBase-Comics/RickMorty
|
||||
// eg. 3 -> /comics/album/Incase-Comics/Comic/Alfie
|
||||
searchMangas.add(mangaFromElement(element))
|
||||
}
|
||||
}
|
||||
else -> null // SEARCH_RESULTS_OR_BASE shouldn't be a case
|
||||
}
|
||||
}
|
||||
searchMangas
|
||||
}
|
||||
else -> emptyList()
|
||||
}
|
||||
return MangasPage(mangas, pageStack.isNotEmpty())
|
||||
}
|
||||
|
||||
protected fun stackRequest(): Request {
|
||||
stackItem = pageStack.removeLast()
|
||||
val url = if (stackItem.pageType == AUTHOR && currentSortingMode.isNotEmpty() && !stackItem.url.contains("sort")) {
|
||||
HttpUrl.parse(stackItem.url)!!.newBuilder().addQueryParameter("sort", currentSortingMode).toString()
|
||||
} else {
|
||||
stackItem.url
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
protected fun fetchManga(url: String, page: Int, sortingMode: String): Observable<MangasPage> {
|
||||
if (page == 1) {
|
||||
pageStack.clear()
|
||||
pageStack.addLast(StackItem(url, VARIOUS_AUTHORS))
|
||||
currentSortingMode = sortingMode
|
||||
}
|
||||
|
||||
return client.newCall(stackRequest())
|
||||
.asObservableSuccess()
|
||||
.map { response -> parseManga(response.asJsoup()) }
|
||||
}
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> = fetchManga("$baseUrl/comics/album/Various-Authors", page, "")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException("Not used")
|
||||
override fun popularMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Latest
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> = fetchManga("$baseUrl/comics/album/Various-Authors?sort=date", page, "date")
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used")
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Search
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (page == 1) {
|
||||
pageStack.clear()
|
||||
|
||||
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||
currentSortingMode = filterList.filterIsInstance<SortFilter>().first().toQueryValue()
|
||||
|
||||
if (query.isNotBlank()) {
|
||||
val url = HttpUrl.parse("$baseUrl/search?q=$query")!!.newBuilder().apply {
|
||||
if (currentSortingMode.isNotEmpty()) addQueryParameter("sort", currentSortingMode)
|
||||
addQueryParameter("page", "1")
|
||||
}
|
||||
pageStack.addLast(StackItem(url.toString(), SEARCH_RESULTS_OR_BASE))
|
||||
} else {
|
||||
val albumFilter = filterList.filterIsInstance<AlbumFilter>().first().selection()
|
||||
val url = HttpUrl.parse("$baseUrl/comics/${albumFilter.pathSegments}")!!.newBuilder().apply {
|
||||
if (currentSortingMode.isNotEmpty()) addQueryParameter("sort", currentSortingMode)
|
||||
if (albumFilter.pageType != AUTHOR) addQueryParameter("page", "1")
|
||||
}
|
||||
pageStack.addLast(StackItem(url.toString(), albumFilter.pageType))
|
||||
}
|
||||
}
|
||||
|
||||
return client.newCall(stackRequest())
|
||||
.asObservableSuccess()
|
||||
.map { response -> parseManga(response.asJsoup()) }
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException("Not used")
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
return SManga.create().apply {
|
||||
with(response.asJsoup()) {
|
||||
setUrlWithoutDomain(response.request().url().toString())
|
||||
thumbnail_url = select("$albumSelector img").firstOrNull()?.imgAttr()
|
||||
author = when (getAlbumType(url)) {
|
||||
AUTHOR -> {
|
||||
// eg. https://comics.8muses.com/comics/album/ShadBase-Comics/RickMorty
|
||||
// eg. https://comics.8muses.com/comics/album/Incase-Comics/Comic/Alfie
|
||||
select("div.top-menu-breadcrumb li:nth-child(2)").text()
|
||||
}
|
||||
VARIOUS_AUTHORS -> {
|
||||
// eg. https://comics.8muses.com/comics/album/Various-Authors/NLT-Media/A-Sunday-Schooling
|
||||
select("div.top-menu-breadcrumb li:nth-child(3)").text()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
protected open val linkedChapterSelector = "a.c-tile:has(img)[href*=/comics/album/]"
|
||||
protected open val pageThumbnailSelector = "a.c-tile:has(img)[href*=/comics/picture/] img"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
fun parseChapters(document: Document, isFirstPage: Boolean, chapters: ArrayDeque<SChapter>): List<SChapter> {
|
||||
// Linked chapters
|
||||
document.select(linkedChapterSelector)
|
||||
.mapNotNull {
|
||||
chapters.addFirst(
|
||||
SChapter.create().apply {
|
||||
name = it.text()
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (isFirstPage) {
|
||||
// Self
|
||||
document.select(pageThumbnailSelector).firstOrNull()?.let {
|
||||
chapters.add(
|
||||
SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
setUrlWithoutDomain(response.request().url().toString())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
document.nextPageOrNull()?.let { url -> parseChapters(client.newCall(GET(url, headers)).execute().asJsoup(), false, chapters) }
|
||||
return chapters
|
||||
}
|
||||
|
||||
return parseChapters(response.asJsoup(), true, ArrayDeque())
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
protected open val pageThumbnailPathSegment = "/th/"
|
||||
protected open val pageFullSizePathSegment = "/fl/"
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
fun parsePages(
|
||||
document: Document,
|
||||
nestedChapterDocuments: ArrayDeque<Document> = ArrayDeque(),
|
||||
pages: ArrayList<Page> = ArrayList()
|
||||
): List<Page> {
|
||||
|
||||
// Nested chapters aka folders
|
||||
document.select(linkedChapterSelector)
|
||||
.mapNotNull {
|
||||
nestedChapterDocuments.add(
|
||||
client.newCall(GET(it.attr("abs:href"), headers)).execute().asJsoup()
|
||||
)
|
||||
}
|
||||
|
||||
var lastPage: Int = pages.size
|
||||
pages.addAll(
|
||||
document.select(pageThumbnailSelector).mapIndexed { i, img ->
|
||||
Page(lastPage + i, "", img.imgAttr().replace(pageThumbnailPathSegment, pageFullSizePathSegment))
|
||||
}
|
||||
)
|
||||
|
||||
document.nextPageOrNull()?.let {
|
||||
url ->
|
||||
pages.addAll(parsePages(client.newCall(GET(url, headers)).execute().asJsoup(), nestedChapterDocuments, pages))
|
||||
}
|
||||
|
||||
while (!nestedChapterDocuments.isEmpty()) {
|
||||
pages.addAll(parsePages(nestedChapterDocuments.removeFirst()))
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
return parsePages(response.asJsoup())
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
return FilterList(
|
||||
Filter.Header("Text search only combines with sort!"),
|
||||
Filter.Separator(),
|
||||
AlbumFilter(getAlbumList()),
|
||||
SortFilter(getSortList())
|
||||
)
|
||||
}
|
||||
|
||||
protected class AlbumFilter(private val vals: Array<Triple<String, String, Int>>) : Filter.Select<String>("Album", vals.map { it.first }.toTypedArray()) {
|
||||
fun selection() = AlbumFilterData(vals[state].second, vals[state].third)
|
||||
data class AlbumFilterData(val pathSegments: String, val pageType: Int)
|
||||
}
|
||||
protected open fun getAlbumList() = arrayOf(
|
||||
Triple("All Authors", "", SEARCH_RESULTS_OR_BASE),
|
||||
Triple("Various Authors", "album/Various-Authors", VARIOUS_AUTHORS),
|
||||
Triple("Fakku Comics", "album/Fakku-Comics", VARIOUS_AUTHORS),
|
||||
Triple("Hentai and Manga English", "album/Hentai-and-Manga-English", VARIOUS_AUTHORS),
|
||||
Triple("Fake Celebrities Sex Pictures", "album/Fake-Celebrities-Sex-Pictures", AUTHOR),
|
||||
Triple("MilfToon Comics", "album/MilfToon-Comics", AUTHOR),
|
||||
Triple("BE Story Club Comics", "album/BE-Story-Club-Comics", AUTHOR),
|
||||
Triple("ShadBase Comics", "album/ShadBase-Comics", AUTHOR),
|
||||
Triple("ZZZ Comics", "album/ZZZ-Comics", AUTHOR),
|
||||
Triple("PalComix Comics", "album/PalComix-Comics", AUTHOR),
|
||||
Triple("MCC Comics", "album/MCC-Comics", AUTHOR),
|
||||
Triple("Expansionfan Comics", "album/Expansionfan-Comics", AUTHOR),
|
||||
Triple("JAB Comics", "album/JAB-Comics", AUTHOR),
|
||||
Triple("Giantess Fan Comics", "album/Giantess-Fan-Comics", AUTHOR),
|
||||
Triple("Renderotica Comics", "album/Renderotica-Comics", AUTHOR),
|
||||
Triple("IllustratedInterracial.com Comics", "album/IllustratedInterracial_com-Comics", AUTHOR),
|
||||
Triple("Giantess Club Comics", "album/Giantess-Club-Comics", AUTHOR),
|
||||
Triple("Innocent Dickgirls Comics", "album/Innocent-Dickgirls-Comics", AUTHOR),
|
||||
Triple("Locofuria Comics", "album/Locofuria-Comics", AUTHOR),
|
||||
Triple("PigKing - CrazyDad Comics", "album/PigKing-CrazyDad-Comics", AUTHOR),
|
||||
Triple("Cartoon Reality Comics", "album/Cartoon-Reality-Comics", AUTHOR),
|
||||
Triple("Affect3D Comics", "album/Affect3D-Comics", AUTHOR),
|
||||
Triple("TG Comics", "album/TG-Comics", AUTHOR),
|
||||
Triple("Melkormancin.com Comics", "album/Melkormancin_com-Comics", AUTHOR),
|
||||
Triple("Seiren.com.br Comics", "album/Seiren_com_br-Comics", AUTHOR),
|
||||
Triple("Tracy Scops Comics", "album/Tracy-Scops-Comics", AUTHOR),
|
||||
Triple("Fred Perry Comics", "album/Fred-Perry-Comics", AUTHOR),
|
||||
Triple("Witchking00 Comics", "album/Witchking00-Comics", AUTHOR),
|
||||
Triple("8muses Comics", "album/8muses-Comics", AUTHOR),
|
||||
Triple("KAOS Comics", "album/KAOS-Comics", AUTHOR),
|
||||
Triple("Vaesark Comics", "album/Vaesark-Comics", AUTHOR),
|
||||
Triple("Fansadox Comics", "album/Fansadox-Comics", AUTHOR),
|
||||
Triple("DreamTales Comics", "album/DreamTales-Comics", AUTHOR),
|
||||
Triple("Croc Comics", "album/Croc-Comics", AUTHOR),
|
||||
Triple("Jay Marvel Comics", "album/Jay-Marvel-Comics", AUTHOR),
|
||||
Triple("JohnPersons.com Comics", "album/JohnPersons_com-Comics", AUTHOR),
|
||||
Triple("MuscleFan Comics", "album/MuscleFan-Comics", AUTHOR),
|
||||
Triple("Taboolicious.xxx Comics", "album/Taboolicious_xxx-Comics", AUTHOR),
|
||||
Triple("MongoBongo Comics", "album/MongoBongo-Comics", AUTHOR),
|
||||
Triple("Slipshine Comics", "album/Slipshine-Comics", AUTHOR),
|
||||
Triple("Everfire Comics", "album/Everfire-Comics", AUTHOR),
|
||||
Triple("PrismGirls Comics", "album/PrismGirls-Comics", AUTHOR),
|
||||
Triple("Abimboleb Comics", "album/Abimboleb-Comics", AUTHOR),
|
||||
Triple("Y3DF - Your3DFantasy.com Comics", "album/Y3DF-Your3DFantasy_com-Comics", AUTHOR),
|
||||
Triple("Grow Comics", "album/Grow-Comics", AUTHOR),
|
||||
Triple("OkayOkayOKOk Comics", "album/OkayOkayOKOk-Comics", AUTHOR),
|
||||
Triple("Tufos Comics", "album/Tufos-Comics", AUTHOR),
|
||||
Triple("Cartoon Valley", "album/Cartoon-Valley", AUTHOR),
|
||||
Triple("3DMonsterStories.com Comics", "album/3DMonsterStories_com-Comics", AUTHOR),
|
||||
Triple("Kogeikun Comics", "album/Kogeikun-Comics", AUTHOR),
|
||||
Triple("The Foxxx Comics", "album/The-Foxxx-Comics", AUTHOR),
|
||||
Triple("Theme Collections", "album/Theme-Collections", AUTHOR),
|
||||
Triple("Interracial-Comics", "album/Interracial-Comics", AUTHOR),
|
||||
Triple("Expansion Comics", "album/Expansion-Comics", AUTHOR),
|
||||
Triple("Moiarte Comics", "album/Moiarte-Comics", AUTHOR),
|
||||
Triple("Incognitymous Comics", "album/Incognitymous-Comics", AUTHOR),
|
||||
Triple("DizzyDills Comics", "album/DizzyDills-Comics", AUTHOR),
|
||||
Triple("DukesHardcoreHoneys.com Comics", "album/DukesHardcoreHoneys_com-Comics", AUTHOR),
|
||||
Triple("Stormfeder Comics", "album/Stormfeder-Comics", AUTHOR),
|
||||
Triple("Bimbo Story Club Comics", "album/Bimbo-Story-Club-Comics", AUTHOR),
|
||||
Triple("Smudge Comics", "album/Smudge-Comics", AUTHOR),
|
||||
Triple("Dollproject Comics", "album/Dollproject-Comics", AUTHOR),
|
||||
Triple("SuperHeroineComixxx", "album/SuperHeroineComixxx", AUTHOR),
|
||||
Triple("Karmagik Comics", "album/Karmagik-Comics", AUTHOR),
|
||||
Triple("Blacknwhite.com Comics", "album/Blacknwhite_com-Comics", AUTHOR),
|
||||
Triple("ArtOfJaguar Comics", "album/ArtOfJaguar-Comics", AUTHOR),
|
||||
Triple("Kirtu.com Comics", "album/Kirtu_com-Comics", AUTHOR),
|
||||
Triple("UberMonkey Comics", "album/UberMonkey-Comics", AUTHOR),
|
||||
Triple("DarkSoul3D Comics", "album/DarkSoul3D-Comics", AUTHOR),
|
||||
Triple("Markydaysaid Comics", "album/Markydaysaid-Comics", AUTHOR),
|
||||
Triple("Central Comics", "album/Central-Comics", AUTHOR),
|
||||
Triple("Frozen Parody Comics", "album/Frozen-Parody-Comics", AUTHOR),
|
||||
Triple("Blacknwhitecomics.com Comix", "album/Blacknwhitecomics_com-Comix", AUTHOR)
|
||||
)
|
||||
|
||||
protected class SortFilter(private val vals: Array<Pair<String, String>>) : Filter.Select<String>("Sort Order", vals.map { it.first }.toTypedArray()) {
|
||||
fun toQueryValue() = vals[state].second
|
||||
}
|
||||
protected open fun getSortList() = arrayOf(
|
||||
Pair("Views", ""),
|
||||
Pair("Likes", "like"),
|
||||
Pair("Date", "date"),
|
||||
Pair("A-Z", "az")
|
||||
)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.eromuse
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class EroMuseGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "eromuse"
|
||||
|
||||
override val themeClass = "EroMuse"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("8Muses", "https://comics.8muses.com", "en", className = "EightMuses", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Erofus", "https://www.erofus.com", "en", isNsfw = true, overrideVersionCode = 1)
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
EroMuseGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,473 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.fmreader
|
||||
|
||||
import android.util.Base64
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Calendar
|
||||
|
||||
/**
|
||||
* For sites based on the Flat-Manga CMS
|
||||
*/
|
||||
abstract class FMReader(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64) Gecko/20100101 Firefox/77.0")
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
|
||||
protected fun Elements.imgAttr(): String? = getImgAttr(this.firstOrNull())
|
||||
|
||||
private fun Element.imgAttr(): String? = getImgAttr(this)
|
||||
|
||||
open fun getImgAttr(element: Element?): String? {
|
||||
return when {
|
||||
element == null -> null
|
||||
element.hasAttr("data-original") -> element.attr("abs:data-original")
|
||||
element.hasAttr("data-src") -> element.attr("abs:data-src")
|
||||
element.hasAttr("data-bg") -> element.attr("abs:data-bg")
|
||||
else -> element.attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
open val requestPath = "manga-list.html"
|
||||
|
||||
open val popularSort = "sort=views"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
GET("$baseUrl/$requestPath?listType=pagination&page=$page&$popularSort&sort_type=DESC", headers)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/$requestPath?")!!.newBuilder()
|
||||
.addQueryParameter("name", query)
|
||||
.addQueryParameter("page", page.toString())
|
||||
(if (filters.isEmpty()) getFilterList() else filters).forEach { filter ->
|
||||
when (filter) {
|
||||
is Status -> {
|
||||
val status = arrayOf("", "1", "2")[filter.state]
|
||||
url.addQueryParameter("m_status", status)
|
||||
}
|
||||
is TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is GenreList -> {
|
||||
var genre = String()
|
||||
var ungenre = String()
|
||||
filter.state.forEach {
|
||||
if (it.isIncluded()) genre += ",${it.name}"
|
||||
if (it.isExcluded()) ungenre += ",${it.name}"
|
||||
}
|
||||
url.addQueryParameter("genre", genre)
|
||||
url.addQueryParameter("ungenre", ungenre)
|
||||
}
|
||||
is SortBy -> {
|
||||
url.addQueryParameter(
|
||||
"sort",
|
||||
when (filter.state?.index) {
|
||||
0 -> "name"
|
||||
1 -> "views"
|
||||
else -> "last_update"
|
||||
}
|
||||
)
|
||||
if (filter.state?.ascending == true)
|
||||
url.addQueryParameter("sort_type", "ASC")
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
GET("$baseUrl/$requestPath?listType=pagination&page=$page&sort=last_update&sort_type=DESC", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { popularMangaFromElement(it) }
|
||||
|
||||
// check if there's a next page
|
||||
val hasNextPage = (document.select(popularMangaNextPageSelector())?.first()?.text() ?: "").let {
|
||||
if (it.contains(Regex("""\w*\s\d*\s\w*\s\d*"""))) {
|
||||
it.split(" ").let { pageOf -> pageOf[1] != pageOf[3] } // current page not last page
|
||||
} else {
|
||||
it.isNotEmpty() // standard next page check
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
override fun popularMangaSelector() = "div.media, .thumb-item-flow"
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
open val headerSelector = "h3 a, .series-title a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select("$headerSelector").let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img, .thumb-wrapper .img-in-ratio").imgAttr()
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
/**
|
||||
* can select one of 2 different types of elements
|
||||
* one is an element with text "page x of y", must be the first element if it's part of a collection
|
||||
* the other choice is the standard "next page" element (but most FMReader sources don't have this one)
|
||||
*/
|
||||
override fun popularMangaNextPageSelector() = "div.col-lg-9 button.btn-info, .pagination a:contains(»):not(.disabled)"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.row").first()
|
||||
|
||||
return SManga.create().apply {
|
||||
infoElement.select("li a.btn-info").text().let {
|
||||
if (it.contains("Updating", true).not()) author = it
|
||||
}
|
||||
genre = infoElement.select("li a.btn-danger").joinToString { it.text() }
|
||||
status = parseStatus(infoElement.select("li a.btn-success").first()?.text())
|
||||
description = document.select("div.detail .content, div.row ~ div.row:has(h3:first-child) p, .summary-content p").text().trim()
|
||||
thumbnail_url = infoElement.select("img.thumbnail").imgAttr()
|
||||
|
||||
// add alternative name to manga description
|
||||
infoElement.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not() && it.contains("Updating", true).not()) {
|
||||
description += when {
|
||||
description!!.isEmpty() -> altName + it
|
||||
else -> "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val altNameSelector = "li:contains(Other names)"
|
||||
open val altName = "Alternative Name" // the alt name already contains ": " eg. ": alt name1, alt name2"
|
||||
|
||||
// languages: en, vi, tr
|
||||
fun parseStatus(status: String?): Int {
|
||||
val completedWords = setOf("completed", "complete", "incomplete", "đã hoàn thành", "tamamlandı", "hoàn thành")
|
||||
val ongoingWords = setOf("ongoing", "on going", "updating", "chưa hoàn thành", "đang cập nhật", "devam ediyor", "Đang tiến hành")
|
||||
return when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
completedWords.any { it.equals(status, ignoreCase = true) } -> SManga.COMPLETED
|
||||
ongoingWords.any { it.equals(status, ignoreCase = true) } -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val mangaTitle = document.select(".manga-info h1, .manga-info h3").text()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, mangaTitle) }.distinctBy { it.url }
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return chapterFromElement(element, "")
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#list-chapters p, table.table tr, .list-chapters > a"
|
||||
|
||||
open val chapterUrlSelector = "a"
|
||||
|
||||
open val chapterTimeSelector = "time, .chapter-time"
|
||||
|
||||
open val chapterNameAttrSelector = "title"
|
||||
|
||||
open fun chapterFromElement(element: Element, mangaTitle: String = ""): SChapter {
|
||||
return SChapter.create().apply {
|
||||
if (chapterUrlSelector != "") {
|
||||
element.select(chapterUrlSelector).first().let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
name = it.text().substringAfter("$mangaTitle ")
|
||||
}
|
||||
} else {
|
||||
element.let {
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
name = element.attr(chapterNameAttrSelector).substringAfter("$mangaTitle ")
|
||||
}
|
||||
}
|
||||
date_upload = element.select(chapterTimeSelector).let { if (it.hasText()) parseChapterDate(it.text()) else 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// gets the number from "1 day ago"
|
||||
open val dateValueIndex = 0
|
||||
|
||||
// gets the unit of time (day, week hour) from "1 day ago"
|
||||
open val dateWordIndex = 1
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val value = date.split(' ')[dateValueIndex].toInt()
|
||||
val dateWord = date.split(' ')[dateWordIndex].let {
|
||||
if (it.contains("(")) {
|
||||
it.substringBefore("(")
|
||||
} else {
|
||||
it.substringBefore("s")
|
||||
}
|
||||
}
|
||||
|
||||
// languages: en, vi, es, tr
|
||||
return when (dateWord) {
|
||||
"min", "minute", "phút", "minuto", "dakika" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"hour", "giờ", "hora", "saat" -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"day", "ngày", "día", "gün" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"week", "tuần", "semana", "hafta" -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"month", "tháng", "mes", "ay" -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
"year", "năm", "año", "yıl" -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val pageListImageSelector = "img.chapter-img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListImageSelector).mapIndexed { i, img ->
|
||||
Page(i, document.location(), img.imgAttr())
|
||||
}
|
||||
}
|
||||
|
||||
protected fun base64PageListParse(document: Document): List<Page> {
|
||||
fun Element.decoded(): String {
|
||||
val attr =
|
||||
when {
|
||||
this.hasAttr("data-original") -> "data-original"
|
||||
this.hasAttr("data-src") -> "data-src"
|
||||
this.hasAttr("data-srcset") -> "data-srcset"
|
||||
this.hasAttr("data-aload") -> "data-aload"
|
||||
else -> "src"
|
||||
}
|
||||
return if (!this.attr(attr).contains(".")) {
|
||||
Base64.decode(this.attr(attr), Base64.DEFAULT).toString(Charset.defaultCharset())
|
||||
} else {
|
||||
this.attr("abs:$attr")
|
||||
}
|
||||
}
|
||||
|
||||
return document.select(pageListImageSelector).mapIndexed { i, img ->
|
||||
Page(i, document.location(), img.decoded())
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
private class Status : Filter.Select<String>("Status", arrayOf("Any", "Completed", "Ongoing"))
|
||||
class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||
class Genre(name: String, val id: String = name.replace(' ', '+')) : Filter.TriState(name)
|
||||
private class SortBy : Filter.Sort("Sorted By", arrayOf("A-Z", "Most vỉews", "Last updated"), Selection(1, false))
|
||||
|
||||
// TODO: Country (leftover from original LHTranslation)
|
||||
override fun getFilterList() = FilterList(
|
||||
TextField("Author", "author"),
|
||||
TextField("Group", "group"),
|
||||
Status(),
|
||||
SortBy(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll("div.panel-body a")].map((el,i) => `Genre("${el.innerText.trim()}")`).join(',\n')
|
||||
// on https://lhtranslation.net/search
|
||||
open fun getGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("18+"),
|
||||
Genre("Adult"),
|
||||
Genre("Anime"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Live action"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Art"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("One shot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shojou Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of Life"),
|
||||
Genre("Smut"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Adventure"),
|
||||
Genre("Yaoi")
|
||||
)
|
||||
|
||||
// from manhwa18.com/search, removed a few that didn't return results/wouldn't be terribly useful
|
||||
fun getAdultGenreList() = listOf(
|
||||
Genre("18"),
|
||||
Genre("Action"),
|
||||
Genre("Adult"),
|
||||
Genre("Adventure"),
|
||||
Genre("Anime"),
|
||||
Genre("Comedy"),
|
||||
Genre("Comic"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Drama"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Gender Bender"),
|
||||
Genre("Harem"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Josei"),
|
||||
Genre("Live action"),
|
||||
Genre("Magic"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Martial Arts"),
|
||||
Genre("Mature"),
|
||||
Genre("Mecha"),
|
||||
Genre("Mystery"),
|
||||
Genre("Oneshot"),
|
||||
Genre("Psychological"),
|
||||
Genre("Romance"),
|
||||
Genre("School Life"),
|
||||
Genre("Sci-fi"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen Ai"),
|
||||
Genre("Slice of life"),
|
||||
Genre("Smut"),
|
||||
Genre("Soft Yaoi"),
|
||||
Genre("Soft Yuri"),
|
||||
Genre("Sports"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Tragedy"),
|
||||
Genre("VnComic"),
|
||||
Genre("Webtoon")
|
||||
)
|
||||
|
||||
// taken from readcomiconline.org/search
|
||||
fun getComicsGenreList() = listOf(
|
||||
Genre("Action"),
|
||||
Genre("Adventure"),
|
||||
Genre("Anthology"),
|
||||
Genre("Anthropomorphic"),
|
||||
Genre("Biography"),
|
||||
Genre("Children"),
|
||||
Genre("Comedy"),
|
||||
Genre("Crime"),
|
||||
Genre("Drama"),
|
||||
Genre("Family"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Fighting"),
|
||||
Genre("GraphicNovels"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("LeadingLadies"),
|
||||
Genre("LGBTQ"),
|
||||
Genre("Literature"),
|
||||
Genre("Manga"),
|
||||
Genre("MartialArts"),
|
||||
Genre("Mature"),
|
||||
Genre("Military"),
|
||||
Genre("Mystery"),
|
||||
Genre("Mythology"),
|
||||
Genre("Personal"),
|
||||
Genre("Political"),
|
||||
Genre("Post-Apocalyptic"),
|
||||
Genre("Psychological"),
|
||||
Genre("Pulp"),
|
||||
Genre("Religious"),
|
||||
Genre("Robots"),
|
||||
Genre("Romance"),
|
||||
Genre("Schoollife"),
|
||||
Genre("Sci-Fi"),
|
||||
Genre("Sliceoflife"),
|
||||
Genre("Sport"),
|
||||
Genre("Spy"),
|
||||
Genre("Superhero"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Suspense"),
|
||||
Genre("Thriller"),
|
||||
Genre("Vampires"),
|
||||
Genre("VideoGames"),
|
||||
Genre("War"),
|
||||
Genre("Western"),
|
||||
Genre("Zombies")
|
||||
)
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.fmreader
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class FMReaderGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "fmreader"
|
||||
|
||||
override val themeClass = "FMReader"
|
||||
|
||||
override val baseVersionCode: Int = 2
|
||||
|
||||
/** For future sources: when testing and popularMangaRequest() returns a Jsoup error instead of results
|
||||
* most likely the fix is to override popularMangaNextPageSelector() */
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Epik Manga", "https://www.epikmanga.com", "tr"),
|
||||
SingleLang("HeroScan", "https://heroscan.com", "en"),
|
||||
SingleLang("KissLove", "https://kissaway.net", "ja"),
|
||||
SingleLang("LHTranslation", "https://lhtranslation.net", "en", overrideVersionCode = 1),
|
||||
SingleLang("Manga-TR", "https://manga-tr.com", "tr", className = "MangaTR"),
|
||||
SingleLang("ManhuaScan", "https://manhuascan.com", "en", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Manhwa18", "https://manhwa18.com", "en", isNsfw = true),
|
||||
MultiLang("Manhwa18.net", "https://manhwa18.net", listOf("en", "ko"), className = "Manhwa18NetFactory", isNsfw = true),
|
||||
SingleLang("ManhwaSmut", "https://manhwasmut.com", "en", isNsfw = true, overrideVersionCode = 2),
|
||||
SingleLang("RawLH", "https://lovehug.net", "ja"),
|
||||
SingleLang("Say Truyen", "https://saytruyen.com", "vi"),
|
||||
SingleLang("KSGroupScans", "https://ksgroupscans.com", "en"),
|
||||
// Sites that went down
|
||||
//SingleLang("18LHPlus", "https://18lhplus.com", "en", className = "EighteenLHPlus"),
|
||||
//SingleLang("HanaScan (RawQQ)", "https://hanascan.com", "ja", className = "HanaScanRawQQ"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
FMReaderGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,301 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.foolslide
|
||||
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.source.online.ParsedHttpSource
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.HashSet
|
||||
import java.util.Locale
|
||||
|
||||
abstract class FoolSlide(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
val urlModifier: String = ""
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
protected open val dedupeLatestUpdates = true
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override fun popularMangaSelector() = "div.group"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl$urlModifier/directory/$page/", headers)
|
||||
}
|
||||
|
||||
val latestUpdatesUrls = HashSet<String>()
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val mp = super.latestUpdatesParse(response)
|
||||
return if (dedupeLatestUpdates) {
|
||||
val mangas = mp.mangas.distinctBy { it.url }.filterNot { latestUpdatesUrls.contains(it.url) }
|
||||
latestUpdatesUrls.addAll(mangas.map { it.url })
|
||||
MangasPage(mangas, mp.hasNextPage)
|
||||
} else mp
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = "div.group"
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
if (page == 1) {
|
||||
latestUpdatesUrls.clear()
|
||||
}
|
||||
return GET("$baseUrl$urlModifier/latest/$page/")
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
element.select("a[title]").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
|
||||
element.select("img").first()?.let {
|
||||
manga.thumbnail_url = it.absUrl("src").replace("/thumb_", "/")
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a[title]").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.next"
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = "div.next"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val searchHeaders = headersBuilder().add("Content-Type", "application/x-www-form-urlencoded").build()
|
||||
|
||||
val form = FormBody.Builder()
|
||||
.add("search", query)
|
||||
|
||||
return POST("$baseUrl$urlModifier/search/", searchHeaders, form.build())
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.group"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a[title]").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a:has(span.next)"
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = allowAdult(super.mangaDetailsRequest(manga))
|
||||
|
||||
open val mangaDetailsInfoSelector = "div.info"
|
||||
|
||||
// if there's no image on the details page, get the first page of the first chapter
|
||||
fun getDetailsThumbnail(document: Document, urlSelector: String = chapterUrlSelector): String? {
|
||||
return document.select("div.thumbnail img, table.thumb img").firstOrNull()?.attr("abs:src")
|
||||
?: document.select(chapterListSelector()).last().select(urlSelector).attr("abs:href")
|
||||
.let { url -> client.newCall(allowAdult(GET(url, headers))).execute() }
|
||||
.let { response -> pageListParse(response).first().imageUrl }
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select(mangaDetailsInfoSelector).firstOrNull()?.html()?.let { infoHtml ->
|
||||
author = Regex("""(?i)(Author|Autore)</b>:\s?([^\n<]*)[\n<]""").find(infoHtml)?.groupValues?.get(2)
|
||||
artist = Regex("""Artist</b>:\s?([^\n<]*)[\n<]""").find(infoHtml)?.groupValues?.get(1)
|
||||
description = Regex("""(?i)(Synopsis|Description|Trama)</b>:\s?([^\n<]*)[\n<]""").find(infoHtml)?.groupValues?.get(2)
|
||||
}
|
||||
thumbnail_url = getDetailsThumbnail(document)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a GET request into a POST request that automatically authorizes all adult content
|
||||
*/
|
||||
private fun allowAdult(request: Request) = allowAdult(request.url().toString())
|
||||
|
||||
private fun allowAdult(url: String): Request {
|
||||
return POST(
|
||||
url,
|
||||
body = FormBody.Builder()
|
||||
.add("adult", "true")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = allowAdult(super.chapterListRequest(manga))
|
||||
|
||||
override fun chapterListSelector() = "div.group div.element, div.list div.element"
|
||||
|
||||
open val chapterDateSelector = "div.meta_r"
|
||||
|
||||
open val chapterUrlSelector = "a[title]"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select(chapterUrlSelector).first()
|
||||
val dateElement = element.select(chapterDateSelector).first()
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
chapter.date_upload = dateElement.text()?.let { parseChapterDate(it.substringAfter(", ")) }
|
||||
?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
open fun parseChapterDate(date: String): Long? {
|
||||
val lcDate = date.toLowerCase()
|
||||
if (lcDate.endsWith(" ago"))
|
||||
parseRelativeDate(lcDate)?.let { return it }
|
||||
|
||||
// Handle 'yesterday' and 'today', using midnight
|
||||
var relativeDate: Calendar? = null
|
||||
// Result parsed but no year, copy current year over
|
||||
when {
|
||||
lcDate.startsWith("yesterday") -> {
|
||||
relativeDate = Calendar.getInstance()
|
||||
relativeDate.add(Calendar.DAY_OF_MONTH, -1) // yesterday
|
||||
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
|
||||
relativeDate.set(Calendar.MINUTE, 0)
|
||||
relativeDate.set(Calendar.SECOND, 0)
|
||||
relativeDate.set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
lcDate.startsWith("today") -> {
|
||||
relativeDate = Calendar.getInstance()
|
||||
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
|
||||
relativeDate.set(Calendar.MINUTE, 0)
|
||||
relativeDate.set(Calendar.SECOND, 0)
|
||||
relativeDate.set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
lcDate.startsWith("tomorrow") -> {
|
||||
relativeDate = Calendar.getInstance()
|
||||
relativeDate.add(Calendar.DAY_OF_MONTH, +1) // tomorrow
|
||||
relativeDate.set(Calendar.HOUR_OF_DAY, 0)
|
||||
relativeDate.set(Calendar.MINUTE, 0)
|
||||
relativeDate.set(Calendar.SECOND, 0)
|
||||
relativeDate.set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
}
|
||||
|
||||
relativeDate?.timeInMillis?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
var result = DATE_FORMAT_1.parseOrNull(date)
|
||||
|
||||
for (dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES) {
|
||||
if (result == null)
|
||||
result = dateFormat.parseOrNull(date)
|
||||
else
|
||||
break
|
||||
}
|
||||
|
||||
for (dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES_NO_YEAR) {
|
||||
if (result == null) {
|
||||
result = dateFormat.parseOrNull(date)
|
||||
|
||||
if (result != null) {
|
||||
// Result parsed but no year, copy current year over
|
||||
result = Calendar.getInstance().apply {
|
||||
time = result!!
|
||||
set(Calendar.YEAR, Calendar.getInstance().get(Calendar.YEAR))
|
||||
}.time
|
||||
}
|
||||
} else break
|
||||
}
|
||||
|
||||
return result?.time ?: 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses dates in this form:
|
||||
* `11 days ago`
|
||||
*/
|
||||
private fun parseRelativeDate(date: String): Long? {
|
||||
val trimmedDate = date.split(" ")
|
||||
|
||||
if (trimmedDate[2] != "ago") return null
|
||||
|
||||
val number = trimmedDate[0].toIntOrNull() ?: return null
|
||||
val unit = trimmedDate[1].removeSuffix("s") // Remove 's' suffix
|
||||
|
||||
val now = Calendar.getInstance()
|
||||
|
||||
// Map English unit to Java unit
|
||||
val javaUnit = when (unit) {
|
||||
"year", "yr" -> Calendar.YEAR
|
||||
"month" -> Calendar.MONTH
|
||||
"week", "wk" -> Calendar.WEEK_OF_MONTH
|
||||
"day" -> Calendar.DAY_OF_MONTH
|
||||
"hour", "hr" -> Calendar.HOUR
|
||||
"minute", "min" -> Calendar.MINUTE
|
||||
"second", "sec" -> Calendar.SECOND
|
||||
else -> return null
|
||||
}
|
||||
|
||||
now.add(javaUnit, -number)
|
||||
|
||||
return now.timeInMillis
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.parseOrNull(string: String): Date? {
|
||||
return try {
|
||||
parse(string)
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) = allowAdult(super.pageListRequest(chapter))
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val doc = document.toString()
|
||||
val jsonstr = doc.substringAfter("var pages = ").substringBefore(";")
|
||||
val json = JsonParser().parse(jsonstr).asJsonArray
|
||||
val pages = mutableListOf<Page>()
|
||||
json.forEach {
|
||||
// Create dummy element to resolve relative URL
|
||||
val absUrl = document.createElement("a")
|
||||
.attr("href", it["url"].asString)
|
||||
.absUrl("href")
|
||||
|
||||
pages.add(Page(pages.size, "", absUrl))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
|
||||
|
||||
companion object {
|
||||
private val ORDINAL_SUFFIXES = listOf("st", "nd", "rd", "th")
|
||||
private val DATE_FORMAT_1 = SimpleDateFormat("yyyy.MM.dd", Locale.US)
|
||||
private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES = ORDINAL_SUFFIXES.map {
|
||||
SimpleDateFormat("dd'$it' MMMM, yyyy", Locale.US)
|
||||
}
|
||||
private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES_NO_YEAR = ORDINAL_SUFFIXES.map {
|
||||
SimpleDateFormat("dd'$it' MMMM", Locale.US)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.foolslide
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class FoolSlideGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "foolslide"
|
||||
|
||||
override val themeClass = "FoolSlide"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("The Cat Scans", "https://reader2.thecatscans.com/", "en"),
|
||||
SingleLang("Silent Sky", "https://reader.silentsky-scans.net", "en"),
|
||||
SingleLang("Death Toll Scans", "https://reader.deathtollscans.net", "en"),
|
||||
SingleLang("MangaScouts", "http://onlinereader.mangascouts.org", "de"),
|
||||
SingleLang("Lilyreader", "https://manga.smuglo.li", "en"),
|
||||
SingleLang("Evil Flowers", "https://reader.evilflowers.com", "en"),
|
||||
SingleLang("Русификация", "https://rusmanga.ru", "ru", className = "Russification"),
|
||||
SingleLang("PowerManga", "https://reader.powermanga.org", "it", className = "PowerMangaIT"),
|
||||
MultiLang("FoolSlide Customizable", "", listOf("other")),
|
||||
SingleLang("Menudo-Fansub", "https://www.menudo-fansub.com", "es", className = "MenudoFansub", overrideVersionCode = 1),
|
||||
SingleLang("Sense-Scans", "https://sensescans.com", "en", className = "SenseScans", overrideVersionCode = 1),
|
||||
SingleLang("Kirei Cake", "https://reader.kireicake.com", "en"),
|
||||
SingleLang("Mangatellers", "http://www.mangatellers.gr", "en"),
|
||||
SingleLang("Iskultrip Scans", "https://maryfaye.net", "en"),
|
||||
SingleLang("Anata no Motokare", "https://motokare.xyz", "en", className = "AnataNoMotokare"),
|
||||
SingleLang("Yuri-ism", "https://www.yuri-ism.net", "en", className = "YuriIsm"),
|
||||
SingleLang("Ajia no Scantrad", "https://www.ajianoscantrad.fr", "fr", className = "AjiaNoScantrad"),
|
||||
SingleLang("Storm in Heaven", "https://www.storm-in-heaven.net", "it", className = "StormInHeaven"),
|
||||
SingleLang("LupiTeam", "https://lupiteam.net", "it"),
|
||||
SingleLang("Zandy no Fansub", "https://zandynofansub.aishiteru.org", "en"),
|
||||
SingleLang("Kirishima Fansub", "https://www.kirishimafansub.net", "es"),
|
||||
SingleLang("Baixar Hentai", "https://leitura.baixarhentai.net", "pt-BR", isNsfw = true, overrideVersionCode = 1),
|
||||
MultiLang("HNI-Scantrad", "https://hni-scantrad.com", listOf("fr", "en"), className = "HNIScantradFactory", pkgName = "hniscantrad", overrideVersionCode = 1),
|
||||
SingleLang("The Phoenix Scans", "https://www.phoenixscans.com", "it", className = "PhoenixScans"),
|
||||
SingleLang("GTO The Great Site", "https://www.gtothegreatsite.net", "it", className = "GTO"),
|
||||
SingleLang("Fall World Reader", "https://faworeader.altervista.org", "it", className = "FallenWorldOrder"),
|
||||
SingleLang("NIFTeam", "http://read-nifteam.info", "it"),
|
||||
SingleLang("TuttoAnimeManga", "https://tuttoanimemanga.net", "it"),
|
||||
SingleLang("Tortuga Ceviri", "http://tortuga-ceviri.com", "tr"),
|
||||
SingleLang("Rama", "https://www.ramareader.it", "it"),
|
||||
SingleLang("Mabushimajo", "http://mabushimajo.com", "tr"),
|
||||
SingleLang("Hyakuro", "https://hyakuro.com/reader", "en"),
|
||||
SingleLang("Le Cercle du Scan", "https://lel.lecercleduscan.com", "fr")
|
||||
//Sites that are down
|
||||
//SingleLang("One Time Scans", "https://reader.otscans.com", "en"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
FoolSlideGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.genkan
|
||||
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
|
||||
open class Genkan(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun popularMangaSelector() = "div.list-item"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/comics?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
// Track which manga titles have been added to latestUpdates's MangasPage
|
||||
private val latestUpdatesTitles = mutableSetOf<String>()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
if (page == 1) latestUpdatesTitles.clear()
|
||||
return GET("$baseUrl/latest?page=$page", headers)
|
||||
}
|
||||
|
||||
// To prevent dupes, only add manga to MangasPage if its title is not one we've added already
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val latestManga = mutableListOf<SManga>()
|
||||
val document = response.asJsoup()
|
||||
|
||||
document.select(latestUpdatesSelector()).forEach { element ->
|
||||
latestUpdatesFromElement(element).let { manga ->
|
||||
if (manga.title !in latestUpdatesTitles) {
|
||||
latestManga.add(manga)
|
||||
latestUpdatesTitles.add(manga.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(latestManga, document.select(latestUpdatesNextPageSelector()).hasText())
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.list-title").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.text()
|
||||
}
|
||||
manga.thumbnail_url = styleToUrl(element.select("a.media-content").first())
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "[rel=next]"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return GET("$baseUrl/comics?query=$query", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
private fun styleToUrl(element: Element): String {
|
||||
return element.attr("style").substringAfter("(").substringBefore(")")
|
||||
.let { if (it.startsWith("http")) it else baseUrl + it }
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
title = document.select("div#content h5").first().text()
|
||||
description = document.select("div.col-lg-9").text().substringAfter("Description ").substringBefore(" Volume")
|
||||
thumbnail_url = styleToUrl(document.select("div.media a").first())
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.col-lg-9 div.flex"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
|
||||
val urlElement = element.select("a.item-author")
|
||||
val chapNum = urlElement.attr("href").split("/").last()
|
||||
|
||||
setUrlWithoutDomain(urlElement.attr("href"))
|
||||
name = if (urlElement.text().contains("Chapter $chapNum")) {
|
||||
urlElement.text()
|
||||
} else {
|
||||
"Ch. $chapNum: ${urlElement.text()}"
|
||||
}
|
||||
date_upload = parseChapterDate(element.select("a.item-company").first().text()) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val dateFormat by lazy {
|
||||
SimpleDateFormat("MMM d, yyyy", Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
// If the date string contains the word "ago" send it off for relative date parsing otherwise use dateFormat
|
||||
private fun parseChapterDate(string: String): Long? {
|
||||
return if ("ago" in string) {
|
||||
parseRelativeDate(string) ?: 0
|
||||
} else {
|
||||
dateFormat.parse(string)?.time ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract relative date (e.g. posted 3 days ago)
|
||||
private fun parseRelativeDate(date: String): Long? {
|
||||
val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ")
|
||||
|
||||
val calendar = Calendar.getInstance()
|
||||
when (trimmedDate[1]) {
|
||||
"year" -> calendar.apply { add(Calendar.YEAR, -trimmedDate[0].toInt()) }
|
||||
"month" -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
|
||||
"week" -> calendar.apply { add(Calendar.WEEK_OF_MONTH, -trimmedDate[0].toInt()) }
|
||||
"day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
|
||||
"hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
|
||||
"minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
|
||||
"second" -> calendar.apply { add(Calendar.SECOND, 0) }
|
||||
}
|
||||
|
||||
return calendar.timeInMillis
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
val allImages = document.select("div#pages-container + script").first().data()
|
||||
.substringAfter("[").substringBefore("];")
|
||||
.replace(Regex("""["\\]"""), "")
|
||||
.split(",")
|
||||
|
||||
for (i in allImages.indices) {
|
||||
pages.add(Page(i, "", allImages[i]))
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return if (page.imageUrl!!.startsWith("http")) GET(page.imageUrl!!, headers) else GET(baseUrl + page.imageUrl!!, headers)
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.genkan
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class GenkanGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "genkan"
|
||||
|
||||
override val themeClass = "Genkan"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Hunlight Scans", "https://hunlight-scans.info", "en"),
|
||||
SingleLang("ZeroScans", "https://zeroscans.com", "en"),
|
||||
SingleLang("The Nonames Scans", "https://the-nonames.com", "en"),
|
||||
SingleLang("Edelgarde Scans", "https://edelgardescans.com", "en"),
|
||||
SingleLang("Method Scans", "https://methodscans.com", "en"),
|
||||
SingleLang("LynxScans", "https://lynxscans.com", "en", overrideVersionCode = 1),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
GenkanGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.genkan
|
||||
|
||||
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.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
|
||||
/**
|
||||
* For sites using the older Genkan CMS that didn't have a search function
|
||||
*/
|
||||
open class GenkanOriginal(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : Genkan(name, baseUrl, lang) {
|
||||
|
||||
private var searchQuery = ""
|
||||
private var searchPage = 1
|
||||
private var nextPageSelectorElement = Elements()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (page == 1) searchPage = 1
|
||||
searchQuery = query
|
||||
return popularMangaRequest(page)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val searchMatches = mutableListOf<SManga>()
|
||||
val document = response.asJsoup()
|
||||
searchMatches.addAll(getMatchesFrom(document))
|
||||
|
||||
/* call another function if there's more pages to search
|
||||
not doing it this way can lead to a false "no results found"
|
||||
if no matches are found on the first page but there are matches
|
||||
on subsequent pages */
|
||||
nextPageSelectorElement = document.select(searchMangaNextPageSelector())
|
||||
while (nextPageSelectorElement.hasText()) {
|
||||
searchMatches.addAll(searchMorePages())
|
||||
}
|
||||
|
||||
return MangasPage(searchMatches, false)
|
||||
}
|
||||
|
||||
// search the given document for matches
|
||||
private fun getMatchesFrom(document: Document): MutableList<SManga> {
|
||||
val searchMatches = mutableListOf<SManga>()
|
||||
document.select(searchMangaSelector())
|
||||
.filter { it.text().contains(searchQuery, ignoreCase = true) }
|
||||
.map { searchMatches.add(searchMangaFromElement(it)) }
|
||||
|
||||
return searchMatches
|
||||
}
|
||||
|
||||
// search additional pages if called
|
||||
private fun searchMorePages(): MutableList<SManga> {
|
||||
searchPage++
|
||||
val nextPage = client.newCall(popularMangaRequest(searchPage)).execute().asJsoup()
|
||||
val searchMatches = mutableListOf<SManga>()
|
||||
searchMatches.addAll(getMatchesFrom(nextPage))
|
||||
nextPageSelectorElement = nextPage.select(searchMangaNextPageSelector())
|
||||
|
||||
return searchMatches
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.genkan
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class GenkanOriginalGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "genkan"
|
||||
|
||||
override val themeClass = "GenkanOriginal"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Reaper Scans", "https://reaperscans.com", "en"),
|
||||
SingleLang("Hatigarm Scans", "https://hatigarmscanz.net", "en", overrideVersionCode = 1),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
GenkanOriginalGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,608 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.luscious
|
||||
|
||||
import com.github.salomonbrys.kotson.addProperty
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.set
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
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 okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import rx.Observable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
abstract class Luscious(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String ) : HttpSource() {
|
||||
|
||||
//Based on Luscios single source extension form https://github.com/tachiyomiorg/tachiyomi-extensions/commit/aacf56d0c0ddb173372aac69d798ae998f178377
|
||||
//with modifiaction to make it support multisrc
|
||||
|
||||
override val supportsLatest: Boolean = true
|
||||
private val apiBaseUrl: String = "$baseUrl/graphql/nobatch/"
|
||||
private val gson = Gson()
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
private val lusLang: String = lusLang(lang)
|
||||
private fun lusLang(lang: String): String {
|
||||
return when (lang) {
|
||||
"en" -> "1"
|
||||
"ja" -> "2"
|
||||
"es" -> "3"
|
||||
"it" -> "4"
|
||||
"de" -> "5"
|
||||
"fr" -> "6"
|
||||
"zh" -> "8"
|
||||
"ko" -> "9"
|
||||
"pt" -> "100"
|
||||
"th" -> "101"
|
||||
else -> "99"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Common
|
||||
|
||||
private fun buildAlbumListRequestInput(page: Int, filters: FilterList, query: String = ""): JsonObject {
|
||||
val sortByFilter = filters.findInstance<SortBySelectFilter>()!!
|
||||
val albumTypeFilter = filters.findInstance<AlbumTypeSelectFilter>()!!
|
||||
val interestsFilter = filters.findInstance<InterestGroupFilter>()!!
|
||||
val languagesFilter = filters.findInstance<LanguageGroupFilter>()!!
|
||||
val tagsFilter = filters.findInstance<TagGroupFilter>()!!
|
||||
val genreFilter = filters.findInstance<GenreGroupFilter>()!!
|
||||
val contentTypeFilter = filters.findInstance<ContentTypeSelectFilter>()!!
|
||||
|
||||
return JsonObject().apply {
|
||||
add(
|
||||
"input",
|
||||
JsonObject().apply {
|
||||
addProperty("display", sortByFilter.selected)
|
||||
addProperty("page", page)
|
||||
add(
|
||||
"filters",
|
||||
JsonArray().apply {
|
||||
|
||||
if (contentTypeFilter.selected != FILTER_VALUE_IGNORE)
|
||||
add(contentTypeFilter.toJsonObject("content_id"))
|
||||
|
||||
if (albumTypeFilter.selected != FILTER_VALUE_IGNORE)
|
||||
add(albumTypeFilter.toJsonObject("album_type"))
|
||||
|
||||
with(interestsFilter) {
|
||||
if (this.selected.isEmpty()) {
|
||||
throw Exception("Please select an Interest")
|
||||
}
|
||||
add(this.toJsonObject("audience_ids"))
|
||||
}
|
||||
|
||||
add(
|
||||
languagesFilter.toJsonObject("language_ids").apply {
|
||||
set("value", "+$lusLang${get("value").asString}")
|
||||
}
|
||||
)
|
||||
|
||||
if (tagsFilter.anyNotIgnored()) {
|
||||
add(tagsFilter.toJsonObject("tagged"))
|
||||
}
|
||||
|
||||
if (genreFilter.anyNotIgnored()) {
|
||||
add(genreFilter.toJsonObject("genre_ids"))
|
||||
}
|
||||
|
||||
if (query != "") {
|
||||
add(
|
||||
JsonObject().apply {
|
||||
addProperty("name", "search_query")
|
||||
addProperty("value", query)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAlbumListRequest(page: Int, filters: FilterList, query: String = ""): Request {
|
||||
val input = buildAlbumListRequestInput(page, filters, query)
|
||||
val url = HttpUrl.parse(apiBaseUrl)!!.newBuilder()
|
||||
.addQueryParameter("operationName", "AlbumList")
|
||||
.addQueryParameter("query", ALBUM_LIST_REQUEST_GQL)
|
||||
.addQueryParameter("variables", input.toString())
|
||||
.toString()
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
private fun parseAlbumListResponse(response: Response): MangasPage {
|
||||
val data = gson.fromJson<JsonObject>(response.body()!!.string())
|
||||
with(data["data"]["album"]["list"]) {
|
||||
return MangasPage(
|
||||
this["items"].asJsonArray.map {
|
||||
SManga.create().apply {
|
||||
url = it["url"].asString
|
||||
title = it["title"].asString
|
||||
thumbnail_url = it["cover"]["url"].asString
|
||||
}
|
||||
},
|
||||
this["info"]["has_next_page"].asBoolean
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(LATEST_DEFAULT_SORT_STATE))
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage = parseAlbumListResponse(response)
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
return listOf(
|
||||
SChapter.create().apply {
|
||||
url = response.request().url().toString()
|
||||
name = "Chapter"
|
||||
date_upload = document.select(".album-info-item:contains(Created:)")?.first()?.ownText()?.trim()?.let {
|
||||
DATE_FORMATS_WITH_ORDINAL_SUFFIXES.mapNotNull { format -> format.parseOrNull(it) }.firstOrNull()?.time
|
||||
} ?: 0L
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
private fun buildAlbumPicturesRequestInput(id: String, page: Int, sortPagesByOption: String): JsonObject {
|
||||
return JsonObject().apply {
|
||||
addProperty(
|
||||
"input",
|
||||
JsonObject().apply {
|
||||
addProperty(
|
||||
"filters",
|
||||
JsonArray().apply {
|
||||
add(
|
||||
JsonObject().apply {
|
||||
addProperty("name", "album_id")
|
||||
addProperty("value", id)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
addProperty("display", sortPagesByOption)
|
||||
addProperty("page", page)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAlbumPicturesPageUrl(id: String, page: Int, sortPagesByOption: String): String {
|
||||
val input = buildAlbumPicturesRequestInput(id, page, sortPagesByOption)
|
||||
return HttpUrl.parse(apiBaseUrl)!!.newBuilder()
|
||||
.addQueryParameter("operationName", "AlbumListOwnPictures")
|
||||
.addQueryParameter("query", ALBUM_PICTURES_REQUEST_GQL)
|
||||
.addQueryParameter("variables", input.toString())
|
||||
.toString()
|
||||
}
|
||||
|
||||
private fun parseAlbumPicturesResponse(response: Response, sortPagesByOption: String): List<Page> {
|
||||
|
||||
val id = response.request().url().queryParameter("variables").toString()
|
||||
.let { gson.fromJson<JsonObject>(it)["input"]["filters"].asJsonArray }
|
||||
.let { it.first { f -> f["name"].asString == "album_id" } }
|
||||
.let { it["value"].asString }
|
||||
|
||||
val data = gson.fromJson<JsonObject>(response.body()!!.string())
|
||||
.let { it["data"]["picture"]["list"].asJsonObject }
|
||||
|
||||
return data["items"].asJsonArray.mapIndexed { index, it ->
|
||||
Page(index, imageUrl = it["thumbnails"][0]["url"].asString)
|
||||
} + if (data["info"]["total_pages"].asInt > 1) { // get 2nd page onwards
|
||||
(ITEMS_PER_PAGE until data["info"]["total_items"].asInt).chunked(ITEMS_PER_PAGE).mapIndexed { page, indices ->
|
||||
indices.map { Page(it, url = buildAlbumPicturesPageUrl(id, page + 2, sortPagesByOption)) }
|
||||
}.flatten()
|
||||
} else emptyList()
|
||||
}
|
||||
|
||||
private fun getAlbumSortPagesOption(chapter: SChapter): Observable<String> {
|
||||
return client.newCall(GET(chapter.url))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val sortByKey = it.asJsoup().select(".o-input-select:contains(Sorted By) .o-select-value")?.text() ?: ""
|
||||
ALBUM_PICTURES_SORT_OPTIONS.getValue(sortByKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
val id = chapter.url.substringAfterLast("_").removeSuffix("/")
|
||||
|
||||
return getAlbumSortPagesOption(chapter)
|
||||
.concatMap { sortPagesByOption ->
|
||||
client.newCall(GET(buildAlbumPicturesPageUrl(id, 1, sortPagesByOption)))
|
||||
.asObservableSuccess()
|
||||
.map { parseAlbumPicturesResponse(it, sortPagesByOption) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
if (page.imageUrl != null) {
|
||||
return Observable.just(page.imageUrl)
|
||||
}
|
||||
|
||||
return client.newCall(GET(page.url, headers))
|
||||
.asObservableSuccess()
|
||||
.map {
|
||||
val data = gson.fromJson<JsonObject>(it.body()!!.string()).let { data ->
|
||||
data["data"]["picture"]["list"].asJsonObject
|
||||
}
|
||||
data["items"].asJsonArray[page.index % 50].asJsonObject["thumbnails"][0]["url"].asString
|
||||
}
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
private fun parseMangaGenre(document: Document): String {
|
||||
return listOf(
|
||||
document.select(".o-tag--secondary").map { it.text().substringBefore("(").trim() },
|
||||
document.select(".o-tag:not([href *= /tags/artist])").map { it.text() },
|
||||
document.select(".album-info-item:contains(Content:) .o-tag").map { it.text() }
|
||||
).flatten().joinToString()
|
||||
}
|
||||
|
||||
private fun parseMangaDescription(document: Document): String {
|
||||
val pageCount: String? = (
|
||||
document.select(".album-info-item:contains(pictures)").firstOrNull()
|
||||
?: document.select(".album-info-item:contains(gifs)").firstOrNull()
|
||||
)?.text()
|
||||
|
||||
return listOf(
|
||||
Pair("Description", document.select(".album-description:last-of-type")?.text()),
|
||||
Pair("Pages", pageCount)
|
||||
).let {
|
||||
it + listOf("Parody", "Character", "Ethnicity")
|
||||
.map { key -> key to document.select(".o-tag--category:contains($key) .o-tag").joinToString { t -> t.text() } }
|
||||
}.filter { desc -> !desc.second.isNullOrBlank() }
|
||||
.joinToString("\n\n") { "${it.first}:\n${it.second}" }
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
return SManga.create().apply {
|
||||
|
||||
artist = document.select(".o-tag--category:contains(Artist:) .o-tag")?.joinToString { it.text() }
|
||||
author = artist
|
||||
|
||||
genre = parseMangaGenre(document)
|
||||
|
||||
title = document.select("a[title]").text()
|
||||
status = when {
|
||||
title.contains("ongoing", true) -> SManga.ONGOING
|
||||
else -> SManga.COMPLETED
|
||||
}
|
||||
|
||||
description = parseMangaDescription(document)
|
||||
}
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage = parseAlbumListResponse(response)
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(POPULAR_DEFAULT_SORT_STATE))
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = parseAlbumListResponse(response)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = buildAlbumListRequest(
|
||||
page,
|
||||
filters.let {
|
||||
if (it.isEmpty()) getSortFilters(SEARCH_DEFAULT_SORT_STATE)
|
||||
else it
|
||||
},
|
||||
query
|
||||
)
|
||||
|
||||
class TriStateFilterOption(name: String, val value: String) : Filter.TriState(name)
|
||||
abstract class TriStateGroupFilter(name: String, options: List<TriStateFilterOption>) : Filter.Group<TriStateFilterOption>(name, options) {
|
||||
val included: List<String>
|
||||
get() = state.filter { it.isIncluded() }.map { it.value }
|
||||
|
||||
val excluded: List<String>
|
||||
get() = state.filter { it.isExcluded() }.map { it.value }
|
||||
|
||||
fun anyNotIgnored(): Boolean = state.any { !it.isIgnored() }
|
||||
|
||||
override fun toString(): String = (included.map { "+$it" } + excluded.map { "-$it" }).joinToString("")
|
||||
}
|
||||
|
||||
private fun Filter<*>.toJsonObject(key: String): JsonObject {
|
||||
val value = this.toString()
|
||||
return JsonObject().apply {
|
||||
addProperty("name", key)
|
||||
addProperty("value", value)
|
||||
}
|
||||
}
|
||||
|
||||
private class TagGroupFilter(filters: List<TriStateFilterOption>) : TriStateGroupFilter("Tags", filters)
|
||||
private class GenreGroupFilter(filters: List<TriStateFilterOption>) : TriStateGroupFilter("Genres", filters)
|
||||
|
||||
class CheckboxFilterOption(name: String, val value: String, default: Boolean = true) : Filter.CheckBox(name, default)
|
||||
abstract class CheckboxGroupFilter(name: String, options: List<CheckboxFilterOption>) : Filter.Group<CheckboxFilterOption>(name, options) {
|
||||
val selected: List<String>
|
||||
get() = state.filter { it.state }.map { it.value }
|
||||
|
||||
override fun toString(): String = selected.joinToString("") { "+$it" }
|
||||
}
|
||||
|
||||
private class InterestGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Interests", options)
|
||||
private class LanguageGroupFilter(options: List<CheckboxFilterOption>) : CheckboxGroupFilter("Languages", options)
|
||||
|
||||
class SelectFilterOption(val name: String, val value: String)
|
||||
|
||||
abstract class SelectFilter(name: String, private val options: List<SelectFilterOption>, default: Int = 0) : Filter.Select<String>(name, options.map { it.name }.toTypedArray(), default) {
|
||||
val selected: String
|
||||
get() = options[state].value
|
||||
|
||||
override fun toString(): String = selected
|
||||
}
|
||||
class SortBySelectFilter(options: List<SelectFilterOption>, default: Int) : SelectFilter("Sort By", options, default)
|
||||
class AlbumTypeSelectFilter(options: List<SelectFilterOption>) : SelectFilter("Album Type", options)
|
||||
class ContentTypeSelectFilter(options: List<SelectFilterOption>) : SelectFilter("Content Type", options)
|
||||
|
||||
override fun getFilterList(): FilterList = getSortFilters(POPULAR_DEFAULT_SORT_STATE)
|
||||
|
||||
private fun getSortFilters(sortState: Int) = FilterList(
|
||||
SortBySelectFilter(getSortFilters(), sortState),
|
||||
AlbumTypeSelectFilter(getAlbumTypeFilters()),
|
||||
ContentTypeSelectFilter(getContentTypeFilters()),
|
||||
InterestGroupFilter(getInterestFilters()),
|
||||
LanguageGroupFilter(getLanguageFilters()),
|
||||
TagGroupFilter(getTagFilters()),
|
||||
GenreGroupFilter(getGenreFilters())
|
||||
)
|
||||
|
||||
fun getSortFilters() = listOf(
|
||||
SelectFilterOption("Rating - All Time", "rating_all_time"),
|
||||
SelectFilterOption("Rating - Last 7 Days", "rating_7_days"),
|
||||
SelectFilterOption("Rating - Last 14 Days", "rating_14_days"),
|
||||
SelectFilterOption("Rating - Last 30 Days", "rating_30_days"),
|
||||
SelectFilterOption("Rating - Last 90 Days", "rating_90_days"),
|
||||
SelectFilterOption("Rating - Last Year", "rating_1_year"),
|
||||
SelectFilterOption("Rating - Last Year", "rating_1_year"),
|
||||
SelectFilterOption("Date - Newest First", "date_newest"),
|
||||
SelectFilterOption("Date - 2020", "date_2020"),
|
||||
SelectFilterOption("Date - 2019", "date_2019"),
|
||||
SelectFilterOption("Date - 2018", "date_2018"),
|
||||
SelectFilterOption("Date - 2017", "date_2017"),
|
||||
SelectFilterOption("Date - 2016", "date_2016"),
|
||||
SelectFilterOption("Date - 2015", "date_2015"),
|
||||
SelectFilterOption("Date - 2014", "date_2014"),
|
||||
SelectFilterOption("Date - 2013", "date_2013"),
|
||||
SelectFilterOption("Date - Oldest First", "date_oldest"),
|
||||
SelectFilterOption("Date - Upcoming", "date_upcoming"),
|
||||
SelectFilterOption("Date - Trending", "date_trending"),
|
||||
SelectFilterOption("Date - Featured", "date_featured"),
|
||||
SelectFilterOption("Date - Last Viewed", "date_last_interaction")
|
||||
)
|
||||
|
||||
fun getAlbumTypeFilters() = listOf(
|
||||
SelectFilterOption("Manga", "manga"),
|
||||
SelectFilterOption("All", FILTER_VALUE_IGNORE),
|
||||
SelectFilterOption("Pictures", "pictures")
|
||||
)
|
||||
|
||||
fun getContentTypeFilters() = listOf(
|
||||
SelectFilterOption("All", FILTER_VALUE_IGNORE),
|
||||
SelectFilterOption("Hentai", "0"),
|
||||
SelectFilterOption("Non-Erotic", "5"),
|
||||
SelectFilterOption("Real People", "6")
|
||||
)
|
||||
|
||||
fun getInterestFilters() = listOf(
|
||||
CheckboxFilterOption("Straight Sex", "1"),
|
||||
CheckboxFilterOption("Trans x Girl", "10", false),
|
||||
CheckboxFilterOption("Gay / Yaoi", "2"),
|
||||
CheckboxFilterOption("Lesbian / Yuri", "3"),
|
||||
CheckboxFilterOption("Trans", "5"),
|
||||
CheckboxFilterOption("Solo Girl", "6"),
|
||||
CheckboxFilterOption("Trans x Trans", "8"),
|
||||
CheckboxFilterOption("Trans x Guy", "9")
|
||||
)
|
||||
|
||||
fun getLanguageFilters() = listOf(
|
||||
CheckboxFilterOption("English", ENGLISH_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Japanese", JAPANESE_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Spanish", SPANISH_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Italian", ITALIAN_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("German", GERMAN_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("French", FRENCH_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Chinese", CHINESE_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Korean", KOREAN_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Others", OTHERS_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Portugese", PORTUGESE_LUS_LANG_VAL, false),
|
||||
CheckboxFilterOption("Thai", THAI_LUS_LANG_VAL, false)
|
||||
).filterNot { it.value == lusLang }
|
||||
|
||||
fun getTagFilters() = listOf(
|
||||
TriStateFilterOption("Big Breasts", "big_breasts"),
|
||||
TriStateFilterOption("Blowjob", "blowjob"),
|
||||
TriStateFilterOption("Anal", "anal"),
|
||||
TriStateFilterOption("Group", "group"),
|
||||
TriStateFilterOption("Big Ass", "big_ass"),
|
||||
TriStateFilterOption("Full Color", "full_color"),
|
||||
TriStateFilterOption("Schoolgirl", "schoolgirl"),
|
||||
TriStateFilterOption("Rape", "rape"),
|
||||
TriStateFilterOption("Glasses", "glasses"),
|
||||
TriStateFilterOption("Nakadashi", "nakadashi"),
|
||||
TriStateFilterOption("Yuri", "yuri"),
|
||||
TriStateFilterOption("Paizuri", "paizuri"),
|
||||
TriStateFilterOption("Ahegao", "ahegao"),
|
||||
TriStateFilterOption("Group: metart", "group%3A_metart"),
|
||||
TriStateFilterOption("Brunette", "brunette"),
|
||||
TriStateFilterOption("Solo", "solo"),
|
||||
TriStateFilterOption("Blonde", "blonde"),
|
||||
TriStateFilterOption("Shaved Pussy", "shaved_pussy"),
|
||||
TriStateFilterOption("Small Breasts", "small_breasts"),
|
||||
TriStateFilterOption("Cum", "cum"),
|
||||
TriStateFilterOption("Stockings", "stockings"),
|
||||
TriStateFilterOption("Yuri", "yuri"),
|
||||
TriStateFilterOption("Ass", "ass"),
|
||||
TriStateFilterOption("Creampie", "creampie"),
|
||||
TriStateFilterOption("Rape", "rape"),
|
||||
TriStateFilterOption("Oral Sex", "oral_sex"),
|
||||
TriStateFilterOption("Bondage", "bondage"),
|
||||
TriStateFilterOption("Futanari", "futanari"),
|
||||
TriStateFilterOption("Double Penetration", "double_penetration"),
|
||||
TriStateFilterOption("Threesome", "threesome"),
|
||||
TriStateFilterOption("Anal Sex", "anal_sex"),
|
||||
TriStateFilterOption("Big Cock", "big_cock"),
|
||||
TriStateFilterOption("Straight Sex", "straight_sex"),
|
||||
TriStateFilterOption("Yaoi", "yaoi")
|
||||
)
|
||||
|
||||
fun getGenreFilters() = listOf(
|
||||
TriStateFilterOption("3D / Digital Art", "25"),
|
||||
TriStateFilterOption("Amateurs", "20"),
|
||||
TriStateFilterOption("Artist Collection", "19"),
|
||||
TriStateFilterOption("Asian Girls", "12"),
|
||||
TriStateFilterOption("Cosplay", "22"),
|
||||
TriStateFilterOption("BDSM", "27"),
|
||||
TriStateFilterOption("Cross-Dressing", "30"),
|
||||
TriStateFilterOption("Defloration / First Time", "59"),
|
||||
TriStateFilterOption("Ebony Girls", "32"),
|
||||
TriStateFilterOption("European Girls", "46"),
|
||||
TriStateFilterOption("Fantasy / Monster Girls", "10"),
|
||||
TriStateFilterOption("Fetish", "2"),
|
||||
TriStateFilterOption("Furries", "8"),
|
||||
TriStateFilterOption("Futanari", "31"),
|
||||
TriStateFilterOption("Group Sex", "36"),
|
||||
TriStateFilterOption("Harem", "56"),
|
||||
TriStateFilterOption("Humor", "41"),
|
||||
TriStateFilterOption("Interracial", "28"),
|
||||
TriStateFilterOption("Kemonomimi / Animal Ears", "39"),
|
||||
TriStateFilterOption("Latina Girls", "33"),
|
||||
TriStateFilterOption("Mature", "13"),
|
||||
TriStateFilterOption("Members: Original Art", "18"),
|
||||
TriStateFilterOption("Members: Verified Selfies", "21"),
|
||||
TriStateFilterOption("Military", "48"),
|
||||
TriStateFilterOption("Mind Control", "34"),
|
||||
TriStateFilterOption("Monsters & Tentacles", "38"),
|
||||
TriStateFilterOption("Netorare / Cheating", "40"),
|
||||
TriStateFilterOption("No Genre Given", "1"),
|
||||
TriStateFilterOption("Nonconsent / Reluctance", "37"),
|
||||
TriStateFilterOption("Other Ethnicity Girls", "57"),
|
||||
TriStateFilterOption("Public Sex", "43"),
|
||||
TriStateFilterOption("Romance", "42"),
|
||||
TriStateFilterOption("School / College", "35"),
|
||||
TriStateFilterOption("Sex Workers", "47"),
|
||||
TriStateFilterOption("Softcore / Ecchi", "9"),
|
||||
TriStateFilterOption("Superheroes", "17"),
|
||||
TriStateFilterOption("Tankobon", "45"),
|
||||
TriStateFilterOption("TV / Movies", "51"),
|
||||
TriStateFilterOption("Trans", "14"),
|
||||
TriStateFilterOption("Video Games", "15"),
|
||||
TriStateFilterOption("Vintage", "58"),
|
||||
TriStateFilterOption("Western", "11"),
|
||||
TriStateFilterOption("Workplace Sex", "50")
|
||||
)
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
private fun SimpleDateFormat.parseOrNull(string: String): Date? {
|
||||
return try {
|
||||
parse(string)
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val ALBUM_PICTURES_SORT_OPTIONS = hashMapOf(
|
||||
Pair("Sort By Newest", "date_newest"),
|
||||
Pair("Sort By Rating", "rating_all_time")
|
||||
).withDefault { "position" }
|
||||
|
||||
private const val ITEMS_PER_PAGE = 50
|
||||
|
||||
private val ORDINAL_SUFFIXES = listOf("st", "nd", "rd", "th")
|
||||
private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES = ORDINAL_SUFFIXES.map {
|
||||
SimpleDateFormat("MMMM dd'$it', yyyy", Locale.US)
|
||||
}
|
||||
|
||||
const val ENGLISH_LUS_LANG_VAL = "1"
|
||||
const val JAPANESE_LUS_LANG_VAL = "2"
|
||||
const val SPANISH_LUS_LANG_VAL = "3"
|
||||
const val ITALIAN_LUS_LANG_VAL = "4"
|
||||
const val GERMAN_LUS_LANG_VAL = "5"
|
||||
const val FRENCH_LUS_LANG_VAL = "6"
|
||||
const val CHINESE_LUS_LANG_VAL = "8"
|
||||
const val KOREAN_LUS_LANG_VAL = "9"
|
||||
const val OTHERS_LUS_LANG_VAL = "99"
|
||||
const val PORTUGESE_LUS_LANG_VAL = "100"
|
||||
const val THAI_LUS_LANG_VAL = "101"
|
||||
|
||||
private const val POPULAR_DEFAULT_SORT_STATE = 0
|
||||
private const val LATEST_DEFAULT_SORT_STATE = 7
|
||||
private const val SEARCH_DEFAULT_SORT_STATE = 0
|
||||
|
||||
private const val FILTER_VALUE_IGNORE = "<ignore>"
|
||||
|
||||
private val ALBUM_LIST_REQUEST_GQL = """
|
||||
query AlbumList(${'$'}input: AlbumListInput!) {
|
||||
album {
|
||||
list(input: ${'$'}input) {
|
||||
info {
|
||||
page
|
||||
has_next_page
|
||||
}
|
||||
items
|
||||
}
|
||||
}
|
||||
}
|
||||
""".replace("\n", " ").replace("\\s+".toRegex(), " ")
|
||||
|
||||
private val ALBUM_PICTURES_REQUEST_GQL = """
|
||||
query AlbumListOwnPictures(${'$'}input: PictureListInput!) {
|
||||
picture {
|
||||
list(input: ${'$'}input) {
|
||||
info {
|
||||
total_items
|
||||
total_pages
|
||||
page
|
||||
has_next_page
|
||||
}
|
||||
items {
|
||||
thumbnails {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".replace("\n", " ").replace("\\s+".toRegex(), " ")
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.luscious
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class LusciousGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "luscious"
|
||||
|
||||
override val themeClass = "Luscious"
|
||||
|
||||
override val baseVersionCode: Int = 2
|
||||
|
||||
override val sources = listOf(
|
||||
MultiLang("Luscious", "https://www.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousFactory", overrideVersionCode = 2),
|
||||
MultiLang("Luscious (Members)", "https://members.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousMembersFactory", pkgName = "lusciousmembers"),//Requires Account
|
||||
MultiLang("Luscious (API)", "https://api.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousAPIFactory", pkgName = "lusciousapi")
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
LusciousGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,554 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.madara
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.random.Random
|
||||
|
||||
abstract class Madara(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
// helps with cloudflare for some sources, makes it worse for others; override with empty string if the latter is true
|
||||
protected open val userAgentRandomizer = " ${Random.nextInt().absoluteValue}"
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/78.0$userAgentRandomizer")
|
||||
|
||||
// Popular Manga
|
||||
|
||||
override fun popularMangaSelector() = "div.page-item-detail"
|
||||
|
||||
open val popularMangaUrlSelector = "div.post-title a"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
with(element) {
|
||||
select(popularMangaUrlSelector).first()?.let {
|
||||
manga.setUrlWithoutDomain(it.attr("abs:href"))
|
||||
manga.title = it.ownText()
|
||||
}
|
||||
|
||||
select("img").first()?.let {
|
||||
manga.thumbnail_url = imageFromElement(it)
|
||||
}
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
open fun formBuilder(page: Int, popular: Boolean) = FormBody.Builder().apply {
|
||||
add("action", "madara_load_more")
|
||||
add("page", (page - 1).toString())
|
||||
add("template", "madara-core/content/content-archive")
|
||||
add("vars[orderby]", "meta_value_num")
|
||||
add("vars[paged]", "1")
|
||||
add("vars[posts_per_page]", "20")
|
||||
add("vars[post_type]", "wp-manga")
|
||||
add("vars[post_status]", "publish")
|
||||
add("vars[meta_key]", if (popular) "_wp_manga_views" else "_latest_update")
|
||||
add("vars[order]", "desc")
|
||||
add("vars[sidebar]", if (popular) "full" else "right")
|
||||
add("vars[manga_archives_item_layout]", "big_thumbnail")
|
||||
}
|
||||
|
||||
open val formHeaders: Headers by lazy { headersBuilder().build() }
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return POST("$baseUrl/wp-admin/admin-ajax.php", formHeaders, formBuilder(page, true).build(), CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = "body:not(:has(.no-posts))"
|
||||
|
||||
// Latest Updates
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
// Even if it's different from the popular manga's list, the relevant classes are the same
|
||||
return popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return POST("$baseUrl/wp-admin/admin-ajax.php", formHeaders, formBuilder(page, false).build(), CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = popularMangaNextPageSelector()
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val mp = super.latestUpdatesParse(response)
|
||||
val mangas = mp.mangas.distinctBy { it.url }
|
||||
return MangasPage(mangas, mp.hasNextPage)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
// Error message for exceeding last page
|
||||
if (response.code() == 404)
|
||||
error("Already on the Last Page!")
|
||||
else throw Exception("HTTP error ${response.code()}")
|
||||
}
|
||||
}
|
||||
.map { response ->
|
||||
searchMangaParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
// Search Manga
|
||||
|
||||
protected open fun searchPage(page: Int): String = "page/$page/"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/${searchPage(page)}")!!.newBuilder()
|
||||
url.addQueryParameter("s", query)
|
||||
url.addQueryParameter("post_type", "wp-manga")
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is AuthorFilter -> {
|
||||
if (filter.state.isNotBlank()) {
|
||||
url.addQueryParameter("author", filter.state)
|
||||
}
|
||||
}
|
||||
is ArtistFilter -> {
|
||||
if (filter.state.isNotBlank()) {
|
||||
url.addQueryParameter("artist", filter.state)
|
||||
}
|
||||
}
|
||||
is YearFilter -> {
|
||||
if (filter.state.isNotBlank()) {
|
||||
url.addQueryParameter("release", filter.state)
|
||||
}
|
||||
}
|
||||
is StatusFilter -> {
|
||||
filter.state.forEach {
|
||||
if (it.state) {
|
||||
url.addQueryParameter("status[]", it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is OrderByFilter -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("m_orderby", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
is GenreConditionFilter -> {
|
||||
url.addQueryParameter("op", filter.toUriPart())
|
||||
}
|
||||
is GenreList -> {
|
||||
filter.state
|
||||
.filter { it.state }
|
||||
.let { list ->
|
||||
if (list.isNotEmpty()) { list.forEach { genre -> url.addQueryParameter("genre[]", genre.id) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
private class AuthorFilter : Filter.Text("Author")
|
||||
private class ArtistFilter : Filter.Text("Artist")
|
||||
private class YearFilter : Filter.Text("Year of Released")
|
||||
private class StatusFilter(status: List<Tag>) : Filter.Group<Tag>("Status", status)
|
||||
private class OrderByFilter : UriPartFilter(
|
||||
"Order By",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Latest", "latest"),
|
||||
Pair("A-Z", "alphabet"),
|
||||
Pair("Rating", "rating"),
|
||||
Pair("Trending", "trending"),
|
||||
Pair("Most Views", "views"),
|
||||
Pair("New", "new-manga")
|
||||
)
|
||||
)
|
||||
private class GenreConditionFilter : UriPartFilter(
|
||||
"Genre condition",
|
||||
arrayOf(
|
||||
Pair("or", ""),
|
||||
Pair("and", "1")
|
||||
)
|
||||
)
|
||||
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
class Genre(name: String, val id: String = name) : Filter.CheckBox(name)
|
||||
|
||||
open fun getGenreList() = listOf(
|
||||
Genre("Adventure", "Adventure"),
|
||||
Genre("Action", "action"),
|
||||
Genre("Adventure", "adventure"),
|
||||
Genre("Cars", "cars"),
|
||||
Genre("4-Koma", "4-koma"),
|
||||
Genre("Comedy", "comedy"),
|
||||
Genre("Completed", "completed"),
|
||||
Genre("Cooking", "cooking"),
|
||||
Genre("Dementia", "dementia"),
|
||||
Genre("Demons", "demons"),
|
||||
Genre("Doujinshi", "doujinshi"),
|
||||
Genre("Drama", "drama"),
|
||||
Genre("Ecchi", "ecchi"),
|
||||
Genre("Fantasy", "fantasy"),
|
||||
Genre("Game", "game"),
|
||||
Genre("Gender Bender", "gender-bender"),
|
||||
Genre("Harem", "harem"),
|
||||
Genre("Historical", "historical"),
|
||||
Genre("Horror", "horror"),
|
||||
Genre("Isekai", "isekai"),
|
||||
Genre("Josei", "josei"),
|
||||
Genre("Kids", "kids"),
|
||||
Genre("Magic", "magic"),
|
||||
Genre("Manga", "manga"),
|
||||
Genre("Manhua", "manhua"),
|
||||
Genre("Manhwa", "manhwa"),
|
||||
Genre("Martial Arts", "martial-arts"),
|
||||
Genre("Mature", "mature"),
|
||||
Genre("Mecha", "mecha"),
|
||||
Genre("Military", "military"),
|
||||
Genre("Music", "music"),
|
||||
Genre("Mystery", "mystery"),
|
||||
Genre("Old Comic", "old-comic"),
|
||||
Genre("One Shot", "one-shot"),
|
||||
Genre("Oneshot", "oneshot"),
|
||||
Genre("Parodi", "parodi"),
|
||||
Genre("Parody", "parody"),
|
||||
Genre("Police", "police"),
|
||||
Genre("Psychological", "psychological"),
|
||||
Genre("Romance", "romance"),
|
||||
Genre("Samurai", "samurai"),
|
||||
Genre("School", "school"),
|
||||
Genre("School Life", "school-life"),
|
||||
Genre("Sci-Fi", "sci-fi"),
|
||||
Genre("Seinen", "seinen"),
|
||||
Genre("Shoujo", "shoujo"),
|
||||
Genre("Shoujo Ai", "shoujo-ai"),
|
||||
Genre("Shounen", "shounen"),
|
||||
Genre("Shounen ai", "shounen-ai"),
|
||||
Genre("Slice of Life", "slice-of-life"),
|
||||
Genre("Sports", "sports"),
|
||||
Genre("Super Power", "super-power"),
|
||||
Genre("Supernatural", "supernatural"),
|
||||
Genre("Thriller", "thriller"),
|
||||
Genre("Tragedy", "tragedy"),
|
||||
Genre("Vampire", "vampire"),
|
||||
Genre("Webtoons", "webtoons"),
|
||||
Genre("Yaoi", "yaoi"),
|
||||
Genre("Yuri", "yuri")
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
AuthorFilter(),
|
||||
ArtistFilter(),
|
||||
YearFilter(),
|
||||
StatusFilter(getStatusList()),
|
||||
OrderByFilter(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Genres may not work for all sources"),
|
||||
GenreConditionFilter(),
|
||||
GenreList(getGenreList())
|
||||
)
|
||||
|
||||
private fun getStatusList() = listOf(
|
||||
Tag("end", "Completed"),
|
||||
Tag("on-going", "Ongoing"),
|
||||
Tag("canceled", "Canceled"),
|
||||
Tag("on-hold", "On Hold")
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
open class Tag(val id: String, name: String) : Filter.CheckBox(name)
|
||||
|
||||
override fun searchMangaSelector() = "div.c-tabs-item__content"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
with(element) {
|
||||
select("div.post-title a").first()?.let {
|
||||
manga.setUrlWithoutDomain(it.attr("abs:href"))
|
||||
manga.title = it.ownText()
|
||||
}
|
||||
select("img").first()?.let {
|
||||
manga.thumbnail_url = imageFromElement(it)
|
||||
}
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = "div.nav-previous, nav.navigation-ajax, a.nextpostslink"
|
||||
|
||||
// Manga Details Parse
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
with(document) {
|
||||
select("div.post-title h3").first()?.let {
|
||||
manga.title = it.ownText()
|
||||
}
|
||||
select("div.author-content").first()?.let {
|
||||
if (it.text().notUpdating()) manga.author = it.text()
|
||||
}
|
||||
select("div.artist-content").first()?.let {
|
||||
if (it.text().notUpdating()) manga.artist = it.text()
|
||||
}
|
||||
select("div.description-summary div.summary__content").let {
|
||||
if (it.select("p").text().isNotEmpty()) {
|
||||
manga.description = it.select("p").joinToString(separator = "\n\n") { p ->
|
||||
p.text().replace("<br>", "\n")
|
||||
}
|
||||
} else {
|
||||
manga.description = it.text()
|
||||
}
|
||||
}
|
||||
select("div.summary_image img").first()?.let {
|
||||
manga.thumbnail_url = imageFromElement(it)
|
||||
}
|
||||
select("div.summary-content").last()?.let {
|
||||
manga.status = when (it.text()) {
|
||||
// I don't know what's the corresponding for COMPLETED and LICENSED
|
||||
// There's no support for "Canceled" or "On Hold"
|
||||
"Completed", "Completo", "Concluído", "Terminé" -> SManga.COMPLETED
|
||||
"OnGoing", "Продолжается", "Updating", "Em Lançamento", "Em andamento", "En cours" -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
val genres = select("div.genres-content a")
|
||||
.map { element -> element.text().toLowerCase() }
|
||||
.toMutableSet()
|
||||
|
||||
// add tag(s) to genre
|
||||
select("div.tags-content a").forEach { element ->
|
||||
if (genres.contains(element.text()).not()) {
|
||||
genres.add(element.text().toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
// add manga/manhwa/manhua thinggy to genre
|
||||
document.select(seriesTypeSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not() && it.notUpdating() && it != "-" && genres.contains(it).not()) {
|
||||
genres.add(it.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
manga.genre = genres.toList().map { it.capitalize() }.joinToString(", ")
|
||||
|
||||
// add alternative name to manga description
|
||||
document.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not() && it.notUpdating()) {
|
||||
manga.description += when {
|
||||
manga.description.isNullOrEmpty() -> altName + it
|
||||
else -> "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
open val seriesTypeSelector = ".post-content_item:contains(Type) .summary-content"
|
||||
open val altNameSelector = ".post-content_item:contains(Alt) .summary-content"
|
||||
open val altName = "Alternative Name" + ": "
|
||||
|
||||
private fun String.notUpdating(): Boolean {
|
||||
return this.contains("Updating", true).not()
|
||||
}
|
||||
|
||||
protected fun imageFromElement(element: Element): String? {
|
||||
return when {
|
||||
element.hasAttr("data-src") -> element.attr("abs:data-src")
|
||||
element.hasAttr("data-lazy-src") -> element.attr("abs:data-lazy-src")
|
||||
element.hasAttr("srcset") -> element.attr("abs:srcset").substringBefore(" ")
|
||||
else -> element.attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getXhrChapters(mangaId: String): Document {
|
||||
val xhrHeaders = headersBuilder().add("Content-Type: application/x-www-form-urlencoded; charset=UTF-8")
|
||||
.add("Referer", baseUrl)
|
||||
.build()
|
||||
val body = RequestBody.create(null, "action=manga_get_chapters&manga=$mangaId")
|
||||
return client.newCall(POST("$baseUrl/wp-admin/admin-ajax.php", xhrHeaders, body)).execute().asJsoup()
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val dataIdSelector = "div[id^=manga-chapters-holder]"
|
||||
|
||||
return document.select(chapterListSelector())
|
||||
.let { elements ->
|
||||
if (elements.isEmpty() && !document.select(dataIdSelector).isNullOrEmpty())
|
||||
getXhrChapters(document.select(dataIdSelector).attr("data-id")).select(chapterListSelector())
|
||||
else elements
|
||||
}
|
||||
.map { chapterFromElement(it) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "li.wp-manga-chapter"
|
||||
|
||||
open val chapterUrlSelector = "a"
|
||||
|
||||
open val chapterUrlSuffix = "?style=list"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
|
||||
with(element) {
|
||||
select(chapterUrlSelector).first()?.let { urlElement ->
|
||||
chapter.url = urlElement.attr("abs:href").let {
|
||||
it.substringBefore("?style=paged") + if (!it.endsWith(chapterUrlSuffix)) chapterUrlSuffix else ""
|
||||
}
|
||||
chapter.name = urlElement.text()
|
||||
}
|
||||
|
||||
// Dates can be part of a "new" graphic or plain text
|
||||
chapter.date_upload = select("img").firstOrNull()?.attr("alt")?.let { parseRelativeDate(it) }
|
||||
?: parseChapterDate(select("span.chapter-release-date i").firstOrNull()?.text())
|
||||
}
|
||||
|
||||
return chapter
|
||||
}
|
||||
|
||||
open fun parseChapterDate(date: String?): Long {
|
||||
date ?: return 0
|
||||
|
||||
fun SimpleDateFormat.tryParse(string: String): Long {
|
||||
return try {
|
||||
parse(string)?.time ?: 0
|
||||
} catch (_: ParseException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
date.endsWith(" ago", ignoreCase = true) -> {
|
||||
parseRelativeDate(date)
|
||||
}
|
||||
// Handle translated 'ago' in Portuguese.
|
||||
date.endsWith(" atrás", ignoreCase = true) -> {
|
||||
parseRelativeDate(date)
|
||||
}
|
||||
// Handle translated 'ago' in Turkish.
|
||||
date.endsWith(" önce", ignoreCase = true) -> {
|
||||
parseRelativeDate(date)
|
||||
}
|
||||
// Handle 'yesterday' and 'today', using midnight
|
||||
date.startsWith("year", ignoreCase = true) -> {
|
||||
Calendar.getInstance().apply {
|
||||
add(Calendar.DAY_OF_MONTH, -1) // yesterday
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
}
|
||||
date.startsWith("today", ignoreCase = true) -> {
|
||||
Calendar.getInstance().apply {
|
||||
set(Calendar.HOUR_OF_DAY, 0)
|
||||
set(Calendar.MINUTE, 0)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
}
|
||||
date.contains(Regex("""\d(st|nd|rd|th)""")) -> {
|
||||
// Clean date (e.g. 5th December 2019 to 5 December 2019) before parsing it
|
||||
date.split(" ").map {
|
||||
if (it.contains(Regex("""\d\D\D"""))) {
|
||||
it.replace(Regex("""\D"""), "")
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
.let { dateFormat.tryParse(it.joinToString(" ")) }
|
||||
}
|
||||
else -> dateFormat.tryParse(date)
|
||||
}
|
||||
}
|
||||
|
||||
// Parses dates in this form:
|
||||
// 21 horas ago
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = Regex("""(\d+)""").find(date)?.value?.toIntOrNull() ?: return 0
|
||||
val cal = Calendar.getInstance()
|
||||
|
||||
return when {
|
||||
WordSet("hari", "gün", "jour", "día", "dia", "day").anyWordIn(date) -> cal.apply { add(Calendar.DAY_OF_MONTH, -number) }.timeInMillis
|
||||
WordSet("jam", "saat", "heure", "hora", "hour").anyWordIn(date) -> cal.apply { add(Calendar.HOUR, -number) }.timeInMillis
|
||||
WordSet("menit", "dakika", "min", "minute", "minuto").anyWordIn(date) -> cal.apply { add(Calendar.MINUTE, -number) }.timeInMillis
|
||||
WordSet("detik", "segundo", "second").anyWordIn(date) -> cal.apply { add(Calendar.SECOND, -number) }.timeInMillis
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
if (chapter.url.startsWith("http")) {
|
||||
return GET(chapter.url, headers)
|
||||
}
|
||||
return super.pageListRequest(chapter)
|
||||
}
|
||||
|
||||
open val pageListParseSelector = "div.page-break, li.blocks-gallery-item"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListParseSelector).mapIndexed { index, element ->
|
||||
Page(
|
||||
index,
|
||||
document.location(),
|
||||
element.select("img").first()?.let {
|
||||
it.absUrl(if (it.hasAttr("data-src")) "data-src" else "src")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headers.newBuilder().set("Referer", page.url).build())
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
|
||||
}
|
||||
|
||||
class WordSet(private vararg val words: String) { fun anyWordIn(dateString: String): Boolean = words.any { dateString.contains(it, ignoreCase = true) } }
|
@ -1,269 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.madara
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MadaraGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "madara"
|
||||
|
||||
override val themeClass = "Madara"
|
||||
|
||||
override val baseVersionCode: Int = 3
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Adonis Fansub", "https://manga.adonisfansub.com", "tr"),
|
||||
SingleLang("AkuManga", "https://akumanga.com", "ar"),
|
||||
SingleLang("AlianzaMarcial", "https://alianzamarcial.xyz", "es"),
|
||||
SingleLang("AllPornComic", "https://allporncomic.com", "en", isNsfw = true),
|
||||
SingleLang("Aloalivn", "https://aloalivn.com", "en", overrideVersionCode = 2),
|
||||
SingleLang("AniMangaEs", "https://animangaes.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Agent of Change Translations", "https://aoc.moe", "en", overrideVersionCode = 1),
|
||||
SingleLang("ApollComics", "https://apollcomics.xyz", "es"),
|
||||
SingleLang("Arang Scans", "https://arangscans.com", "en", overrideVersionCode = 2),
|
||||
SingleLang("ArazNovel", "https://www.araznovel.com", "tr"),
|
||||
SingleLang("Argos Scan", "https://argosscan.com", "pt-BR"),
|
||||
SingleLang("Asgard Team", "https://www.asgard1team.com", "ar", overrideVersionCode = 1),
|
||||
SingleLang("Astral Library", "https://www.astrallibrary.net", "en", overrideVersionCode = 1),
|
||||
SingleLang("Atikrost", "https://atikrost.com", "tr"),
|
||||
SingleLang("ATM-Subs", "https://atm-subs.fr", "fr", className = "ATMSubs"),
|
||||
SingleLang("AYATOON", "https://ayatoon.com", "tr"),
|
||||
SingleLang("Azora", "https://azoramanga.com", "ar", overrideVersionCode = 1),
|
||||
SingleLang("Bakaman", "https://bakaman.net", "th", overrideVersionCode = 1),
|
||||
SingleLang("BestManga", "https://bestmanga.club", "ru"),
|
||||
SingleLang("BestManhua", "https://bestmanhua.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("BoysLove", "https://boyslove.me", "en"),
|
||||
SingleLang("CatOnHeadTranslations", "https://catonhead.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("CAT-translator", "https://cat-translator.com", "th", className = "CatTranslator"),
|
||||
SingleLang("Chibi Manga", "https://www.cmreader.info", "en"),
|
||||
SingleLang("Clover Manga", "https://clover-manga.com", "tr", overrideVersionCode = 1),
|
||||
SingleLang("ComicKiba", "https://comickiba.com", "en"),
|
||||
SingleLang("Comics Valley", "https://comicsvalley.com", "hi", isNsfw = true),
|
||||
SingleLang("CopyPasteScan", "https://copypastescan.xyz", "es"),
|
||||
SingleLang("Cutie Pie", "https://cutiepie.ga", "tr"),
|
||||
SingleLang("Darkyu Realm", "https://darkyuerealm.site", "pt-BR"),
|
||||
SingleLang("Decadence Scans", "https://reader.decadencescans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("شبكة كونان العربية", "https://www.manga.detectiveconanar.com", "ar", className = "DetectiveConanAr", overrideVersionCode = 1),
|
||||
SingleLang("DiamondFansub", "https://diamondfansub.com", "tr"),
|
||||
SingleLang("Disaster Scans", "https://disasterscans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("DoujinHentai", "https://doujinhentai.net", "es", isNsfw = true),
|
||||
SingleLang("DoujinYosh", "https://doujinyosh.work", "id"),
|
||||
SingleLang("Dream Manga", "https://en.ruyamanga.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Drope Scan", "https://dropescan.com", "pt-BR"),
|
||||
SingleLang("Einherjar Scan", "https://einherjarscans.space", "en"),
|
||||
SingleLang("FDM Scan", "https://fdmscan.com", "pt-BR"),
|
||||
SingleLang("1st Kiss", "https://1stkissmanga.com", "en", className = "FirstKissManga", overrideVersionCode = 1),
|
||||
SingleLang("1st Kiss Manhua", "https://1stkissmanhua.com", "en", className = "FirstKissManhua"),
|
||||
SingleLang("FreeWebtoonCoins", "https://freewebtooncoins.com", "en"),
|
||||
SingleLang("Furio Scans", "https://furioscans.com", "pt-BR"),
|
||||
SingleLang("موقع لترجمة المانجا", "https://golden-manga.com", "ar", className = "GoldenManga"),
|
||||
SingleLang("GalaxyDegenScans", "https://gdegenscans.xyz/", "en"),
|
||||
SingleLang("Graze Scans", "https://grazescans.com/", "en", overrideVersionCode = 1),
|
||||
SingleLang("GuncelManga", "https://guncelmanga.com", "tr"),
|
||||
SingleLang("Hero Manhua", "https://heromanhua.com", "en"),
|
||||
SingleLang("Heroz Scanlation", "https://herozscans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Himera Fansub", "https://himera-fansub.com", "tr"),
|
||||
SingleLang("Hiperdex", "https://hiperdex.com", "en", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Hscans", "https://hscans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Hunter Fansub", "https://hunterfansub.com", "es"),
|
||||
SingleLang("Ichirin No Hana Yuri", "https://ichirinnohanayuri.com.br", "pt-BR"),
|
||||
SingleLang("Immortal Updates", "https://immortalupdates.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("IsekaiScan.com", "https://isekaiscan.com", "en", className = "IsekaiScanCom", overrideVersionCode = 2),
|
||||
SingleLang("IsekaiScanManga (unoriginal)", "https://isekaiscanmanga.com", "en", className = "IsekaiScanManga"),
|
||||
SingleLang("Its Your Right Manhua", "https://itsyourightmanhua.com/", "en"),
|
||||
SingleLang("JJutsuScans", "https://jjutsuscans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Just For Fun", "https://just-for-fun.ru", "ru"),
|
||||
SingleLang("KingzManga", "https://kingzmanga.com", "ar"),
|
||||
SingleLang("KisekiManga", "https://kisekimanga.com", "en"),
|
||||
SingleLang("KlikManga", "https://klikmanga.com", "id"),
|
||||
SingleLang("Kissmanga.in", "https://kissmanga.in", "en", className= "KissmangaIn"),
|
||||
SingleLang("Kombatch", "https://kombatch.com", "id"),
|
||||
SingleLang("Lily Manga", "https://lilymanga.com", "en"),
|
||||
SingleLang("LovableSubs", "https://lovablesubs.com", "tr"),
|
||||
SingleLang("Manga18 Fun", "https://manga18.fun", "en"),
|
||||
SingleLang("Manga18 Fx", "https://manga18fx.com", "en"),
|
||||
SingleLang("Manga347", "https://manga347.com", "en", overrideVersionCode = 2),
|
||||
SingleLang("مانجا العاشق", "https://3asq.org", "ar", className = "Manga3asq"),
|
||||
SingleLang("Manga3S", "https://manga3s.com", "en"),
|
||||
SingleLang("Manga68", "https://manga68.com", "en"),
|
||||
SingleLang("Manga Action", "https://manga-action.com", "ar", overrideVersionCode = 1),
|
||||
SingleLang("Manga Arab Online مانجا عرب اون لاين", "https://mangaarabonline.com", "ar", className = "MangaArabOnline"),
|
||||
SingleLang("مانجا عرب تيم Manga Arab Team", "https://mangaarabteam.com", "ar", className = "MangaArabTeam"),
|
||||
SingleLang("MangaBaz", "https://mangabaz.com", "tr"),
|
||||
SingleLang("Manga Bin", "https://mangabin.com/", "en"),
|
||||
SingleLang("MangaBob", "https://mangabob.com", "en"),
|
||||
SingleLang("Manga Chill", "https://mangachill.com/", "en"),
|
||||
SingleLang("Manga Clash", "https://mangaclash.com", "en"),
|
||||
SingleLang("MangaCultivator", "https://mangacultivator.com", "en"),
|
||||
SingleLang("MangaDods", "https://www.mangadods.com", "en"),
|
||||
SingleLang("Manga Drop Out", "https://www.mangadropout.xyz", "id", isNsfw = true),
|
||||
SingleLang("MangaEffect", "https://mangaeffect.com", "en"),
|
||||
SingleLang("MangaGreat", "https://mangagreat.com", "en"),
|
||||
SingleLang("Manga Hentai", "https://mangahentai.me", "en", isNsfw = true),
|
||||
SingleLang("Mangakik", "https://mangakik.com", "en"),
|
||||
SingleLang("Manga Kiss", "https://mangakiss.org", "en"),
|
||||
SingleLang("MangaKomi", "https://mangakomi.com", "en"),
|
||||
SingleLang("Manga Land Arabic", "https://mangalandarabic.com", "ar"),
|
||||
SingleLang("مانجا ليك", "https://mangalek.com", "ar", className = "Mangalek"),
|
||||
SingleLang("MangaLionz", "https://mangalionz.com", "ar"),
|
||||
SingleLang("Manga Lord", "https://mangalord.com", "en"),
|
||||
SingleLang("Manganelo.link", "https://manganelo.link", "en", className = "ManganeloLink"),
|
||||
SingleLang("Manga Nine", "https://manganine.com", "en"),
|
||||
SingleLang("Manga-Online.co", "https://www.manga-online.co", "th", className = "MangaOnlineCo"),
|
||||
SingleLang("Mangas Origines", "https://mangas-origines.fr", "fr" , true),
|
||||
SingleLang("Manga Diyari", "https://manga-diyari.com", "tr", overrideVersionCode = 1),
|
||||
SingleLang("MangaRave", "https://www.mangarave.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("ManhwaLive", "https://manhwa.live", "en", overrideVersionCode = 1),
|
||||
SingleLang("Manga Read", "https://mangaread.co", "en"),
|
||||
SingleLang("MangaRead.org", "https://www.mangaread.org", "en", className = "MangaReadOrg"),
|
||||
SingleLang("Mangareceh", "https://mangareceh.id", "id"),
|
||||
SingleLang("Manga Rock Team", "https://mangarockteam.com", "en"),
|
||||
SingleLang("Manga Rocky", "https://mangarocky.com", "en"),
|
||||
SingleLang("MangaRoma", "https://mangaroma.com", "en"),
|
||||
SingleLang("Manga-Scantrad", "https://manga-scantrad.net", "fr", className = "MangaScantrad"),
|
||||
SingleLang("MangaSco", "https://mangasco.com", "en"),
|
||||
SingleLang("MangaSpark", "https://mangaspark.com", "ar"),
|
||||
SingleLang("Manga Starz", "https://mangastarz.com", "ar"),
|
||||
SingleLang("MangaStein", "https://mangastein.com", "tr"),
|
||||
SingleLang("Mangasushi", "https://mangasushi.net", "en", overrideVersionCode = 1),
|
||||
SingleLang("Manga SY", "https://www.mangasy.com", "en"),
|
||||
SingleLang("MangaTeca", "https://www.mangateca.com", "pt-BR"),
|
||||
SingleLang("Manga Too", "https://mangatoo.com/", "en"),
|
||||
SingleLang("Manga Turf", "https://mangaturf.com", "en"),
|
||||
SingleLang("MangaTX", "https://mangatx.com", "en"),
|
||||
SingleLang("Mangauptocats", "https://mangauptocats.online", "th"),
|
||||
SingleLang("MangaUS", "https://mangaus.xyz", "en", overrideVersionCode = 1),
|
||||
SingleLang("Manga Weebs", "https://mangaweebs.in", "en"),
|
||||
SingleLang("MangaWT", "https://mangawt.com", "tr"),
|
||||
SingleLang("MangaYaku", "https://mangayaku.my.id", "id"),
|
||||
SingleLang("MangaYosh", "https://mangayosh.xyz", "id"),
|
||||
MultiLang("Mangazuki.club", "https://mangazuki.club", listOf("ja", "ko"),
|
||||
className = "MangazukiClubFactory"),
|
||||
SingleLang("Mangazuki.me", "https://mangazuki.me", "en", className = "MangazukiMe"),
|
||||
SingleLang("Mangazuki.online", "http://mangazukinew.online", "en", className = "MangazukiOnline"),
|
||||
SingleLang("Mangceh", "https://mangceh.com", "id", isNsfw = true),
|
||||
SingleLang("ManhuaBox", "https://manhuabox.net", "en"),
|
||||
SingleLang("Manhua ES", "https://manhuaes.com", "en", overrideVersionCode = 4),
|
||||
SingleLang("ManhuaFast", "https://manhuafast.com", "en"),
|
||||
SingleLang("Manhuaga", "https://manhuaga.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Manhua Plus", "https://manhuaplus.com", "en", overrideVersionCode = 3),
|
||||
SingleLang("Manhuas.net", "https://manhuas.net", "en", className = "Manhuasnet"),
|
||||
SingleLang("Manhuas World", "https://manhuasworld.com", "en"),
|
||||
SingleLang("Manhua SY", "https://www.manhuasy.com", "en"),
|
||||
SingleLang("ManhuaUS", "https://manhuaus.com", "en", overrideVersionCode = 2),
|
||||
SingleLang("Manhwa Raw", "https://manhwaraw.com", "ko"),
|
||||
SingleLang("Manhwatop", "https://manhwatop.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Manwahentai.me", "https://manhwahentai.me", "en", className = "ManwahentaiMe", isNsfw = true),
|
||||
SingleLang("Manhwa.club", "https://manhwa.club", "en", className="ManwhaClub", overrideVersionCode = 2), // wrong class name for backward compatibility
|
||||
SingleLang("ManyToon", "https://manytoon.com", "en"),
|
||||
SingleLang("ManyToonClub", "https://manytoon.club", "ko"),
|
||||
SingleLang("ManyToon.me", "https://manytoon.me", "en", className = "ManyToonMe"),
|
||||
SingleLang("Mark Scans", "https://markscans.online", "pt-BR"),
|
||||
SingleLang("MG Komik", "https://mgkomik.my.id", "id"),
|
||||
SingleLang("Milftoon", "https://milftoon.xxx", "en", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Miracle Scans", "https://miraclescans.com", "en"),
|
||||
SingleLang("Mixed Manga", "https://mixedmanga.com", "en"),
|
||||
SingleLang("MMScans", "https://mm-scans.com/", "en", overrideVersionCode = 1),
|
||||
SingleLang("Mundo Wuxia", "https://mundowuxia.com", "es"),
|
||||
SingleLang("Mystical Merries", "https://mysticalmerries.com", "en"),
|
||||
SingleLang("Nazarick Scans", "https://nazarickscans.com", "en"),
|
||||
SingleLang("NeatManga", "https://neatmanga.com", "en"),
|
||||
SingleLang("NekoBreaker", "https://nekobreaker.com", "pt-BR"),
|
||||
SingleLang("NekoScan", "https://nekoscan.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Neox Scanlator", "https://neoxscans.net", "pt-BR", overrideVersionCode = 1),
|
||||
SingleLang("Night Comic", "https://www.nightcomic.com", "en"),
|
||||
SingleLang("Niji Translations", "https://niji-translations.com", "ar"),
|
||||
SingleLang("Ninjavi", "https://ninjavi.com", "ar", overrideVersionCode = 1),
|
||||
SingleLang("Nitro Scans", "https://nitroscans.com", "en"),
|
||||
SingleLang("Off Scan", "https://offscan.top", "pt-BR"),
|
||||
SingleLang("مانجا اولاو", "https://olaoe.giize.com", "ar", className = "OlaoeManga"),
|
||||
SingleLang("OnManga", "https://onmanga.com", "en"),
|
||||
SingleLang("Origami Orpheans", "https://origami-orpheans.com.br", "pt-BR"),
|
||||
SingleLang("Painful Nightz Scan", "https://painfulnightzscan.com", "en"),
|
||||
SingleLang("Pojok Manga", "https://pojokmanga.com", "id", overrideVersionCode = 1),
|
||||
SingleLang("PornComix", "https://www.porncomixonline.net", "en", isNsfw = true),
|
||||
SingleLang("Prime Manga", "https://primemanga.com", "en"),
|
||||
SingleLang("Projeto Scanlator", "https://projetoscanlator.com", "pt-BR"),
|
||||
SingleLang("QueensManga ملكات المانجا", "https://queensmanga.com", "ar", className = "QueensManga"),
|
||||
SingleLang("Raider Scans", "https://raiderscans.com", "en"),
|
||||
SingleLang("Random Translations", "https://randomtranslations.com", "en"),
|
||||
SingleLang("RawDEX", "https://rawdex.net", "ko", isNsfw = true),
|
||||
SingleLang("Raw Mangas", "https://rawmangas.net", "ja", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("ReadManhua", "https://readmanhua.net", "en", overrideVersionCode = 2),
|
||||
SingleLang("Renascence Scans (Renascans)", "https://new.renascans.com", "en", className = "RenaScans"),
|
||||
SingleLang("Rüya Manga", "https://www.ruyamanga.com", "tr", className = "RuyaManga"),
|
||||
SingleLang("S2Manga", "https://s2manga.com", "en"),
|
||||
SingleLang("SamuraiScan", "https://samuraiscan.com", "es"),
|
||||
SingleLang("Sekte Doujin", "https://sektedoujin.xyz", "id", isNsfw = true),
|
||||
SingleLang("Shield Manga", "https://shieldmanga.club", "en", overrideVersionCode = 2),
|
||||
SingleLang("Shinzoo Scan", "https://shinzooscan.xyz", "pt-BR", overrideVersionCode = 1),
|
||||
SingleLang("ShoujoHearts", "https://shoujohearts.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("SISI GELAP", "https://sisigelap.club/", "id"),
|
||||
SingleLang("SiXiang Scans", "http://www.sixiangscans.com", "en"),
|
||||
SingleLang("Siyahmelek", "https://siyahmelek.com", "tr", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Skymanga", "https://skymanga.co", "en"),
|
||||
SingleLang("Sleepy Translations", "https://sleepytranslations.com/", "en"),
|
||||
SingleLang("SocialWeebs", "https://socialweebs.in/", "en"),
|
||||
SingleLang("SoloScanlation", "https://soloscanlation.site", "en"),
|
||||
SingleLang("Spooky Scanlations", "https://spookyscanlations.xyz", "es"),
|
||||
SingleLang("StageComics", "https://stagecomics.com", "pt-BR"),
|
||||
SingleLang("TheTopComic", "https://thetopcomic.com", "en"),
|
||||
SingleLang("365Manga", "https://365manga.com", "en", className = "ThreeSixtyFiveManga"),
|
||||
SingleLang("ToonGod", "https://www.toongod.com", "en"),
|
||||
SingleLang("Toonily", "https://toonily.com", "en", isNsfw = true, overrideVersionCode = 2),
|
||||
SingleLang("Toonily.net", "https://toonily.net", "en", isNsfw = true, className = "ToonilyNet", overrideVersionCode = 1),
|
||||
SingleLang("ToonPoint", "https://toonpoint.com", "en"),
|
||||
SingleLang("Top Manhua", "https://topmanhua.com", "en"),
|
||||
SingleLang("TritiniaScans", "https://tritinia.com", "en"),
|
||||
SingleLang("TruyenTranhAudio.com", "https://truyentranhaudio.com", "vi", className = "TruyenTranhAudioCom"),
|
||||
SingleLang("TruyenTranhAudio.online", "https://truyentranhaudio.online", "vi", className = "TruyenTranhAudioOnline"),
|
||||
SingleLang("Tsubaki No Scan", "https://tsubakinoscan.com", "fr"),
|
||||
SingleLang("Tsundoku Traducoes", "https://tsundokutraducoes.com.br", "pt-br"),
|
||||
SingleLang("Türkçe Manga", "https://turkcemanga.com", "tr", className = "TurkceManga"),
|
||||
SingleLang("Twilight Scans", "https://twilightscans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Uyuyan Balik", "https://uyuyanbalik.com/", "tr"),
|
||||
SingleLang("Vanguard Bun", "https://vanguardbun.com/", "en"),
|
||||
SingleLang("Wakascan", "https://wakascan.com", "fr"),
|
||||
SingleLang("War Queen Scan", "https://wqscan.com", "pt-BR"),
|
||||
SingleLang("WebNovel", "https://webnovel.live", "en", className = "WebNovelLive", overrideVersionCode = 1),
|
||||
SingleLang("WebToonily", "https://webtoonily.com", "en"),
|
||||
SingleLang("WebtoonUK", "https://webtoon.uk", "en"),
|
||||
SingleLang("WebtoonXYZ", "https://www.webtoon.xyz", "en"),
|
||||
SingleLang("WeScans", "https://wescans.xyz", "en"),
|
||||
SingleLang("WoopRead", "https://woopread.com", "en"),
|
||||
SingleLang("WuxiaWorld", "https://wuxiaworld.site", "en"),
|
||||
SingleLang("Yaoi Toshokan", "https://yaoitoshokan.com.br", "pt-BR", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Yokai Jump", "https://yokaijump.fr", "fr"),
|
||||
SingleLang("Yuri Verso", "https://yuri.live", "pt-BR"),
|
||||
SingleLang("Zin Translator", "https://zinmanga.com", "en"),
|
||||
SingleLang("ZManga", "https://zmanga.org", "es"),
|
||||
SingleLang("Sleeping Knight Scans", "https://skscans.com", "en", overrideVersionCode = 2),
|
||||
MultiLang("Leviatan Scans", "https://leviatanscans.com", listOf("en", "es"),
|
||||
className = "LeviatanScansFactory", overrideVersionCode = 3),
|
||||
SingleLang("Manga1st", "https://manga1st.com", "en"),
|
||||
SingleLang("Manga1st.online", "https://manga1st.online", "en", className = "MangaFirstOnline"),
|
||||
SingleLang("Imperfect Comics", "https://imperfectcomic.com", "en"),
|
||||
SingleLang("Ookamii Manga", "https://manga.ookamii.xyz/", "en"),
|
||||
SingleLang("Mortals Groove", "https://mortalsgroove.com", "en"),
|
||||
SingleLang("Cervo Scanlator", "https://cervoscan.xyz", "pt-BR"),
|
||||
SingleLang("Comic Star", "https://comicstar.org", "en"),
|
||||
SingleLang("Reset Scans", "https://reset-scans.com", "en", overrideVersionCode = 2),
|
||||
SingleLang("XuN Scans", "https://reader.xunscans.xyz", "en"),
|
||||
SingleLang("Sani-Go", "https://sani-go.net", "ar", className = "SaniGo"),
|
||||
SingleLang("Random Scan", "https://randomscan.online", "pt-BR", overrideVersionCode = 1),
|
||||
SingleLang("Fukushuu no Yuusha", "https://fny-scantrad.com", "fr"),
|
||||
SingleLang("Three Queens Scanlator", "https://tqscan.com.br", "pt-BR"),
|
||||
SingleLang("Winter Scan", "https://winterscan.com.br", "pt-BR"),
|
||||
SingleLang("Little Monster Scan", "https://littlemonsterscan.com.br", "pt-BR"),
|
||||
SingleLang("Wonderland", "https://landwebtoons.site", "pt-BR"),
|
||||
SingleLang("Pornwha", "https://pornwha.com", "en", isNsfw = true),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MadaraGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,385 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// Based off of Mangakakalot 1.2.8
|
||||
abstract class MangaBox(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val dateformat: SimpleDateFormat = SimpleDateFormat("MMM-dd-yy", Locale.ENGLISH)
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", baseUrl) // for covers
|
||||
|
||||
open val popularUrlPath = "manga_list?type=topview&category=all&state=all&page="
|
||||
|
||||
open val latestUrlPath = "manga_list?type=latest&category=all&state=all&page="
|
||||
|
||||
open val simpleQueryPath = "search/"
|
||||
|
||||
override fun popularMangaSelector() = "div.truyen-list > div.list-truyen-item-wrap"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/$popularUrlPath$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/$latestUrlPath$page", headers)
|
||||
}
|
||||
|
||||
protected fun mangaFromElement(element: Element, urlSelector: String = "h3 a"): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select(urlSelector).first().let {
|
||||
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img").first().attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = mangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = mangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.group_page, div.group-page a:not([href]) + a:not(:contains(Last))"
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank() && getAdvancedGenreFilters().isEmpty()) {
|
||||
GET("$baseUrl/$simpleQueryPath${normalizeSearchQuery(query)}?page=$page", headers)
|
||||
} else {
|
||||
val url = HttpUrl.parse(baseUrl)!!.newBuilder()
|
||||
if (getAdvancedGenreFilters().isNotEmpty()) {
|
||||
url.addPathSegment("advanced_search")
|
||||
url.addQueryParameter("page", page.toString())
|
||||
url.addQueryParameter("keyw", normalizeSearchQuery(query))
|
||||
var genreInclude = ""
|
||||
var genreExclude = ""
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is KeywordFilter -> filter.toUriPart()?.let { url.addQueryParameter("keyt", it) }
|
||||
is SortFilter -> url.addQueryParameter("orby", filter.toUriPart())
|
||||
is StatusFilter -> url.addQueryParameter("sts", filter.toUriPart())
|
||||
is AdvGenreFilter -> {
|
||||
filter.state.forEach { if (it.isIncluded()) genreInclude += "_${it.id}" }
|
||||
filter.state.forEach { if (it.isExcluded()) genreExclude += "_${it.id}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("g_i", genreInclude)
|
||||
url.addQueryParameter("g_e", genreExclude)
|
||||
} else {
|
||||
url.addPathSegment("manga_list")
|
||||
url.addQueryParameter("page", page.toString())
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SortFilter -> url.addQueryParameter("type", filter.toUriPart())
|
||||
is StatusFilter -> url.addQueryParameter("state", filter.toUriPart())
|
||||
is GenreFilter -> url.addQueryParameter("category", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
}
|
||||
GET(url.toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = ".panel_story_list .story_item"
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = mangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = "a.page_select + a:not(.page_last), a.page-select + a:not(.page-last)"
|
||||
|
||||
open val mangaDetailsMainSelector = "div.manga-info-top, div.panel-story-info"
|
||||
|
||||
open val thumbnailSelector = "div.manga-info-pic img, span.info-image img"
|
||||
|
||||
open val descriptionSelector = "div#noidungm, div#panel-story-info-description"
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
if (manga.url.startsWith("http")) {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
|
||||
private fun checkForRedirectMessage(document: Document) {
|
||||
if (document.select("body").text().startsWith("REDIRECT :"))
|
||||
throw Exception("Source URL has changed")
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select(mangaDetailsMainSelector).firstOrNull()?.let { infoElement ->
|
||||
title = infoElement.select("h1, h2").first().text()
|
||||
author = infoElement.select("li:contains(author) a, td:containsOwn(author) + td").text()
|
||||
status = parseStatus(infoElement.select("li:contains(status), td:containsOwn(status) + td").text())
|
||||
genre = infoElement.select("div.manga-info-top li:contains(genres)").firstOrNull()
|
||||
?.select("a")?.joinToString { it.text() } // kakalot
|
||||
?: infoElement.select("td:containsOwn(genres) + td a").joinToString { it.text() } // nelo
|
||||
} ?: checkForRedirectMessage(document)
|
||||
description = document.select(descriptionSelector)?.firstOrNull()?.ownText()
|
||||
?.replace("""^$title summary:\s""".toRegex(), "")
|
||||
?.replace("""<\s*br\s*/?>""".toRegex(), "\n")
|
||||
?.replace("<[^>]*>".toRegex(), "")
|
||||
thumbnail_url = document.select(thumbnailSelector).attr("abs:src")
|
||||
|
||||
// add alternative name to manga description
|
||||
document.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not()) {
|
||||
description += when {
|
||||
description!!.isEmpty() -> altName + it
|
||||
else -> "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val altNameSelector = ".story-alternative, tr:has(.info-alternative) h2"
|
||||
open val altName = "Alternative Name" + ": "
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
if (manga.url.startsWith("http")) {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
return super.chapterListRequest(manga)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
|
||||
return document.select(chapterListSelector())
|
||||
.map { chapterFromElement(it) }
|
||||
.also { if (it.isEmpty()) checkForRedirectMessage(document) }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.chapter-list div.row, ul.row-content-chapter li"
|
||||
|
||||
protected open val alternateChapterDateSelector = String()
|
||||
|
||||
private fun Element.selectDateFromElement(): Element {
|
||||
val defaultChapterDateSelector = "span"
|
||||
return this.select(defaultChapterDateSelector).lastOrNull() ?: this.select(alternateChapterDateSelector).last()
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.select("a").let {
|
||||
url = it.attr("abs:href").substringAfter(baseUrl) // intentionally not using setUrlWithoutDomain
|
||||
name = it.text()
|
||||
scanlator = HttpUrl.parse(it.attr("abs:href"))!!.host() // show where chapters are actually from
|
||||
}
|
||||
date_upload = parseChapterDate(element.selectDateFromElement().text(), scanlator!!) ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String, host: String): Long? {
|
||||
return if ("ago" in date) {
|
||||
val value = date.split(' ')[0].toIntOrNull()
|
||||
val cal = Calendar.getInstance()
|
||||
when {
|
||||
value != null && "min" in date -> cal.apply { add(Calendar.MINUTE, value * -1) }
|
||||
value != null && "hour" in date -> cal.apply { add(Calendar.HOUR_OF_DAY, value * -1) }
|
||||
value != null && "day" in date -> cal.apply { add(Calendar.DATE, value * -1) }
|
||||
else -> null
|
||||
}?.timeInMillis
|
||||
} else {
|
||||
try {
|
||||
if (host.contains("manganelo", ignoreCase = true)) {
|
||||
// Nelo's date format
|
||||
SimpleDateFormat("MMM dd,yy", Locale.ENGLISH).parse(date)
|
||||
} else {
|
||||
dateformat.parse(date)
|
||||
}
|
||||
} catch (e: ParseException) {
|
||||
null
|
||||
}?.time
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
if (chapter.url.startsWith("http")) {
|
||||
return GET(chapter.url, headers)
|
||||
}
|
||||
return super.pageListRequest(chapter)
|
||||
}
|
||||
|
||||
open val pageListSelector = "div#vungdoc img, div.container-chapter-reader img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListSelector)
|
||||
// filter out bad elements for mangakakalots
|
||||
.filterNot { it.attr("src").endsWith("log") }
|
||||
.mapIndexed { i, element ->
|
||||
val url = element.attr("abs:src").let { src ->
|
||||
if (src.startsWith("https://convert_image_digi.mgicdn.com")) {
|
||||
"https://images.weserv.nl/?url=" + src.substringAfter("//")
|
||||
} else {
|
||||
src
|
||||
}
|
||||
}
|
||||
Page(i, document.location(), url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
return GET(page.imageUrl!!, headersBuilder().set("Referer", page.url).build())
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Based on change_alias JS function from Mangakakalot's website
|
||||
@SuppressLint("DefaultLocale")
|
||||
open fun normalizeSearchQuery(query: String): String {
|
||||
var str = query.toLowerCase()
|
||||
str = str.replace("[àáạảãâầấậẩẫăằắặẳẵ]".toRegex(), "a")
|
||||
str = str.replace("[èéẹẻẽêềếệểễ]".toRegex(), "e")
|
||||
str = str.replace("[ìíịỉĩ]".toRegex(), "i")
|
||||
str = str.replace("[òóọỏõôồốộổỗơờớợởỡ]".toRegex(), "o")
|
||||
str = str.replace("[ùúụủũưừứựửữ]".toRegex(), "u")
|
||||
str = str.replace("[ỳýỵỷỹ]".toRegex(), "y")
|
||||
str = str.replace("đ".toRegex(), "d")
|
||||
str = str.replace("""!|@|%|\^|\*|\(|\)|\+|=|<|>|\?|/|,|\.|:|;|'| |"|&|#|\[|]|~|-|$|_""".toRegex(), "_")
|
||||
str = str.replace("_+_".toRegex(), "_")
|
||||
str = str.replace("""^_+|_+$""".toRegex(), "")
|
||||
return str
|
||||
}
|
||||
|
||||
override fun getFilterList() = if (getAdvancedGenreFilters().isNotEmpty()) {
|
||||
FilterList(
|
||||
KeywordFilter(getKeywordFilters()),
|
||||
SortFilter(getSortFilters()),
|
||||
StatusFilter(getStatusFilters()),
|
||||
AdvGenreFilter(getAdvancedGenreFilters())
|
||||
)
|
||||
} else {
|
||||
FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
SortFilter(getSortFilters()),
|
||||
StatusFilter(getStatusFilters()),
|
||||
GenreFilter(getGenreFilters())
|
||||
)
|
||||
}
|
||||
|
||||
private class KeywordFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Keyword search ", vals)
|
||||
private class SortFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Order by", vals)
|
||||
private class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
||||
private class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Category", vals)
|
||||
|
||||
// For advanced search, specifically tri-state genres
|
||||
private class AdvGenreFilter(vals: List<AdvGenre>) : Filter.Group<AdvGenre>("Category", vals)
|
||||
class AdvGenre(val id: String?, name: String) : Filter.TriState(name)
|
||||
|
||||
// keyt query parameter
|
||||
private fun getKeywordFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair(null, "Everything"),
|
||||
Pair("title", "Title"),
|
||||
Pair("alternative", "Alt title"),
|
||||
Pair("author", "Author")
|
||||
)
|
||||
|
||||
private fun getSortFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("latest", "Latest"),
|
||||
Pair("newest", "Newest"),
|
||||
Pair("topview", "Top read")
|
||||
)
|
||||
|
||||
open fun getStatusFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("all", "ALL"),
|
||||
Pair("completed", "Completed"),
|
||||
Pair("ongoing", "Ongoing"),
|
||||
Pair("drop", "Dropped")
|
||||
)
|
||||
|
||||
open fun getGenreFilters(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair("all", "ALL"),
|
||||
Pair("2", "Action"),
|
||||
Pair("3", "Adult"),
|
||||
Pair("4", "Adventure"),
|
||||
Pair("6", "Comedy"),
|
||||
Pair("7", "Cooking"),
|
||||
Pair("9", "Doujinshi"),
|
||||
Pair("10", "Drama"),
|
||||
Pair("11", "Ecchi"),
|
||||
Pair("12", "Fantasy"),
|
||||
Pair("13", "Gender bender"),
|
||||
Pair("14", "Harem"),
|
||||
Pair("15", "Historical"),
|
||||
Pair("16", "Horror"),
|
||||
Pair("45", "Isekai"),
|
||||
Pair("17", "Josei"),
|
||||
Pair("44", "Manhua"),
|
||||
Pair("43", "Manhwa"),
|
||||
Pair("19", "Martial arts"),
|
||||
Pair("20", "Mature"),
|
||||
Pair("21", "Mecha"),
|
||||
Pair("22", "Medical"),
|
||||
Pair("24", "Mystery"),
|
||||
Pair("25", "One shot"),
|
||||
Pair("26", "Psychological"),
|
||||
Pair("27", "Romance"),
|
||||
Pair("28", "School life"),
|
||||
Pair("29", "Sci fi"),
|
||||
Pair("30", "Seinen"),
|
||||
Pair("31", "Shoujo"),
|
||||
Pair("32", "Shoujo ai"),
|
||||
Pair("33", "Shounen"),
|
||||
Pair("34", "Shounen ai"),
|
||||
Pair("35", "Slice of life"),
|
||||
Pair("36", "Smut"),
|
||||
Pair("37", "Sports"),
|
||||
Pair("38", "Supernatural"),
|
||||
Pair("39", "Tragedy"),
|
||||
Pair("40", "Webtoons"),
|
||||
Pair("41", "Yaoi"),
|
||||
Pair("42", "Yuri")
|
||||
)
|
||||
|
||||
// To be overridden if using tri-state genres
|
||||
protected open fun getAdvancedGenreFilters(): List<AdvGenre> = emptyList()
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String?, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangabox
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MangaBoxGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "mangabox"
|
||||
|
||||
override val themeClass = "MangaBox"
|
||||
|
||||
override val baseVersionCode: Int = 2
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Mangakakalot", "https://mangakakalot.com", "en"),
|
||||
SingleLang("Manganelo", "https://manganelo.com", "en"),
|
||||
SingleLang("Mangabat", "https://m.mangabat.com", "en", overrideVersionCode = 4),
|
||||
SingleLang("Mangakakalots (unoriginal)", "https://mangakakalots.com", "en", className = "Mangakakalots", pkgName = "mangakakalots"),
|
||||
SingleLang("Mangairo", "https://h.mangairo.com", "en", overrideVersionCode = 3),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MangaBoxGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangacatalog
|
||||
|
||||
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.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
|
||||
// Based On the original manga maniac source
|
||||
// MangaCatalog is a network of sites for single franshise sites
|
||||
|
||||
abstract class MangaCatalog(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
open val sourceList = listOf(
|
||||
Pair("$name", "$baseUrl")
|
||||
).sortedBy { it.first }.distinctBy { it.second }
|
||||
|
||||
// Info
|
||||
|
||||
override val supportsLatest: Boolean = false
|
||||
|
||||
// Popular
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return Observable.just(MangasPage(sourceList.map { popularMangaFromPair(it.first, it.second) }, false))
|
||||
}
|
||||
private fun popularMangaFromPair(name: String, sourceurl: String): SManga = SManga.create().apply {
|
||||
title = name
|
||||
url = sourceurl
|
||||
}
|
||||
override fun popularMangaRequest(page: Int): Request = throw Exception("Not used")
|
||||
override fun popularMangaNextPageSelector(): String? = throw Exception("Not used")
|
||||
override fun popularMangaSelector(): String = throw Exception("Not used")
|
||||
override fun popularMangaFromElement(element: Element) = throw Exception("Not used")
|
||||
|
||||
// Latest
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw Exception("Not used")
|
||||
override fun latestUpdatesNextPageSelector(): String? = throw Exception("Not used")
|
||||
override fun latestUpdatesSelector(): String = throw Exception("Not used")
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = throw Exception("Not used")
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw Exception("No Search Function")
|
||||
override fun searchMangaNextPageSelector() = throw Exception("Not used")
|
||||
override fun searchMangaSelector() = throw Exception("Not used")
|
||||
override fun searchMangaFromElement(element: Element) = throw Exception("Not used")
|
||||
|
||||
// Get Override
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(chapter.url, headers)
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = SManga.create().apply {
|
||||
val info = document.select("div.bg-bg-secondary > div.px-6 > div.flex-col").text()
|
||||
title = document.select("div.container > h1").text()
|
||||
description = if ("Description" in info) info.substringAfter("Description").trim() else info
|
||||
thumbnail_url = document.select("div.flex > img").attr("src")
|
||||
}
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector(): String = "div.w-full > div.bg-bg-secondary > div.grid"
|
||||
override fun chapterFromElement(element: Element): SChapter = SChapter.create().apply {
|
||||
val name1 = element.select(".col-span-3 > a").text()
|
||||
val name2 = element.select(".text-xs:not(a)").text()
|
||||
if (name2 == ""){
|
||||
name = name1
|
||||
} else {
|
||||
name = "$name1 - $name2"
|
||||
}
|
||||
url = element.select(".col-span-3 > a").attr("abs:href")
|
||||
date_upload = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = mutableListOf<Page>().apply {
|
||||
document.select(".text-center img,.img_container img").forEach { img ->
|
||||
add(Page(size, "", img.attr("src")))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw Exception("Not Used")
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangacatalog
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MangaCatalogGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "mangacatalog"
|
||||
|
||||
override val themeClass = "MangaCatalog"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Read Boku no Hero Academia/My Hero Academia Manga", "https://ww6.readmha.com", "en", className = "ReadBokuNoHeroAcademiaMyHeroAcademiaManga"),
|
||||
SingleLang("Read One-Punch Man Manga Online", "https://ww3.readopm.com", "en", className = "ReadOnePunchManMangaOnlineTwo", pkgName = "readonepunchmanmangaonlinetwo"), //exact same name as the one in mangamainac extension
|
||||
SingleLang("Read Tokyo Ghoul Re & Tokyo Ghoul Manga Online", "https://ww8.tokyoghoulre.com", "en", className = "ReadTokyoGhoulReTokyoGhoulMangaOnline"),
|
||||
SingleLang("Read Nanatsu no Taizai/7 Deadly Sins Manga Online", "https://ww3.read7deadlysins.com", "en", className = "ReadNanatsuNoTaizai7DeadlySinsMangaOnline"),
|
||||
SingleLang("Read Kaguya-sama Manga Online", "https://ww1.readkaguyasama.com", "en", className = "ReadKaguyaSamaMangaOnline"),
|
||||
SingleLang("Read Jujutsu Kaisen Manga Online", "https://ww1.readjujutsukaisen.com", "en"),
|
||||
SingleLang("Read Tower of God Manhwa/Manga Online", "https://ww1.readtowerofgod.com", "en", className = "ReadTowerOfGodManhwaMangaOnline"),
|
||||
SingleLang("Read Hunter x Hunter Manga Online", "https://ww2.readhxh.com", "en"),
|
||||
SingleLang("Read Solo Leveling Manga/Manhwa Online", "https://readsololeveling.org", "en", className = "ReadSoloLevelingMangaManhwaOnline"),
|
||||
SingleLang("Read The Promised Neverland Manga Online", "https://ww3.readneverland.com", "en"),
|
||||
SingleLang("Read Attack on Titan/Shingeki no Kyojin Manga", "https://ww7.readsnk.com", "en", className = "ReadAttackOnTitanShingekiNoKyojinManga")
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MangaCatalogGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,280 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangadventure
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build.VERSION
|
||||
import eu.kanade.tachiyomi.extensions.BuildConfig
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import rx.Observable
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* MangAdventure base source.
|
||||
*
|
||||
* @property categories the available manga categories of the site.
|
||||
*/
|
||||
abstract class MangAdventure(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
val categories: List<String> = DEFAULT_CATEGORIES
|
||||
) : HttpSource() {
|
||||
|
||||
override val versionId = 1
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
/** The full URL to the site's API. */
|
||||
open val apiUrl by lazy { "$baseUrl/api/v$versionId" }
|
||||
|
||||
/**
|
||||
* A user agent representing Tachiyomi.
|
||||
* Includes the user's Android version
|
||||
* and the current extension version.
|
||||
*/
|
||||
private val userAgent = "Mozilla/5.0 " +
|
||||
"(Android ${VERSION.RELEASE}; Mobile) " +
|
||||
"Tachiyomi/${BuildConfig.VERSION_NAME}"
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add("User-Agent", userAgent)
|
||||
add("Referer", baseUrl)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
GET("$apiUrl/releases/", headers)
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET("$apiUrl/series/${chapter.path}", headers)
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$apiUrl/series/${manga.slug}/", headers)
|
||||
|
||||
// Workaround to allow "Open in browser" to use the real URL
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
client.newCall(chapterListRequest(manga)).asObservableSuccess()
|
||||
.map { mangaDetailsParse(it).apply { initialized = true } }
|
||||
|
||||
// Return the real URL for "Open in browser"
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(manga.url, headers)
|
||||
|
||||
override fun searchMangaRequest(
|
||||
page: Int,
|
||||
query: String,
|
||||
filters: FilterList
|
||||
): Request {
|
||||
val uri = Uri.parse("$apiUrl/series/").buildUpon()
|
||||
if (query.startsWith(SLUG_QUERY)) {
|
||||
uri.appendQueryParameter("slug", query.substringAfter(SLUG_QUERY))
|
||||
return GET(uri.toString(), headers)
|
||||
}
|
||||
uri.appendQueryParameter("q", query)
|
||||
val cat = mutableListOf<String>()
|
||||
filters.forEach {
|
||||
when (it) {
|
||||
is Person -> uri.appendQueryParameter("author", it.state)
|
||||
is Status -> uri.appendQueryParameter("status", it.string())
|
||||
is CategoryList -> cat.addAll(
|
||||
it.state.mapNotNull { c ->
|
||||
Uri.encode(c.optString())
|
||||
}
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
return GET("$uri&categories=${cat.joinToString(",")}", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
JSONArray(response.asString()).run {
|
||||
MangasPage(
|
||||
(0 until length()).map {
|
||||
val obj = getJSONObject(it)
|
||||
SManga.create().apply {
|
||||
url = obj.getString("url")
|
||||
title = obj.getString("title")
|
||||
thumbnail_url = obj.getString("cover")
|
||||
// A bit of a hack to sort by date
|
||||
description = httpDateToTimestamp(
|
||||
obj.getJSONObject("latest_chapter").getString("date")
|
||||
).toString()
|
||||
}
|
||||
}.sortedByDescending(SManga::description),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response) =
|
||||
JSONObject(response.asString()).getJSONObject("volumes").run {
|
||||
keys().asSequence().flatMap { vol ->
|
||||
val chapters = getJSONObject(vol)
|
||||
chapters.keys().asSequence().map { ch ->
|
||||
SChapter.create().fromJSON(
|
||||
chapters.getJSONObject(ch).also {
|
||||
it.put("volume", vol)
|
||||
it.put("chapter", ch)
|
||||
}
|
||||
)
|
||||
}
|
||||
}.toList().reversed()
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
SManga.create().fromJSON(JSONObject(response.asString()))
|
||||
|
||||
override fun pageListParse(response: Response) =
|
||||
JSONObject(response.asString()).run {
|
||||
val url = getString("url")
|
||||
val root = getString("pages_root")
|
||||
val arr = getJSONArray("pages_list")
|
||||
(0 until arr.length()).map {
|
||||
Page(it, "$url${it + 1}", "$root${arr.getString(it)}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response) =
|
||||
JSONArray(response.asString()).run {
|
||||
MangasPage(
|
||||
(0 until length()).map {
|
||||
SManga.create().fromJSON(getJSONObject(it))
|
||||
}.sortedBy(SManga::title),
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
override fun getFilterList() =
|
||||
FilterList(Person(), Status(), CategoryList())
|
||||
|
||||
override fun fetchPopularManga(page: Int) =
|
||||
fetchSearchManga(page, "", FilterList())
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
|
||||
override fun imageUrlParse(response: Response) =
|
||||
throw UnsupportedOperationException(
|
||||
"This method should not be called!"
|
||||
)
|
||||
|
||||
companion object {
|
||||
/** The possible statuses of a manga. */
|
||||
private val STATUSES = arrayOf("Any", "Completed", "Ongoing")
|
||||
|
||||
/** Manga categories from MangAdventure `categories.xml` fixture. */
|
||||
internal val DEFAULT_CATEGORIES = listOf(
|
||||
"4-Koma",
|
||||
"Action",
|
||||
"Adventure",
|
||||
"Comedy",
|
||||
"Doujinshi",
|
||||
"Drama",
|
||||
"Ecchi",
|
||||
"Fantasy",
|
||||
"Gender Bender",
|
||||
"Harem",
|
||||
"Hentai",
|
||||
"Historical",
|
||||
"Horror",
|
||||
"Josei",
|
||||
"Martial Arts",
|
||||
"Mecha",
|
||||
"Mystery",
|
||||
"Psychological",
|
||||
"Romance",
|
||||
"School Life",
|
||||
"Sci-Fi",
|
||||
"Seinen",
|
||||
"Shoujo",
|
||||
"Shoujo Ai",
|
||||
"Shounen",
|
||||
"Shounen Ai",
|
||||
"Slice of Life",
|
||||
"Smut",
|
||||
"Sports",
|
||||
"Supernatural",
|
||||
"Tragedy",
|
||||
"Yaoi",
|
||||
"Yuri"
|
||||
)
|
||||
|
||||
/** Query to search by manga slug. */
|
||||
internal const val SLUG_QUERY = "slug:"
|
||||
|
||||
/**
|
||||
* The HTTP date format specified in
|
||||
* [RFC 1123](https://tools.ietf.org/html/rfc1123#page-55).
|
||||
*/
|
||||
private const val HTTP_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"
|
||||
|
||||
/**
|
||||
* Converts a date in the [HTTP_DATE] format to a Unix timestamp.
|
||||
*
|
||||
* @param date The date to convert.
|
||||
* @return The timestamp of the date.
|
||||
*/
|
||||
internal fun httpDateToTimestamp(date: String) =
|
||||
SimpleDateFormat(HTTP_DATE, Locale.US).parse(date)?.time ?: 0L
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter representing the status of a manga.
|
||||
*
|
||||
* @constructor Creates a [Filter.Select] object with [STATUSES].
|
||||
*/
|
||||
inner class Status : Filter.Select<String>("Status", STATUSES) {
|
||||
/** Returns the [state] as a string. */
|
||||
fun string() = values[state].toLowerCase(Locale.ENGLISH)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter representing a manga category.
|
||||
*
|
||||
* @property name The display name of the category.
|
||||
* @constructor Creates a [Filter.TriState] object using [name].
|
||||
*/
|
||||
inner class Category(name: String) : Filter.TriState(name) {
|
||||
/** Returns the [state] as a string, or null if [isIgnored]. */
|
||||
fun optString() = when (state) {
|
||||
STATE_INCLUDE -> name.toLowerCase(Locale(lang))
|
||||
STATE_EXCLUDE -> "-" + name.toLowerCase(Locale(lang))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter representing the [categories][Category] of a manga.
|
||||
*
|
||||
* @constructor Creates a [Filter.Group] object with categories.
|
||||
*/
|
||||
inner class CategoryList : Filter.Group<Category>(
|
||||
"Categories", categories.map(::Category)
|
||||
)
|
||||
|
||||
/**
|
||||
* Filter representing the name of an author or artist.
|
||||
*
|
||||
* @constructor Creates a [Filter.Text] object.
|
||||
*/
|
||||
inner class Person : Filter.Text("Author/Artist")
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangadventure
|
||||
|
||||
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 `{baseUrl}/reader/{slug}`
|
||||
* intents and redirects them to the main Tachiyomi process.
|
||||
*/
|
||||
class MangAdventureActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
intent?.data?.pathSegments?.takeIf { it.size > 1 }?.let {
|
||||
try {
|
||||
startActivity(
|
||||
Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", MangAdventure.SLUG_QUERY + it[1])
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
)
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Log.e("MangAdventureActivity", ex.message, ex)
|
||||
}
|
||||
} ?: Log.e(
|
||||
"MangAdventureActivity",
|
||||
"Failed to parse URI from intent: $intent"
|
||||
)
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangadventure
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.text.DecimalFormat
|
||||
|
||||
/** Returns the body of a response as a `String`. */
|
||||
fun Response.asString(): String = body()!!.string()
|
||||
|
||||
/**
|
||||
* Formats the number according to [fmt].
|
||||
*
|
||||
* @param fmt A [DecimalFormat] string.
|
||||
* @return A string representation of the number.
|
||||
*/
|
||||
fun Number.format(fmt: String): String = DecimalFormat(fmt).format(this)
|
||||
|
||||
/**
|
||||
* Joins each value of a given [field] of the array using [sep].
|
||||
*
|
||||
* @param field The index of a [JSONArray].
|
||||
* When its type is [String], it is treated as the key of a [JSONObject].
|
||||
* @param sep The separator used to join the array.
|
||||
* @return The joined string, or `null` if the array is empty.
|
||||
*/
|
||||
fun JSONArray.joinField(field: Int, sep: String = ", ") =
|
||||
length().takeIf { it != 0 }?.run {
|
||||
(0 until this).joinToString(sep) {
|
||||
getJSONArray(it).getString(field)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins each value of a given [field] of the array using [sep].
|
||||
*
|
||||
* @param field The key of a [JSONObject].
|
||||
* @param sep The separator used to join the array.
|
||||
* @return The joined string, or `null` if the array is empty.
|
||||
*/
|
||||
fun JSONArray.joinField(field: String, sep: String = ", ") =
|
||||
length().takeIf { it != 0 }?.run {
|
||||
(0 until this).joinToString(sep) {
|
||||
getJSONObject(it).getString(field)
|
||||
}
|
||||
}
|
||||
|
||||
/** The slug of a manga. */
|
||||
val SManga.slug: String
|
||||
get() = Uri.parse(url).lastPathSegment!!
|
||||
|
||||
/**
|
||||
* Creates a [SManga] by parsing a [JSONObject].
|
||||
*
|
||||
* @param obj The object containing the manga info.
|
||||
*/
|
||||
fun SManga.fromJSON(obj: JSONObject) = apply {
|
||||
url = obj.getString("url")
|
||||
title = obj.getString("title")
|
||||
description = obj.getString("description")
|
||||
thumbnail_url = obj.getString("cover")
|
||||
author = obj.getJSONArray("authors").joinField(0)
|
||||
artist = obj.getJSONArray("artists").joinField(0)
|
||||
genre = obj.getJSONArray("categories").joinField("name")
|
||||
status = if (obj.getBoolean("completed"))
|
||||
SManga.COMPLETED else SManga.ONGOING
|
||||
}
|
||||
|
||||
/** The unique path of a chapter. */
|
||||
val SChapter.path: String
|
||||
get() = url.substringAfter("/reader/")
|
||||
|
||||
/**
|
||||
* Creates a [SChapter] by parsing a [JSONObject].
|
||||
*
|
||||
* @param obj The object containing the chapter info.
|
||||
*/
|
||||
fun SChapter.fromJSON(obj: JSONObject) = apply {
|
||||
url = obj.getString("url")
|
||||
chapter_number = obj.optString("chapter", "-1").toFloat()
|
||||
date_upload = MangAdventure.httpDateToTimestamp(obj.getString("date"))
|
||||
scanlator = obj.getJSONArray("groups").joinField("name", " & ")
|
||||
name = obj.optString(
|
||||
"full_title",
|
||||
buildString {
|
||||
obj.optInt("volume").let { if (it != 0) append("Vol. $it, ") }
|
||||
append("Ch. ${chapter_number.format("#.#")}: ")
|
||||
append(obj.getString("title"))
|
||||
}
|
||||
)
|
||||
if (obj.getBoolean("final")) name += " [END]"
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangadventure
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
/** [MangAdventure] source generator. */
|
||||
class MangAdventureGenerator : ThemeSourceGenerator {
|
||||
override val themePkg = "mangadventure"
|
||||
|
||||
override val themeClass = "MangAdventure"
|
||||
|
||||
override val baseVersionCode = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Arc-Relight", "https://arc-relight.com", "en", className = "ArcRelight"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun main(args: Array<String>) = MangAdventureGenerator().createAll()
|
||||
}
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangamainac
|
||||
|
||||
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 okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.Calendar
|
||||
|
||||
|
||||
// Based On TCBScans sources
|
||||
// MangaManiac is a network of sites built by Animemaniac.co.
|
||||
|
||||
abstract class MangaMainac(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
) : ParsedHttpSource() {
|
||||
override val supportsLatest = false
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
// popular
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET(baseUrl)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "#page"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select(".mangainfo_body > img").attr("src")
|
||||
manga.url = "" //element.select("#primary-menu .menu-item:first-child").attr("href")
|
||||
manga.title = element.select(".intro_content h2").text()
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
// latest
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException()
|
||||
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException()
|
||||
|
||||
// search
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
|
||||
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
|
||||
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
|
||||
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException()
|
||||
|
||||
// manga details
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
val info = document.select(".intro_content").text()
|
||||
thumbnail_url = document.select(".mangainfo_body > img").attr("src")
|
||||
title = document.select(".intro_content h2").text()
|
||||
author = if ("Author" in info) substringextract(info, "Author(s):", "Released") else null
|
||||
artist = author
|
||||
genre = if ("Genre" in info) substringextract(info, "Genre(s):", "Status") else null
|
||||
status = parseStatus(document.select(".intro_content").text())
|
||||
description = if ("Description" in info) info.substringAfter("Description:").trim() else null
|
||||
}
|
||||
|
||||
private fun substringextract(text: String, start: String, end: String): String = text.substringAfter(start).substringBefore(end).trim()
|
||||
|
||||
private fun parseStatus(element: String): Int = when {
|
||||
element.toLowerCase().contains("ongoing (pub") -> SManga.ONGOING
|
||||
element.toLowerCase().contains("completed (pub") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// chapters
|
||||
override fun chapterListSelector() = "table.chap_tab tr"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a").first()
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = element.select("a").text()
|
||||
chapter.date_upload = element.select("#time i").last()?.text()?.let { parseChapterDate(it) }
|
||||
?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val dateWords: List<String> = date.split(" ")
|
||||
if (dateWords.size == 3) {
|
||||
val timeAgo = Integer.parseInt(dateWords[0])
|
||||
val dates: Calendar = Calendar.getInstance()
|
||||
when {
|
||||
dateWords[1].contains("minute") -> {
|
||||
dates.add(Calendar.MINUTE, -timeAgo)
|
||||
}
|
||||
dateWords[1].contains("hour") -> {
|
||||
dates.add(Calendar.HOUR_OF_DAY, -timeAgo)
|
||||
}
|
||||
dateWords[1].contains("day") -> {
|
||||
dates.add(Calendar.DAY_OF_YEAR, -timeAgo)
|
||||
}
|
||||
dateWords[1].contains("week") -> {
|
||||
dates.add(Calendar.WEEK_OF_YEAR, -timeAgo)
|
||||
}
|
||||
dateWords[1].contains("month") -> {
|
||||
dates.add(Calendar.MONTH, -timeAgo)
|
||||
}
|
||||
dateWords[1].contains("year") -> {
|
||||
dates.add(Calendar.YEAR, -timeAgo)
|
||||
}
|
||||
}
|
||||
return dates.timeInMillis
|
||||
}
|
||||
return 0L
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val chapterList = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
|
||||
return if (hasCountdown(chapterList[0]))
|
||||
chapterList.subList(1, chapterList.size)
|
||||
else
|
||||
chapterList
|
||||
}
|
||||
|
||||
private fun hasCountdown(chapter: SChapter): Boolean {
|
||||
val document = client.newCall(
|
||||
GET(
|
||||
baseUrl + chapter.url,
|
||||
headersBuilder().build()
|
||||
)
|
||||
).execute().asJsoup()
|
||||
|
||||
return document
|
||||
.select("iframe[src^=https://free.timeanddate.com/countdown/]")
|
||||
.isNotEmpty()
|
||||
}
|
||||
|
||||
// pages
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
var i = 0
|
||||
document.select(".container .img_container center img").forEach { element ->
|
||||
val url = element.attr("src")
|
||||
i++
|
||||
if (url.isNotEmpty()) {
|
||||
pages.add(Page(i, "", url))
|
||||
}
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document) = ""
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangamainac
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MangaMainacGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "mangamainac"
|
||||
|
||||
override val themeClass = "MangaMainac"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Read Boku No Hero Academia Manga Online", "https://w23.readheroacademia.com/", "en"),
|
||||
SingleLang("Read One Punch Man Manga Online", "https://w17.readonepunchman.net/", "en"),
|
||||
SingleLang("Read One Webcomic Manga Online", "https://w1.onewebcomic.net/", "en"),
|
||||
SingleLang("Read Solo Leveling", "https://w3.sololeveling.net/", "en"),
|
||||
SingleLang("Read Jojolion", "https://readjojolion.com/", "en"),
|
||||
SingleLang("Hajime no Ippo Manga", "https://readhajimenoippo.com/", "en"),
|
||||
SingleLang("Read Berserk Manga Online", "https://berserkmanga.net/", "en"),
|
||||
SingleLang("Read Kaguya-sama: Love is War", "https://kaguyasama.net/", "en", className = "ReadKaguyaSamaLoveIsWar", pkgName = "readkaguyasamaloveiswar"),
|
||||
SingleLang("Read Domestic Girlfriend Manga", "https://domesticgirlfriend.net/", "en"),
|
||||
SingleLang("Read Black Clover Manga", "https://w1.blackclovermanga2.com/", "en"),
|
||||
SingleLang("TCB Scans", "https://onepiecechapters.com/", "en", overrideVersionCode = 2),
|
||||
SingleLang("Read Shingeki no Kyojin Manga", "https://readshingekinokyojin.com/", "en"),
|
||||
SingleLang("Read Nanatsu no Taizai Manga", "https://w1.readnanatsutaizai.net/", "en"),
|
||||
SingleLang("Read Rent a Girlfriend Manga", "https://kanojo-okarishimasu.com/", "en"),
|
||||
//Sites that are currently down from my end, should be rechecked by some one else at some point
|
||||
//
|
||||
//SingleLang("", "https://5-toubunnohanayome.net/", "en"), //Down at time of creating this generator
|
||||
//SingleLang("", "http://beastars.net/", "en"), //Down at time of creating this generator
|
||||
//SingleLang("", "https://neverlandmanga.net/", "en"), //Down at time of creating this generator
|
||||
//SingleLang("", "https://ww1.readhunterxhunter.net/", "en"), //Down at time of creating this generator
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MangaMainacGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,333 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangasproject
|
||||
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
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.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class MangasProject(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : HttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Sometimes the site is slow.
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("Referer", baseUrl)
|
||||
.add("User-Agent", USER_AGENT)
|
||||
|
||||
// Use internal headers to allow "Open in WebView" to work.
|
||||
private fun sourceHeadersBuilder(): Headers.Builder = headersBuilder()
|
||||
.add("Accept", ACCEPT_JSON)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
protected val sourceHeaders: Headers by lazy { sourceHeadersBuilder().build() }
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/home/most_read?page=$page&type=", sourceHeaders)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = response.asJsonObject()
|
||||
|
||||
val popularMangas = result["most_read"].array
|
||||
.map { popularMangaItemParse(it.obj) }
|
||||
|
||||
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 10
|
||||
|
||||
return MangasPage(popularMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["serie_name"].string
|
||||
thumbnail_url = obj["cover"].string
|
||||
url = obj["link"].string
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/home/releases?page=$page&type=", sourceHeaders)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val result = response.asJsonObject()
|
||||
|
||||
val latestMangas = result["releases"].array
|
||||
.map { latestMangaItemParse(it.obj) }
|
||||
|
||||
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 5
|
||||
|
||||
return MangasPage(latestMangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["image"].string
|
||||
url = obj["link"].string
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val form = FormBody.Builder()
|
||||
.add("search", query)
|
||||
.build()
|
||||
|
||||
val newHeaders = sourceHeadersBuilder()
|
||||
.add("Content-Length", form.contentLength().toString())
|
||||
.add("Content-Type", form.contentType().toString())
|
||||
.build()
|
||||
|
||||
return POST("$baseUrl/lib/search/series.json", newHeaders, form)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val result = response.asJsonObject()
|
||||
|
||||
// If "series" have boolean false value, then it doesn't have results.
|
||||
if (!result["series"]!!.isJsonArray)
|
||||
return MangasPage(emptyList(), false)
|
||||
|
||||
val searchMangas = result["series"].array
|
||||
.map { searchMangaItemParse(it.obj) }
|
||||
|
||||
return MangasPage(searchMangas, false)
|
||||
}
|
||||
|
||||
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["cover"].string
|
||||
url = obj["link"].string
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val seriesData = document.select("#series-data")
|
||||
|
||||
val isCompleted = seriesData.select("span.series-author i.complete-series").first() != null
|
||||
|
||||
// Check if the manga was removed by the publisher.
|
||||
val seriesBlocked = document.select("div.series-blocked-img").first()
|
||||
|
||||
val seriesAuthors = document.select("div#series-data span.series-author").text()
|
||||
.substringAfter("Completo")
|
||||
.substringBefore("+")
|
||||
.split("&")
|
||||
.groupBy(
|
||||
{ it.contains("(Arte)") },
|
||||
{
|
||||
it.replace(" (Arte)", "")
|
||||
.trim()
|
||||
.split(", ")
|
||||
.reversed()
|
||||
.joinToString(" ")
|
||||
}
|
||||
)
|
||||
|
||||
return SManga.create().apply {
|
||||
thumbnail_url = seriesData.select("div.series-img > div.cover > img").attr("src")
|
||||
description = seriesData.select("span.series-desc span").text()
|
||||
|
||||
status = parseStatus(seriesBlocked, isCompleted)
|
||||
author = seriesAuthors[false]?.joinToString(", ") ?: author
|
||||
artist = seriesAuthors[true]?.joinToString(", ") ?: author
|
||||
genre = seriesData.select("div#series-data ul.tags li")
|
||||
.joinToString { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(seriesBlocked: Element?, isCompleted: Boolean) = when {
|
||||
seriesBlocked != null -> SManga.LICENSED
|
||||
isCompleted -> SManga.COMPLETED
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
if (manga.status != SManga.LICENSED)
|
||||
return super.fetchChapterList(manga)
|
||||
|
||||
return Observable.error(Exception(MANGA_REMOVED))
|
||||
}
|
||||
|
||||
private fun chapterListRequestPaginated(mangaUrl: String, id: String, page: Int): Request {
|
||||
val newHeaders = sourceHeadersBuilder()
|
||||
.set("Referer", baseUrl + mangaUrl)
|
||||
.build()
|
||||
|
||||
return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", newHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val licensedMessage = document.select("div.series-blocked-img").firstOrNull()
|
||||
|
||||
if (licensedMessage != null) {
|
||||
// If the manga is licensed and has been removed from the source,
|
||||
// the extension will not fetch the chapters, even if they are returned
|
||||
// by the API. This is just to mimic the website behavior.
|
||||
throw Exception(MANGA_REMOVED)
|
||||
}
|
||||
|
||||
val mangaUrl = response.request().url().toString().replace(baseUrl, "")
|
||||
val mangaId = mangaUrl.substringAfterLast("/")
|
||||
var page = 1
|
||||
|
||||
var chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, page)
|
||||
var result = client.newCall(chapterListRequest).execute().asJsonObject()
|
||||
|
||||
if (!result["chapters"]!!.isJsonArray)
|
||||
return emptyList()
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
||||
while (result["chapters"]!!.isJsonArray) {
|
||||
chapters += result["chapters"].array
|
||||
.flatMap { chapterListItemParse(it.obj) }
|
||||
.toMutableList()
|
||||
|
||||
chapterListRequest = chapterListRequestPaginated(mangaUrl, mangaId, ++page)
|
||||
result = client.newCall(chapterListRequest).execute().asJsonObject()
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
private fun chapterListItemParse(obj: JsonObject): List<SChapter> {
|
||||
val chapterName = obj["chapter_name"]!!.string
|
||||
|
||||
return obj["releases"].obj.entrySet().map {
|
||||
val release = it.value.obj
|
||||
|
||||
SChapter.create().apply {
|
||||
name = "Cap. ${obj["number"].string}" +
|
||||
(if (chapterName == "") "" else " - $chapterName")
|
||||
date_upload = obj["date_created"].string.substringBefore("T").toDate()
|
||||
scanlator = release["scanlators"]!!.array
|
||||
.mapNotNull { scanObj -> scanObj.obj["name"].string.ifEmpty { null } }
|
||||
.sorted()
|
||||
.joinToString()
|
||||
url = release["link"].string
|
||||
chapter_number = obj["number"].string.toFloatOrNull() ?: -1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.add("Accept", ACCEPT)
|
||||
.add("Accept-Language", ACCEPT_LANGUAGE)
|
||||
.set("Referer", "$baseUrl/home")
|
||||
.build()
|
||||
|
||||
return GET(baseUrl + chapter.url, newHeaders)
|
||||
}
|
||||
|
||||
private fun pageListApiRequest(chapterUrl: String, token: String): Request {
|
||||
val newHeaders = sourceHeadersBuilder()
|
||||
.set("Referer", chapterUrl)
|
||||
.build()
|
||||
|
||||
val id = chapterUrl
|
||||
.substringBeforeLast("/")
|
||||
.substringAfterLast("/")
|
||||
|
||||
return GET("$baseUrl/leitor/pages/$id.json?key=$token", newHeaders)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val document = response.asJsoup()
|
||||
val readerToken = getReaderToken(document) ?: throw Exception(TOKEN_NOT_FOUND)
|
||||
val chapterUrl = getChapterUrl(response)
|
||||
|
||||
val apiRequest = pageListApiRequest(chapterUrl, readerToken)
|
||||
val apiResponse = client.newCall(apiRequest).execute().asJsonObject()
|
||||
|
||||
return apiResponse["images"].array
|
||||
.filter { it.string.startsWith("http") }
|
||||
.mapIndexed { i, obj -> Page(i, chapterUrl, obj.string) }
|
||||
}
|
||||
|
||||
open fun getChapterUrl(response: Response): String {
|
||||
return response.request().url().toString()
|
||||
}
|
||||
|
||||
protected open fun getReaderToken(document: Document): String? {
|
||||
return document.select("script[src*=\"reader.\"]").firstOrNull()
|
||||
?.attr("abs:src")
|
||||
?.let { HttpUrl.parse(it) }
|
||||
?.queryParameter("token")
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
|
||||
|
||||
override fun imageUrlParse(response: Response): String = ""
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", page.url)
|
||||
.build()
|
||||
|
||||
return GET(page.imageUrl!!, newHeaders)
|
||||
}
|
||||
|
||||
private fun Response.asJsonObject(): JsonObject {
|
||||
if (!isSuccessful) {
|
||||
throw Exception("HTTP error ${code()}")
|
||||
}
|
||||
|
||||
return JSON_PARSER.parse(body()!!.string()).obj
|
||||
}
|
||||
|
||||
private fun String.toDate(): Long {
|
||||
return try {
|
||||
DATE_FORMATTER.parse(this)?.time ?: 0L
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," +
|
||||
"image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
|
||||
private const val ACCEPT_JSON = "application/json, text/javascript, */*; q=0.01"
|
||||
private const val ACCEPT_LANGUAGE = "pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7,es;q=0.6,gl;q=0.5"
|
||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36"
|
||||
|
||||
private val JSON_PARSER by lazy { JsonParser() }
|
||||
|
||||
private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) }
|
||||
|
||||
private const val MANGA_REMOVED = "Mangá licenciado e removido pela fonte."
|
||||
private const val TOKEN_NOT_FOUND = "Não foi possível obter o token de leitura."
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.mangasproject
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class MangasProjectGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "mangasproject"
|
||||
|
||||
override val themeClass = "MangasProject"
|
||||
|
||||
override val baseVersionCode: Int = 2
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("Leitor.net", "https://leitor.net", "pt-BR", className = "LeitorNet", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Mangá Livre", "https://mangalivre.net", "pt-BR", className = "MangaLivre", isNsfw = true, overrideVersionCode = 1),
|
||||
SingleLang("Toonei", "https://toonei.com", "pt-BR", isNsfw = true, overrideVersionCode = 1),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
MangasProjectGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,365 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.nepnep
|
||||
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.github.salomonbrys.kotson.get
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonElement
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
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 gson = GsonBuilder().setLenient().create()
|
||||
|
||||
private lateinit var directory: List<JsonElement>
|
||||
|
||||
// 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 directoryFromResponse(response: Response): String {
|
||||
return response.asJsoup().select("script:containsData(MainFunction)").first().data()
|
||||
.substringAfter("vm.Directory = ").substringBefore("vm.GetIntValue").trim()
|
||||
.replace(";", " ")
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
directory = gson.fromJson<JsonArray>(directoryFromResponse(response))
|
||||
.sortedByDescending { it["v"].string }
|
||||
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]["s"].string
|
||||
url = "/manga/${directory[i]["i"].string}"
|
||||
thumbnail_url = "https://cover.nep.li/cover/${directory[i]["i"].string}.jpg"
|
||||
}
|
||||
)
|
||||
}
|
||||
return MangasPage(mangas, endRange < directory.lastIndex)
|
||||
}
|
||||
|
||||
// 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 = gson.fromJson<JsonArray>(directoryFromResponse(response))
|
||||
.sortedByDescending { it["lt"].string }
|
||||
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 = gson.fromJson<JsonArray>(directoryFromResponse(response))
|
||||
.filter {
|
||||
it["s"].string.contains(trimmedQuery, ignoreCase = true) or
|
||||
it["al"].asJsonArray.any { altName -> altName.string.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[sortBy].string }
|
||||
} else {
|
||||
directory.sortedByDescending { it[sortBy].string }.reversed()
|
||||
}
|
||||
}
|
||||
is SelectField -> if (filter.state != 0) directory = when (filter.name) {
|
||||
"Scan Status" -> directory.filter { it["ss"].string.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Publish Status" -> directory.filter { it["ps"].string.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Type" -> directory.filter { it["t"].string.contains(filter.values[filter.state], ignoreCase = true) }
|
||||
"Translation" -> directory.filter { it["o"].string.contains("yes", ignoreCase = true) }
|
||||
else -> directory
|
||||
}
|
||||
is YearField -> if (filter.state.isNotEmpty()) directory = directory.filter { it["y"].string.contains(filter.state) }
|
||||
is AuthorField -> if (filter.state.isNotEmpty()) directory = directory.filter { e -> e["a"].asJsonArray.any { it.string.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (genres.isNotEmpty()) genres.map { genre -> directory = directory.filter { e -> e["g"].asJsonArray.any { it.string.contains(genre, ignoreCase = true) } } }
|
||||
if (genresNo.isNotEmpty()) genresNo.map { genre -> directory = directory.filterNot { e -> e["g"].asJsonArray.any { it.string.contains(genre, ignoreCase = true) } } }
|
||||
|
||||
return parseDirectory(1)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// 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.isEmpty().not() && it !="N/A") {
|
||||
description += when {
|
||||
description!!.isEmpty() -> altName + it
|
||||
else -> "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toStatus() = when {
|
||||
this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("Complete", ignoreCase = true) -> SManga.COMPLETED
|
||||
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 {
|
||||
val a = e.substring(1, e.length - 1).let { if (cleanString) it.replace(chapterImageRegex, "") else it }
|
||||
val b = e.substring(e.length - 1).toInt()
|
||||
return if (b == 0) {
|
||||
a
|
||||
} else {
|
||||
"$a.$b"
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val vmChapters = response.asJsoup().select("script:containsData(MainFunction)").first().data()
|
||||
.substringAfter("vm.Chapters = ").substringBefore(";")
|
||||
|
||||
return gson.fromJson<JsonArray>(vmChapters).map { json ->
|
||||
val indexChapter = json["Chapter"].string
|
||||
SChapter.create().apply {
|
||||
name = json["ChapterName"].nullString.let { if (it.isNullOrEmpty()) "${json["Type"].string} ${chapterImage(indexChapter, true)}" else it }
|
||||
url = "/read-online/" + response.request().url().toString().substringAfter("/manga/") + chapterURLEncode(indexChapter)
|
||||
date_upload = try {
|
||||
json["Date"].nullString?.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.select("script:containsData(MainFunction)").first().data()
|
||||
val curChapter = gson.fromJson<JsonElement>(script.substringAfter("vm.CurChapter = ").substringBefore(";"))
|
||||
|
||||
val pageTotal = curChapter["Page"].string.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["Directory"].string
|
||||
.let { if (it.isEmpty()) "" else "$it/" }
|
||||
val path = "$host/manga/$titleURI/$seasonURI"
|
||||
|
||||
val chNum = chapterImage(curChapter["Chapter"].string)
|
||||
|
||||
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("Not used")
|
||||
|
||||
// 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")
|
||||
)
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.nepnep
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class NepNepGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "nepnep"
|
||||
|
||||
override val themeClass = "NepNep"
|
||||
|
||||
override val baseVersionCode: Int = 3
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("MangaSee", "https://mangasee123.com", "en", overrideVersionCode = 20),
|
||||
SingleLang("MangaLife", "https://manga4life.com", "en", overrideVersionCode = 16),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
NepNepGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,307 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.paprika
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
abstract class Paprika(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/popular-manga?page=$page")
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.media"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select("a:has(h4)").let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a[rel=next]"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/latest-manga?page=$page")
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/search?q=$query&page=$page")
|
||||
} else {
|
||||
val url = HttpUrl.parse("$baseUrl/mangas/")!!.newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> url.addPathSegment(filter.toUriPart())
|
||||
is OrderFilter -> url.addQueryParameter("orderby", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
GET(url.toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Manga details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
title = document.select("div.manga-detail h1").text()
|
||||
thumbnail_url = document.select("div.manga-detail img").attr("abs:src")
|
||||
document.select("div.media-body p").html().split("<br>").forEach {
|
||||
with(Jsoup.parse(it).text()) {
|
||||
when {
|
||||
this.startsWith("Author") -> author = this.substringAfter(":").trim()
|
||||
this.startsWith("Artist") -> artist = this.substringAfter(":").trim()
|
||||
this.startsWith("Genre") -> genre = this.substringAfter(":").trim().replace(";", ",")
|
||||
this.startsWith("Status") -> status = this.substringAfter(":").trim().toStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
description = document.select("div.manga-content p").joinToString("\n") { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.toStatus() = when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
|
||||
this.contains("Completed", ignoreCase = true) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
/**
|
||||
* This theme has 3 chapter blocks: latest chapters with dates, all chapters without dates, and upcoming chapters
|
||||
* Avoid parsing the upcoming chapters and filter out duplicate chapters
|
||||
*/
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val mangaTitle = document.select("div.manga-detail h1").text()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, mangaTitle) }.distinctBy { it.url }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.total-chapter:has(h2) li"
|
||||
|
||||
// never called
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
throw Exception("unreachable code was reached!")
|
||||
}
|
||||
|
||||
open fun chapterFromElement(element: Element, mangaTitle: String): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.select("a").let {
|
||||
name = it.text().substringAfter("$mangaTitle ")
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
date_upload = element.select("div.small").firstOrNull()?.text().toDate()
|
||||
}
|
||||
}
|
||||
|
||||
private val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) }
|
||||
|
||||
fun String?.toDate(): Long {
|
||||
this ?: return 0L
|
||||
return try {
|
||||
when {
|
||||
this.contains("yesterday", ignoreCase = true) -> Calendar.getInstance().apply { add(Calendar.DAY_OF_MONTH, -1) }.timeInMillis
|
||||
this.contains("ago", ignoreCase = true) -> {
|
||||
val trimmedDate = this.substringBefore(" ago").removeSuffix("s").split(" ")
|
||||
val num = trimmedDate[0].toIntOrNull() ?: 1 // for "an hour ago"
|
||||
val calendar = Calendar.getInstance()
|
||||
when (trimmedDate[1]) {
|
||||
"day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -num) }
|
||||
"hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -num) }
|
||||
"minute" -> calendar.apply { add(Calendar.MINUTE, -num) }
|
||||
"second" -> calendar.apply { add(Calendar.SECOND, -num) }
|
||||
else -> null
|
||||
}?.timeInMillis ?: 0L
|
||||
}
|
||||
else ->
|
||||
SimpleDateFormat("MMM d yy", Locale.US)
|
||||
.parse("${this.substringBefore(",")} $currentYear")?.time ?: 0
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("#arraydata").text().split(",").mapIndexed { i, url ->
|
||||
Page(i, "", url)
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
OrderFilter(getOrderList()),
|
||||
GenreFilter(getGenreList())
|
||||
)
|
||||
|
||||
class OrderFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Category", vals)
|
||||
|
||||
private fun getOrderList() = arrayOf(
|
||||
Pair("Views", "2"),
|
||||
Pair("Latest", "3"),
|
||||
Pair("A-Z", "1")
|
||||
)
|
||||
|
||||
class GenreFilter(vals: Array<Pair<String, String>>) : UriPartFilter("Category", vals)
|
||||
|
||||
private fun getGenreList() = arrayOf(
|
||||
Pair("4 koma", "4-koma"),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adaptation", "adaptation"),
|
||||
Pair("Adult", "adult"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Aliens", "aliens"),
|
||||
Pair("Animals", "animals"),
|
||||
Pair("Anthology", "anthology"),
|
||||
Pair("Award winning", "award-winning"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Cooking", "cooking"),
|
||||
Pair("Crime", "crime"),
|
||||
Pair("Crossdressing", "crossdressing"),
|
||||
Pair("Delinquents", "delinquents"),
|
||||
Pair("Demons", "demons"),
|
||||
Pair("Doujinshi", "doujinshi"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Fan colored", "fan-colored"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Food", "food"),
|
||||
Pair("Full color", "full-color"),
|
||||
Pair("Game", "game"),
|
||||
Pair("Gender bender", "gender-bender"),
|
||||
Pair("Genderswap", "genderswap"),
|
||||
Pair("Ghosts", "ghosts"),
|
||||
Pair("Gore", "gore"),
|
||||
Pair("Gossip", "gossip"),
|
||||
Pair("Gyaru", "gyaru"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Historical", "historical"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Isekai", "isekai"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Kids", "kids"),
|
||||
Pair("Loli", "loli"),
|
||||
Pair("Lolicon", "lolicon"),
|
||||
Pair("Long strip", "long-strip"),
|
||||
Pair("Mafia", "mafia"),
|
||||
Pair("Magic", "magic"),
|
||||
Pair("Magical girls", "magical-girls"),
|
||||
Pair("Manhwa", "manhwa"),
|
||||
Pair("Martial arts", "martial-arts"),
|
||||
Pair("Mature", "mature"),
|
||||
Pair("Mecha", "mecha"),
|
||||
Pair("Medical", "medical"),
|
||||
Pair("Military", "military"),
|
||||
Pair("Monster girls", "monster-girls"),
|
||||
Pair("Monsters", "monsters"),
|
||||
Pair("Music", "music"),
|
||||
Pair("Mystery", "mystery"),
|
||||
Pair("Ninja", "ninja"),
|
||||
Pair("Office workers", "office-workers"),
|
||||
Pair("Official colored", "official-colored"),
|
||||
Pair("One shot", "one-shot"),
|
||||
Pair("Parody", "parody"),
|
||||
Pair("Philosophical", "philosophical"),
|
||||
Pair("Police", "police"),
|
||||
Pair("Post apocalyptic", "post-apocalyptic"),
|
||||
Pair("Psychological", "psychological"),
|
||||
Pair("Reincarnation", "reincarnation"),
|
||||
Pair("Reverse harem", "reverse-harem"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("Samurai", "samurai"),
|
||||
Pair("School life", "school-life"),
|
||||
Pair("Sci fi", "sci-fi"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shota", "shota"),
|
||||
Pair("Shotacon", "shotacon"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shoujo ai", "shoujo-ai"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("Shounen ai", "shounen-ai"),
|
||||
Pair("Slice of life", "slice-of-life"),
|
||||
Pair("Smut", "smut"),
|
||||
Pair("Space", "space"),
|
||||
Pair("Sports", "sports"),
|
||||
Pair("Super power", "super-power"),
|
||||
Pair("Superhero", "superhero"),
|
||||
Pair("Supernatural", "supernatural"),
|
||||
Pair("Survival", "survival"),
|
||||
Pair("Suspense", "suspense"),
|
||||
Pair("Thriller", "thriller"),
|
||||
Pair("Time travel", "time-travel"),
|
||||
Pair("Toomics", "toomics"),
|
||||
Pair("Traditional games", "traditional-games"),
|
||||
Pair("Tragedy", "tragedy"),
|
||||
Pair("User created", "user-created"),
|
||||
Pair("Vampire", "vampire"),
|
||||
Pair("Vampires", "vampires"),
|
||||
Pair("Video games", "video-games"),
|
||||
Pair("Virtual reality", "virtual-reality"),
|
||||
Pair("Web comic", "web-comic"),
|
||||
Pair("Webtoon", "webtoon"),
|
||||
Pair("Wuxia", "wuxia"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Yuri", "yuri"),
|
||||
Pair("Zombies", "zombies")
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.paprika
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
abstract class PaprikaAlt(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : Paprika(name, baseUrl, lang) {
|
||||
override fun popularMangaSelector() = "div.anipost"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
// Log.d("Paprika", "processing popular element")
|
||||
return SManga.create().apply {
|
||||
element.select("a:has(h3)").let {
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
title = it.text()
|
||||
// Log.d("Paprika", "manga url: $url")
|
||||
// Log.d("Paprika", "manga title: $title")
|
||||
}
|
||||
thumbnail_url = element.select("img").attr("src")
|
||||
// Log.d("Paprika", "manga thumb: $thumbnail_url")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/search?s=$query&post_type=manga&page=$page")
|
||||
} else {
|
||||
val url = HttpUrl.parse("$baseUrl/genres/")!!.newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> url.addPathSegment(filter.toUriPart())
|
||||
is OrderFilter -> url.addQueryParameter("orderby", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
url.addQueryParameter("page", page.toString())
|
||||
GET(url.toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
title = document.select(".animeinfo .rm h1")[0].text()
|
||||
thumbnail_url = document.select(".animeinfo .lm img").attr("abs:src")
|
||||
document.select(".listinfo li").forEach {
|
||||
it.text().apply {
|
||||
when {
|
||||
this.startsWith("Author") -> author = this.substringAfter(":").trim()
|
||||
this.startsWith("Artist") -> artist = this.substringAfter(":").trim().replace(";", ",")
|
||||
this.startsWith("Genre") -> genre = this.substringAfter(":").trim().replace(";", ",")
|
||||
this.startsWith("Status") -> status = this.substringAfter(":").trim().toStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
description = document.select("#noidungm").joinToString("\n") { it.text() }
|
||||
|
||||
// Log.d("Paprika", "mangaDetials")
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val mangaTitle = document.select(".animeinfo .rm h1")[0].text()
|
||||
return document.select(chapterListSelector()).map { chapterFromElement(it, mangaTitle) }.distinctBy { it.url }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = ".animeinfo .rm .cl li"
|
||||
|
||||
// changing the signature to pass the manga title in order to trim the title from chapter titles
|
||||
override fun chapterFromElement(element: Element, mangaTitle: String): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.select(".leftoff").let {
|
||||
name = it.text().substringAfter("$mangaTitle ")
|
||||
setUrlWithoutDomain(it.select("a").attr("href"))
|
||||
}
|
||||
date_upload = element.select(".rightoff").firstOrNull()?.text().toDate()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.paprika
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class PaprikaAltGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "paprika"
|
||||
|
||||
override val themeClass = "PaprikaAlt"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("MangaReader.cc", "http://mangareader.cc/", "en", className = "MangaReaderCC") // more sites in the future might use MangaReader.cc 's overrides as they did in the past
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
PaprikaAltGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.paprika
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class PaprikaGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "paprika"
|
||||
|
||||
override val themeClass = "Paprika"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("MangaStream.xyz", "http://mangastream.xyz", "en", className = "MangaStreamXYZ"),
|
||||
SingleLang("ReadMangaFox", "http://readmangafox.xyz", "en"),
|
||||
// SingleLang("MangaZuki.xyz", "http://mangazuki.xyz", "en", className = "MangaZuki"),
|
||||
// SingleLang("MangaTensei", "http://www.mangatensei.com", "en"),
|
||||
SingleLang("MangaNelos.com", "http://manganelos.com", "en", className = "MangaNelosCom"),
|
||||
SingleLang("MangaDogs.fun", "http://mangadogs.fun", "en", className = "MangaDogsFun"),
|
||||
SingleLang("MangaHere.today", "http://mangahere.today", "en", className = "MangaHereToday"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
PaprikaGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,243 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Header
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Select
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Separator
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
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.model.MangasPage
|
||||
import java.util.Locale
|
||||
import java.util.Calendar
|
||||
|
||||
open class Webtoons(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
open val langCode: String = lang,
|
||||
open val localeForCookie: String = lang,
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = super.client.newBuilder()
|
||||
.cookieJar(
|
||||
object : CookieJar {
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {}
|
||||
override fun loadForRequest(url: HttpUrl): List<Cookie> {
|
||||
return listOf<Cookie>(
|
||||
Cookie.Builder()
|
||||
.domain("www.webtoons.com")
|
||||
.path("/")
|
||||
.name("ageGatePass")
|
||||
.value("true")
|
||||
.name("locale")
|
||||
.value(localeForCookie)
|
||||
.name("needGDPR")
|
||||
.value("false")
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
.build()
|
||||
|
||||
private val day: String
|
||||
get() {
|
||||
return when (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.SUNDAY -> "div._list_SUNDAY"
|
||||
Calendar.MONDAY -> "div._list_MONDAY"
|
||||
Calendar.TUESDAY -> "div._list_TUESDAY"
|
||||
Calendar.WEDNESDAY -> "div._list_WEDNESDAY"
|
||||
Calendar.THURSDAY -> "div._list_THURSDAY"
|
||||
Calendar.FRIDAY -> "div._list_FRIDAY"
|
||||
Calendar.SATURDAY -> "div._list_SATURDAY"
|
||||
else -> {
|
||||
"div"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "not using"
|
||||
|
||||
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.add("Referer", "https://www.webtoons.com/$langCode/")
|
||||
|
||||
protected val mobileHeaders: Headers = super.headersBuilder()
|
||||
.add("Referer", "https://m.webtoons.com")
|
||||
.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val mangas = mutableListOf<SManga>()
|
||||
val document = response.asJsoup()
|
||||
var maxChild = 0
|
||||
|
||||
// For ongoing webtoons rows are ordered by descending popularity, count how many rows there are
|
||||
document.select("div#dailyList > div").forEach { day ->
|
||||
day.select("li").count().let { rowCount ->
|
||||
if (rowCount > maxChild) maxChild = rowCount
|
||||
}
|
||||
}
|
||||
|
||||
// Process each row
|
||||
for (i in 1..maxChild) {
|
||||
document.select("div#dailyList > div li:nth-child($i) a").map { mangas.add(popularMangaFromElement(it)) }
|
||||
}
|
||||
|
||||
// Add completed webtoons, no sorting needed
|
||||
document.select("div.daily_lst.comp li a").map { mangas.add(popularMangaFromElement(it)) }
|
||||
|
||||
return MangasPage(mangas.distinctBy { it.url }, false)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers)
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
|
||||
manga.setUrlWithoutDomain(element.attr("href"))
|
||||
manga.title = element.select("p.subj").text()
|
||||
manga.thumbnail_url = element.select("img").attr("abs:src")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = null
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = null
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/$langCode/search?keyword=$query")?.newBuilder()!!
|
||||
val uriPart = (filters.find { it is SearchType } as? SearchType)?.toUriPart() ?: ""
|
||||
|
||||
url.addQueryParameter("searchType", uriPart)
|
||||
if (uriPart != "WEBTOON" && page > 1) url.addQueryParameter("page", page.toString())
|
||||
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "#content > div.card_wrap.search li a"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = "div.more_area, div.paginate a[onclick] + a"
|
||||
|
||||
open fun parseDetailsThumbnail(document: Document): String? {
|
||||
val picElement = document.select("#content > div.cont_box > div.detail_body")
|
||||
val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb")
|
||||
return discoverPic.select("img").not("[alt='Representative image']").first()?.attr("src") ?: picElement.attr("style")?.substringAfter("url(")?.substringBeforeLast(")")
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val detailElement = document.select("#content > div.cont_box > div.detail_header > div.info")
|
||||
val infoElement = document.select("#_asideDetail")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.author = detailElement.select(".author:nth-of-type(1)").first()?.ownText()
|
||||
manga.artist = detailElement.select(".author:nth-of-type(2)").first()?.ownText() ?: manga.author
|
||||
manga.genre = detailElement.select(".genre").joinToString(", ") { it.text() }
|
||||
manga.description = infoElement.select("p.summary").text()
|
||||
manga.status = infoElement.select("p.day_info").text().orEmpty().let { parseStatus(it) }
|
||||
manga.thumbnail_url = parseDetailsThumbnail(document)
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("UP") -> SManga.ONGOING
|
||||
status.contains("COMPLETED") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = document.select("img").first().attr("src")
|
||||
|
||||
// Filters
|
||||
|
||||
override fun getFilterList(): FilterList {
|
||||
return FilterList(
|
||||
Header("Query can not be blank"),
|
||||
Separator(),
|
||||
SearchType(getOfficialList())
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "ul#_episodeList li[id*=episode]"
|
||||
|
||||
private class SearchType(vals: Array<Pair<String, String>>) : UriPartFilter("Official or Challenge", vals)
|
||||
|
||||
private fun getOfficialList() = arrayOf(
|
||||
Pair("Any", ""),
|
||||
Pair("Official only", "WEBTOON"),
|
||||
Pair("Challenge only", "CHALLENGE")
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select("a")
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = element.select("a > div.row > div.info > p.sub_title > span.ellipsis").text()
|
||||
val select = element.select("a > div.row > div.num")
|
||||
if (select.isNotEmpty()) {
|
||||
chapter.name += " Ch. " + select.text().substringAfter("#")
|
||||
}
|
||||
if (element.select(".ico_bgm").isNotEmpty()) {
|
||||
chapter.name += " ♫"
|
||||
}
|
||||
chapter.date_upload = element.select("a > div.row > div.info > p.date").text()?.let { chapterParseDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
open fun chapterParseDate(date: String): Long {
|
||||
return dateFormat.parse(date)?.time ?: 0
|
||||
}
|
||||
override fun chapterListRequest(manga: SManga) = GET("https://m.webtoons.com" + manga.url, mobileHeaders)
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
|
||||
|
||||
if (pages.isNotEmpty()) { return pages }
|
||||
|
||||
val docString = document.toString()
|
||||
|
||||
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
|
||||
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
|
||||
|
||||
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
|
||||
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
|
||||
|
||||
val motiontoonJson = JSONObject(client.newCall(GET(docUrl, headers)).execute().body()!!.string()).getJSONObject("assets").getJSONObject("image")
|
||||
|
||||
val keys = motiontoonJson.keys().asSequence().toList().filter { it.contains("layer") }
|
||||
|
||||
return keys.mapIndexed { i, key ->
|
||||
Page(i, "", motiontoonPath + motiontoonJson.getString(key))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class WebtoonsGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "webtoons"
|
||||
|
||||
override val themeClass = "Webtoons"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
MultiLang("Webtoons.com", "https://www.webtoons.com", listOf("en", "fr", "es", "id", "th", "zh"), className = "WebtoonsFactory", pkgName = "webtoons", overrideVersionCode = 27),
|
||||
SingleLang("Dongman Manhua", "https://www.dongmanmanhua.cn", "zh")
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
WebtoonsGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
||||
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.util.ArrayList
|
||||
|
||||
open class WebtoonsTranslate (
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val translateLangCode: String
|
||||
) : Webtoons(name, baseUrl, lang) {
|
||||
// popularMangaRequest already returns manga sorted by latest update
|
||||
override val supportsLatest = false
|
||||
|
||||
private val apiBaseUrl = HttpUrl.parse("https://global.apis.naver.com")!!
|
||||
private val mobileBaseUrl = HttpUrl.parse("https://m.webtoons.com")!!
|
||||
private val thumbnailBaseUrl = "https://mwebtoon-phinf.pstatic.net"
|
||||
|
||||
private val pageListUrlPattern = "/lineWebtoon/ctrans/translatedEpisodeDetail_jsonp.json?titleNo=%s&episodeNo=%d&languageCode=%s&teamVersion=%d"
|
||||
|
||||
private val pageSize = 24
|
||||
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
|
||||
.removeAll("Referer")
|
||||
.add("Referer", mobileBaseUrl.toString())
|
||||
|
||||
private fun mangaRequest(page: Int, requeztSize: Int): Request {
|
||||
val url = apiBaseUrl
|
||||
.resolve("/lineWebtoon/ctrans/translatedWebtoons_jsonp.json")!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("orderType", "UPDATE")
|
||||
.addQueryParameter("offset", "${(page - 1) * requeztSize}")
|
||||
.addQueryParameter("size", "$requeztSize")
|
||||
.addQueryParameter("languageCode", translateLangCode)
|
||||
.build()
|
||||
return GET(url.toString(), headers)
|
||||
}
|
||||
|
||||
// Webtoons translations doesn't really have a "popular" sort; just "UPDATE", "TITLE_ASC",
|
||||
// and "TITLE_DESC". Pick UPDATE as the most useful sort.
|
||||
override fun popularMangaRequest(page: Int): Request = mangaRequest(page, pageSize)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val offset = response.request().url().queryParameter("offset")!!.toInt()
|
||||
var totalCount: Int
|
||||
val mangas = mutableListOf<SManga>()
|
||||
|
||||
JSONObject(response.body()!!.string()).let { json ->
|
||||
json.getString("code").let { code ->
|
||||
if (code != "000") throw Exception("Error getting popular manga: error code $code")
|
||||
}
|
||||
|
||||
json.getJSONObject("result").let { results ->
|
||||
totalCount = results.getInt("totalCount")
|
||||
|
||||
results.getJSONArray("titleList").let { array ->
|
||||
for (i in 0 until array.length()) {
|
||||
mangas.add(mangaFromJson(array[i] as JSONObject))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, totalCount > pageSize + offset)
|
||||
}
|
||||
|
||||
private fun mangaFromJson(json: JSONObject): SManga {
|
||||
val relativeThumnailURL = json.getString("thumbnailIPadUrl")
|
||||
?: json.getString("thumbnailMobileUrl")
|
||||
|
||||
return SManga.create().apply {
|
||||
title = json.getString("representTitle")
|
||||
author = json.getString("writeAuthorName")
|
||||
artist = json.getString("pictureAuthorName") ?: author
|
||||
thumbnail_url = if (relativeThumnailURL != null) "$thumbnailBaseUrl$relativeThumnailURL" else null
|
||||
status = SManga.UNKNOWN
|
||||
url = mobileBaseUrl
|
||||
.resolve("/translate/episodeList")!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("titleNo", json.getInt("titleNo").toString())
|
||||
.addQueryParameter("languageCode", translateLangCode)
|
||||
.addQueryParameter("teamVersion", json.optInt("teamVersion", 0).toString())
|
||||
.build()
|
||||
.toString()
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return client.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, query)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't see a search function for Fan Translations, so let's do it client side.
|
||||
* There's 75 webtoons as of 2019/11/21, a hardcoded request of 200 should be a sufficient request
|
||||
* to get all titles, in 1 request, for quite a while
|
||||
*/
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = mangaRequest(page, 200)
|
||||
|
||||
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
||||
val mangas = mutableListOf<SManga>()
|
||||
|
||||
JSONObject(response.body()!!.string()).let { json ->
|
||||
json.getString("code").let { code ->
|
||||
if (code != "000") throw Exception("Error getting manga: error code $code")
|
||||
}
|
||||
|
||||
json.getJSONObject("result").getJSONArray("titleList").let { array ->
|
||||
for (i in 0 until array.length()) {
|
||||
(array[i] as JSONObject).let { jsonManga ->
|
||||
if (jsonManga.getString("representTitle").contains(query, ignoreCase = true))
|
||||
mangas.add(mangaFromJson(jsonManga))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val getMetaProp = fun(property: String): String =
|
||||
document.head().select("meta[property=\"$property\"]").attr("content")
|
||||
var parsedAuthor = getMetaProp("com-linewebtoon:webtoon:author")
|
||||
var parsedArtist = parsedAuthor
|
||||
val authorSplit = parsedAuthor.split(" / ", limit = 2)
|
||||
if (authorSplit.count() > 1) {
|
||||
parsedAuthor = authorSplit[0]
|
||||
parsedArtist = authorSplit[1]
|
||||
}
|
||||
|
||||
return SManga.create().apply {
|
||||
title = getMetaProp("og:title")
|
||||
artist = parsedArtist
|
||||
author = parsedAuthor
|
||||
description = getMetaProp("og:description")
|
||||
status = SManga.UNKNOWN
|
||||
thumbnail_url = getMetaProp("og:image")
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String = throw Exception("Not used")
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used")
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> = throw Exception("Not used")
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val titleNo = HttpUrl.parse(manga.url)!!
|
||||
.queryParameter("titleNo")
|
||||
val chapterUrl = apiBaseUrl
|
||||
.resolve("/lineWebtoon/ctrans/translatedEpisodes_jsonp.json")!!
|
||||
.newBuilder()
|
||||
.addQueryParameter("titleNo", titleNo)
|
||||
.addQueryParameter("languageCode", translateLangCode)
|
||||
.addQueryParameter("offset", "0")
|
||||
.addQueryParameter("limit", "10000")
|
||||
.toString()
|
||||
return GET(chapterUrl, mobileHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapterData = response.body()!!.string()
|
||||
val chapterJson = JSONObject(chapterData)
|
||||
val responseCode = chapterJson.getString("code")
|
||||
if (responseCode != "000") {
|
||||
val message = chapterJson.optString("message", "error code $responseCode")
|
||||
throw Exception("Error getting chapter list: $message")
|
||||
}
|
||||
val results = chapterJson.getJSONObject("result").getJSONArray("episodes")
|
||||
val ret = ArrayList<SChapter>()
|
||||
for (i in 0 until results.length()) {
|
||||
val result = results.getJSONObject(i)
|
||||
if (result.getBoolean("translateCompleted")) {
|
||||
ret.add(parseChapterJson(result))
|
||||
}
|
||||
}
|
||||
ret.reverse()
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun parseChapterJson(obj: JSONObject) = SChapter.create().apply {
|
||||
name = obj.getString("title") + " #" + obj.getString("episodeSeq")
|
||||
chapter_number = obj.getInt("episodeSeq").toFloat()
|
||||
date_upload = obj.getLong("updateYmdt")
|
||||
scanlator = obj.getString("teamVersion")
|
||||
if (scanlator == "0") {
|
||||
scanlator = "(wiki)"
|
||||
}
|
||||
url = String.format(pageListUrlPattern, obj.getInt("titleNo"), obj.getInt("episodeNo"), obj.getString("languageCode"), obj.getInt("teamVersion"))
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(apiBaseUrl.resolve(chapter.url).toString(), headers)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val pageJson = JSONObject(response.body()!!.string())
|
||||
val results = pageJson.getJSONObject("result").getJSONArray("imageInfo")
|
||||
val ret = ArrayList<Page>()
|
||||
for (i in 0 until results.length()) {
|
||||
val result = results.getJSONObject(i)
|
||||
ret.add(Page(i, "", result.getString("imageUrl")))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
override fun getFilterList(): FilterList = FilterList()
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.webtoons
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class WebtoonsTranslateGenerator : ThemeSourceGenerator {
|
||||
override val themePkg = "webtoons"
|
||||
|
||||
override val themeClass = "WebtoonsTranslation"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
MultiLang("Webtoons.com Translations", "https://translate.webtoons.com", listOf("en", "zh-hans", "zh-hant", "th", "id", "fr", "vi", "ru", "ar", "fil", "de", "hi", "it", "ja", "pt-br", "tr", "ms", "pl", "pt", "bg", "da", "nl", "ro", "mn", "el", "lt", "cs", "sv", "bn", "fa", "uk", "es"), className = "WebtoonsTranslateFactory", pkgName = "webtoonstranslate"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
WebtoonsTranslateGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,292 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.wpcomics
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
abstract class WPComics(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("HH:mm - dd/MM/yyyy Z", Locale.US),
|
||||
private val gmtOffset: String? = "+0500"
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0")
|
||||
.add("Referer", baseUrl)
|
||||
|
||||
private fun List<String>.doesInclude(thisWord: String): Boolean = this.any { it.contains(thisWord, ignoreCase = true) }
|
||||
|
||||
// Popular
|
||||
|
||||
open val popularPath = "hot"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/$popularPath" + if (page > 1) "?page=$page" else "", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.items div.item"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select("h3 a").let {
|
||||
title = it.text()
|
||||
setUrlWithoutDomain(it.attr("abs:href"))
|
||||
}
|
||||
thumbnail_url = imageOrNull(element.select("div.image:first-of-type img").first())
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.next-page, a[rel=next]"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET(baseUrl + if (page > 1) "?page=$page" else "", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
protected open val searchPath = "tim-truyen"
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val filterList = filters.let { if (it.isEmpty()) getFilterList() else it }
|
||||
return if (filterList.isEmpty()) {
|
||||
GET("$baseUrl/?s=$query&post_type=comics&page=$page")
|
||||
} else {
|
||||
val url = HttpUrl.parse("$baseUrl/$searchPath")!!.newBuilder()
|
||||
|
||||
filterList.forEach { filter ->
|
||||
when (filter) {
|
||||
is GenreFilter -> filter.toUriPart()?.let { url.addPathSegment(it) }
|
||||
is StatusFilter -> filter.toUriPart()?.let { url.addQueryParameter("status", it) }
|
||||
}
|
||||
}
|
||||
|
||||
url.apply {
|
||||
addQueryParameter("keyword", query)
|
||||
addQueryParameter("page", page.toString())
|
||||
addQueryParameter("sort", "0")
|
||||
}
|
||||
|
||||
GET(url.toString().replace("www.nettruyen.com/tim-truyen?status=2&", "www.nettruyen.com/truyen-full?"), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = "div.items div.item div.image a"
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
title = element.attr("title")
|
||||
setUrlWithoutDomain(element.attr("href"))
|
||||
thumbnail_url = imageOrNull(element.select("img").first())
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select("article#item-detail").let { info ->
|
||||
author = info.select("li.author p.col-xs-8").text()
|
||||
status = info.select("li.status p.col-xs-8").text().toStatus()
|
||||
genre = info.select("li.kind p.col-xs-8 a").joinToString { it.text() }
|
||||
description = info.select("div.detail-content p").text()
|
||||
thumbnail_url = imageOrNull(info.select("div.col-image img").first())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun String?.toStatus(): Int {
|
||||
val ongoingWords = listOf("Ongoing", "Updating", "Đang tiến hành")
|
||||
val completedWords = listOf("Complete", "Hoàn thành")
|
||||
return when {
|
||||
this == null -> SManga.UNKNOWN
|
||||
ongoingWords.doesInclude(this) -> SManga.ONGOING
|
||||
completedWords.doesInclude(this) -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector() = "div.list-chapter li.row:not(.heading)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.select("a").let {
|
||||
name = it.text()
|
||||
setUrlWithoutDomain(it.attr("href"))
|
||||
}
|
||||
date_upload = element.select("div.col-xs-4").text().toDate()
|
||||
}
|
||||
}
|
||||
|
||||
private val currentYear by lazy { Calendar.getInstance(Locale.US)[1].toString().takeLast(2) }
|
||||
|
||||
private fun String?.toDate(): Long {
|
||||
this ?: return 0
|
||||
|
||||
val secondWords = listOf("second", "giây")
|
||||
val minuteWords = listOf("minute", "phút")
|
||||
val hourWords = listOf("hour", "giờ")
|
||||
val dayWords = listOf("day", "ngày")
|
||||
val agoWords = listOf("ago", "trước")
|
||||
|
||||
return try {
|
||||
if (agoWords.any { this.contains(it, ignoreCase = true) }) {
|
||||
val trimmedDate = this.substringBefore(" ago").removeSuffix("s").split(" ")
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
when {
|
||||
dayWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
|
||||
hourWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
|
||||
minuteWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
|
||||
secondWords.doesInclude(trimmedDate[1]) -> calendar.apply { add(Calendar.SECOND, -trimmedDate[0].toInt()) }
|
||||
}
|
||||
|
||||
calendar.timeInMillis
|
||||
} else {
|
||||
(if (gmtOffset == null) this.substringAfterLast(" ") else "$this $gmtOffset").let {
|
||||
// timestamp has year
|
||||
if (Regex("""\d+/\d+/\d\d""").find(it)?.value != null) {
|
||||
dateFormat.parse(it)?.time ?: 0
|
||||
} else {
|
||||
// MangaSum - timestamp sometimes doesn't have year (current year implied)
|
||||
dateFormat.parse("$it/$currentYear")?.time ?: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
// sources sometimes have an image element with an empty attr that isn't really an image
|
||||
open fun imageOrNull(element: Element): String? {
|
||||
fun Element.hasValidAttr(attr: String): Boolean {
|
||||
val regex = Regex("""https?://.*""", RegexOption.IGNORE_CASE)
|
||||
return when {
|
||||
this.attr(attr).isNullOrBlank() -> false
|
||||
this.attr("abs:$attr").matches(regex) -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
return when {
|
||||
element.hasValidAttr("data-original") -> element.attr("abs:data-original")
|
||||
element.hasValidAttr("data-src") -> element.attr("abs:data-src")
|
||||
element.hasValidAttr("src") -> element.attr("abs:src")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
open val pageListSelector = "div.page-chapter > img, li.blocks-gallery-item img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select(pageListSelector).mapNotNull { img -> imageOrNull(img) }
|
||||
.distinct()
|
||||
.mapIndexed { i, image -> Page(i, "", image) }
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
protected class StatusFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Status", vals)
|
||||
protected class GenreFilter(vals: Array<Pair<String?, String>>) : UriPartFilter("Genre", vals)
|
||||
|
||||
protected open fun getStatusList(): Array<Pair<String?, String>> = arrayOf(
|
||||
Pair(null, "Tất cả"),
|
||||
Pair("1", "Đang tiến hành"),
|
||||
Pair("2", "Đã hoàn thành"),
|
||||
Pair("3", "Tạm ngừng")
|
||||
)
|
||||
protected open fun getGenreList(): Array<Pair<String?, String>> = arrayOf(
|
||||
null to "Tất cả",
|
||||
"action" to "Action",
|
||||
"adult" to "Adult",
|
||||
"adventure" to "Adventure",
|
||||
"anime" to "Anime",
|
||||
"chuyen-sinh" to "Chuyển Sinh",
|
||||
"comedy" to "Comedy",
|
||||
"comic" to "Comic",
|
||||
"cooking" to "Cooking",
|
||||
"co-dai" to "Cổ Đại",
|
||||
"doujinshi" to "Doujinshi",
|
||||
"drama" to "Drama",
|
||||
"dam-my" to "Đam Mỹ",
|
||||
"ecchi" to "Ecchi",
|
||||
"fantasy" to "Fantasy",
|
||||
"gender-bender" to "Gender Bender",
|
||||
"harem" to "Harem",
|
||||
"historical" to "Historical",
|
||||
"horror" to "Horror",
|
||||
"josei" to "Josei",
|
||||
"live-action" to "Live action",
|
||||
"manga" to "Manga",
|
||||
"manhua" to "Manhua",
|
||||
"manhwa" to "Manhwa",
|
||||
"martial-arts" to "Martial Arts",
|
||||
"mature" to "Mature",
|
||||
"mecha" to "Mecha",
|
||||
"mystery" to "Mystery",
|
||||
"ngon-tinh" to "Ngôn Tình",
|
||||
"one-shot" to "One shot",
|
||||
"psychological" to "Psychological",
|
||||
"romance" to "Romance",
|
||||
"school-life" to "School Life",
|
||||
"sci-fi" to "Sci-fi",
|
||||
"seinen" to "Seinen",
|
||||
"shoujo" to "Shoujo",
|
||||
"shoujo-ai" to "Shoujo Ai",
|
||||
"shounen" to "Shounen",
|
||||
"shounen-ai" to "Shounen Ai",
|
||||
"slice-of-life" to "Slice of Life",
|
||||
"smut" to "Smut",
|
||||
"soft-yaoi" to "Soft Yaoi",
|
||||
"soft-yuri" to "Soft Yuri",
|
||||
"sports" to "Sports",
|
||||
"supernatural" to "Supernatural",
|
||||
"thieu-nhi" to "Thiếu Nhi",
|
||||
"tragedy" to "Tragedy",
|
||||
"trinh-tham" to "Trinh Thám",
|
||||
"truyen-scan" to "Truyện scan",
|
||||
"truyen-mau" to "Truyện Màu",
|
||||
"webtoon" to "Webtoon",
|
||||
"xuyen-khong" to "Xuyên Không"
|
||||
)
|
||||
|
||||
protected open class UriPartFilter(displayName: String, val vals: Array<Pair<String?, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.wpcomics
|
||||
|
||||
import generator.ThemeSourceData.MultiLang
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class WPComicsGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "wpcomics"
|
||||
|
||||
override val themeClass = "WPComics"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("ComicLatest", "https://comiclatest.com", "en", overrideVersionCode = 1),
|
||||
MultiLang("MangaSum", "https://mangasum.com", listOf("en", "ja")),
|
||||
SingleLang("NetTruyen", "https://www.nettruyen.com", "vi", overrideVersionCode = 1),
|
||||
SingleLang("NhatTruyen", "http://nhattruyen.com", "vi", overrideVersionCode = 1),
|
||||
SingleLang("TruyenChon", "http://truyenchon.com", "vi", overrideVersionCode = 1),
|
||||
SingleLang("XOXO Comics", "https://xoxocomics.com", "en", className = "XoxoComics", overrideVersionCode = 1),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
WPComicsGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,290 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.wpmangareader
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
abstract class WPMangaReader(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
val mangaUrlDirectory: String = "/manga",
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMMM dd, yyyy", Locale.US)
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient
|
||||
|
||||
// popular
|
||||
override fun popularMangaSelector() = ".utao .uta .imgu, .listupd .bs .bsx, .listo .bs .bsx"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl$mangaUrlDirectory/?page=$page&order=popular", headers)
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = SManga.create().apply {
|
||||
thumbnail_url = element.select("img").attr("abs:src")
|
||||
title = element.select("a").attr("title")
|
||||
setUrlWithoutDomain(element.select("a").attr("href"))
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "div.pagination .next, div.hpage .r"
|
||||
|
||||
// latest
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl$mangaUrlDirectory/?page=$page&order=update", headers)
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// search
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val filters = if (filters.isEmpty()) getFilterList() else filters
|
||||
val genre = filters.findInstance<GenreList>()?.toUriPart()
|
||||
val order = filters.findInstance<OrderByFilter>()?.toUriPart()
|
||||
|
||||
return when {
|
||||
order!!.isNotEmpty() -> GET("$baseUrl$mangaUrlDirectory/?page=$page&order=$order")
|
||||
genre!!.isNotEmpty() -> GET("$baseUrl/genres/$genre/page/$page/?s=$query")
|
||||
else -> GET("$baseUrl/page/$page/?s=$query")
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// manga details
|
||||
override fun mangaDetailsParse(document: Document) = SManga.create().apply {
|
||||
author = document.select(".listinfo li:contains(Author), .tsinfo .imptdt:nth-child(4) i, .infotable tr:contains(author) td:last-child")
|
||||
.firstOrNull()?.ownText()
|
||||
|
||||
artist = document.select(".infotable tr:contains(artist) td:last-child, .tsinfo .imptdt:contains(artist) i")
|
||||
.firstOrNull()?.ownText()
|
||||
|
||||
genre = document.select("div.gnr a, .mgen a, .seriestugenre a").joinToString { it.text() }
|
||||
status = parseStatus(
|
||||
document.select("div.listinfo li:contains(Status), .tsinfo .imptdt:contains(status), .infotable tr:contains(status) td")
|
||||
.text()
|
||||
)
|
||||
|
||||
thumbnail_url = document.select(".infomanga > div[itemprop=image] img, .thumb img").attr("abs:src")
|
||||
description = document.select(".desc, .entry-content[itemprop=description]").joinToString("\n") { it.text() }
|
||||
|
||||
// add series type(manga/manhwa/manhua/other) thinggy to genre
|
||||
document.select(seriesTypeSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not() && genre!!.contains(it, true).not()) {
|
||||
genre += if (genre!!.isEmpty()) it else ", $it"
|
||||
}
|
||||
}
|
||||
|
||||
// add alternative name to manga description
|
||||
document.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not()) {
|
||||
description += when {
|
||||
description!!.isEmpty() -> altName + it
|
||||
else -> "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val seriesTypeSelector = "span:contains(Type) a, .imptdt:contains(Type) a, a[href*=type\\=], .infotable tr:contains(Type) td:last-child"
|
||||
open val altNameSelector = ".alternative, .seriestualt"
|
||||
open val altName = "Alternative Name" + ": "
|
||||
|
||||
private fun parseStatus(status: String) = when {
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// chapters
|
||||
override fun chapterListSelector() = "div.bxcl li, #chapterlist li .eph-num a"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
|
||||
// Add timestamp to latest chapter, taken from "Updated On". so source which not provide chapter timestamp will have atleast one
|
||||
val date = document.select(".listinfo time[itemprop=dateModified]").attr("datetime")
|
||||
val checkChapter = document.select(chapterListSelector()).firstOrNull()
|
||||
if (date != "" && checkChapter != null) chapters[0].date_upload = parseDate(date)
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return try {
|
||||
dateFormat.parse(date)?.time ?: 0
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(date: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date)?.time ?: 0L
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = SChapter.create().apply {
|
||||
setUrlWithoutDomain(element.select("a").attr("href").substringAfter(baseUrl))
|
||||
name = element.select(".lch a, .chapternum").text()
|
||||
date_upload = element.select(".chapterdate").firstOrNull()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
}
|
||||
|
||||
// pages
|
||||
open val pageSelector = "div#readerarea img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
var pages = mutableListOf<Page>()
|
||||
document.select(pageSelector)
|
||||
.filterNot { it.attr("src").isNullOrEmpty() }
|
||||
.mapIndexed { i, img -> pages.add(Page(i, "", img.attr("abs:src"))) }
|
||||
|
||||
// Some sites like mangakita now load pages via javascript
|
||||
if (pages.isNotEmpty()) { return pages }
|
||||
|
||||
val docString = document.toString()
|
||||
val imageListRegex = Regex("\\\"images.*?:.*?(\\[.*?\\])")
|
||||
|
||||
val imageList = JSONArray(imageListRegex.find(docString)!!.destructured.toList()[0])
|
||||
|
||||
for (i in 0 until imageList.length()) {
|
||||
pages.add(Page(i, "", imageList.getString(i)))
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not Used")
|
||||
|
||||
// filters
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("Order by filter cannot be used with others"),
|
||||
OrderByFilter(),
|
||||
Filter.Separator(),
|
||||
GenreList()
|
||||
)
|
||||
|
||||
private class OrderByFilter : UriPartFilter(
|
||||
"Order By",
|
||||
arrayOf(
|
||||
Pair("", "<select>"),
|
||||
Pair("title", "A-Z"),
|
||||
Pair("update", "Latest Update"),
|
||||
Pair("create", "Latest Added")
|
||||
)
|
||||
)
|
||||
|
||||
private class GenreList : UriPartFilter(
|
||||
"Select Genre",
|
||||
arrayOf(
|
||||
Pair("", "<select>"),
|
||||
Pair("4-koma", "4-Koma"),
|
||||
Pair("action", "Action"),
|
||||
Pair("adaptation", "Adaptation"),
|
||||
Pair("adult", "Adult"),
|
||||
Pair("adventure", "Adventure"),
|
||||
Pair("animal", "Animal"),
|
||||
Pair("animals", "Animals"),
|
||||
Pair("anthology", "Anthology"),
|
||||
Pair("apocalypto", "Apocalypto"),
|
||||
Pair("comedy", "Comedy"),
|
||||
Pair("comic", "Comic"),
|
||||
Pair("cooking", "Cooking"),
|
||||
Pair("crime", "Crime"),
|
||||
Pair("demons", "Demons"),
|
||||
Pair("doujinshi", "Doujinshi"),
|
||||
Pair("drama", "Drama"),
|
||||
Pair("ecchi", "Ecchi"),
|
||||
Pair("fantasi", "Fantasi"),
|
||||
Pair("fantasy", "Fantasy"),
|
||||
Pair("game", "Game"),
|
||||
Pair("gender-bender", "Gender Bender"),
|
||||
Pair("genderswap", "Genderswap"),
|
||||
Pair("drama", "Drama"),
|
||||
Pair("gore", "Gore"),
|
||||
Pair("harem", "Harem"),
|
||||
Pair("hentai", "Hentai"),
|
||||
Pair("historical", "Historical"),
|
||||
Pair("horor", "Horor"),
|
||||
Pair("horror", "Horror"),
|
||||
Pair("isekai", "Isekai"),
|
||||
Pair("josei", "Josei"),
|
||||
Pair("kingdom", "kingdom"),
|
||||
Pair("magic", "Magic"),
|
||||
Pair("manga", "Manga"),
|
||||
Pair("manhua", "Manhua"),
|
||||
Pair("manhwa", "Manhwa"),
|
||||
Pair("martial-art", "Martial Art"),
|
||||
Pair("martial-arts", "Martial Arts"),
|
||||
Pair("mature", "Mature"),
|
||||
Pair("mecha", "Mecha"),
|
||||
Pair("medical", "Medical"),
|
||||
Pair("military", "Military"),
|
||||
Pair("modern", "Modern"),
|
||||
Pair("monster", "Monster"),
|
||||
Pair("monster-girls", "Monster Girls"),
|
||||
Pair("music", "Music"),
|
||||
Pair("mystery", "Mystery"),
|
||||
Pair("oneshot", "Oneshot"),
|
||||
Pair("post-apocalyptic", "Post-Apocalyptic"),
|
||||
Pair("project", "Project"),
|
||||
Pair("psychological", "Psychological"),
|
||||
Pair("reincarnation", "Reincarnation"),
|
||||
Pair("romance", "Romance"),
|
||||
Pair("romancem", "Romancem"),
|
||||
Pair("samurai", "Samurai"),
|
||||
Pair("school", "School"),
|
||||
Pair("school-life", "School Life"),
|
||||
Pair("sci-fi", "Sci-Fi"),
|
||||
Pair("seinen", "Seinen"),
|
||||
Pair("shoujo", "Shoujo"),
|
||||
Pair("shoujo-ai", "Shoujo Ai"),
|
||||
Pair("shounen", "Shounen"),
|
||||
Pair("shounen-ai", "Shounen Ai"),
|
||||
Pair("slice-of-life", "Slice of Life"),
|
||||
Pair("smut", "Smut"),
|
||||
Pair("sports", "Sports"),
|
||||
Pair("style-ancient", "Style ancient"),
|
||||
Pair("super-power", "Super Power"),
|
||||
Pair("superhero", "Superhero"),
|
||||
Pair("supernatural", "Supernatural"),
|
||||
Pair("survival", "Survival"),
|
||||
Pair("survive", "Survive"),
|
||||
Pair("thriller", "Thriller"),
|
||||
Pair("time-travel", "Time Travel"),
|
||||
Pair("tragedy", "Tragedy"),
|
||||
Pair("urban", "Urban"),
|
||||
Pair("vampire", "Vampire"),
|
||||
Pair("video-games", "Video Games"),
|
||||
Pair("virtual-reality", "Virtual Reality"),
|
||||
Pair("webtoons", "Webtoons"),
|
||||
Pair("yuri", "Yuri"),
|
||||
Pair("zombies", "Zombies")
|
||||
)
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.second }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].first
|
||||
}
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.wpmangareader
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class WPMangaReaderGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "wpmangareader"
|
||||
|
||||
override val themeClass = "WPMangaReader"
|
||||
|
||||
override val baseVersionCode: Int = 4
|
||||
|
||||
override val sources = listOf(
|
||||
|
||||
SingleLang("KomikMama", "https://komikmama.net", "id"),
|
||||
SingleLang("MangaKita", "https://mangakita.net", "id"),
|
||||
SingleLang("Ngomik", "https://ngomik.net", "id"),
|
||||
SingleLang("Sekaikomik", "https://www.sekaikomik.club", "id", isNsfw = true, overrideVersionCode = 3),
|
||||
SingleLang("TurkToon", "https://turktoon.com", "tr"),
|
||||
SingleLang("Gecenin Lordu", "https://geceninlordu.com/", "tr", overrideVersionCode = 1),
|
||||
SingleLang("Flame Scans", "http://flamescans.org", "en", overrideVersionCode = 1),
|
||||
SingleLang("PMScans", "https://reader.pmscans.com", "en"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
WPMangaReaderGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,460 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.wpmangastream
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.support.v7.preference.ListPreference
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
//import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor // added to override
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class WPMangaStream(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String,
|
||||
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.US)
|
||||
) : ConfigurableSource, ParsedHttpSource() {
|
||||
override val supportsLatest = true
|
||||
|
||||
companion object {
|
||||
private const val MID_QUALITY = 1
|
||||
private const val LOW_QUALITY = 2
|
||||
|
||||
private const val SHOW_THUMBNAIL_PREF_Title = "Default thumbnail quality"
|
||||
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
|
||||
val thumbsPref = androidx.preference.ListPreference(screen.context).apply {
|
||||
key = SHOW_THUMBNAIL_PREF_Title
|
||||
title = SHOW_THUMBNAIL_PREF_Title
|
||||
entries = arrayOf("Show high quality", "Show mid quality", "Show low quality")
|
||||
entryValues = arrayOf("0", "1", "2")
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = this.findIndexOfValue(selected)
|
||||
preferences.edit().putInt(SHOW_THUMBNAIL_PREF, index).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(thumbsPref)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val thumbsPref = ListPreference(screen.context).apply {
|
||||
key = SHOW_THUMBNAIL_PREF_Title
|
||||
title = SHOW_THUMBNAIL_PREF_Title
|
||||
entries = arrayOf("Show high quality", "Show mid quality", "Show low quality")
|
||||
entryValues = arrayOf("0", "1", "2")
|
||||
summary = "%s"
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
val selected = newValue as String
|
||||
val index = this.findIndexOfValue(selected)
|
||||
preferences.edit().putInt(SHOW_THUMBNAIL_PREF, index).commit()
|
||||
}
|
||||
}
|
||||
screen.addPreference(thumbsPref)
|
||||
}
|
||||
|
||||
private fun getShowThumbnail(): Int = preferences.getInt(SHOW_THUMBNAIL_PREF, 0)
|
||||
|
||||
//private val rateLimitInterceptor = RateLimitInterceptor(4)
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
//.addNetworkInterceptor(rateLimitInterceptor)
|
||||
.build()
|
||||
|
||||
protected fun Element.imgAttr(): String = if (this.hasAttr("data-src")) this.attr("abs:data-src") else this.attr("abs:src")
|
||||
protected fun Elements.imgAttr(): String = this.first().imgAttr()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga/?page=$page&order=popular", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga/?page=$page&order=update", headers)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/manga/")!!.newBuilder()
|
||||
url.addQueryParameter("title", query)
|
||||
url.addQueryParameter("page", page.toString())
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is AuthorFilter -> {
|
||||
url.addQueryParameter("author", filter.state)
|
||||
}
|
||||
is YearFilter -> {
|
||||
url.addQueryParameter("yearx", filter.state)
|
||||
}
|
||||
is StatusFilter -> {
|
||||
val status = when (filter.state) {
|
||||
Filter.TriState.STATE_INCLUDE -> "completed"
|
||||
Filter.TriState.STATE_EXCLUDE -> "ongoing"
|
||||
else -> ""
|
||||
}
|
||||
url.addQueryParameter("status", status)
|
||||
}
|
||||
is TypeFilter -> {
|
||||
url.addQueryParameter("type", filter.toUriPart())
|
||||
}
|
||||
is SortByFilter -> {
|
||||
url.addQueryParameter("order", filter.toUriPart())
|
||||
}
|
||||
is GenreListFilter -> {
|
||||
filter.state
|
||||
.filter { it.state != Filter.TriState.STATE_IGNORE }
|
||||
.forEach { url.addQueryParameter("genre[]", it.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return GET(url.build().toString(), headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.bs"
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.thumbnail_url = element.select("div.limit img").imgAttr()
|
||||
element.select("div.bsx > a").first().let {
|
||||
manga.setUrlWithoutDomain(it.attr("href"))
|
||||
manga.title = it.attr("title")
|
||||
}
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = "a.next.page-numbers, a.r"
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
return SManga.create().apply {
|
||||
document.select("div.bigcontent, div.animefull, div.main-info").firstOrNull()?.let { infoElement ->
|
||||
status = parseStatus(infoElement.select("span:contains(Status:), .imptdt:contains(Status) i").firstOrNull()?.ownText())
|
||||
author = infoElement.select("span:contains(Author:), span:contains(Pengarang:), .fmed b:contains(Author)+span, .imptdt:contains(Author) i").firstOrNull()?.ownText()
|
||||
artist = infoElement.select(".fmed b:contains(Artist)+span, .imptdt:contains(Artist) i").firstOrNull()?.ownText()
|
||||
description = infoElement.select("div.desc p, div.entry-content p").joinToString("\n") { it.text() }
|
||||
thumbnail_url = infoElement.select("div.thumb img").imgAttr()
|
||||
|
||||
val genres = infoElement.select("span:contains(Genre) a, .mgen a")
|
||||
.map { element -> element.text().toLowerCase() }
|
||||
.toMutableSet()
|
||||
|
||||
// add series type(manga/manhwa/manhua/other) thinggy to genre
|
||||
document.select(seriesTypeSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not() && genres.contains(it).not()) {
|
||||
genres.add(it.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
||||
genre = genres.toList().map { it.capitalize() }.joinToString(", ")
|
||||
|
||||
// add alternative name to manga description
|
||||
document.select(altNameSelector).firstOrNull()?.ownText()?.let {
|
||||
if (it.isEmpty().not() && it !="N/A" && it != "-") {
|
||||
description += when {
|
||||
description!!.isEmpty() -> altName + it
|
||||
else -> "\n\n$altName" + it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val seriesTypeSelector = "span:contains(Type) a, .imptdt:contains(Type) a, a[href*=type\\=], .infotable tr:contains(Type) td:last-child"
|
||||
open val altNameSelector = ".alternative, .wd-full:contains(Alt) span, .alter, .seriestualt"
|
||||
open val altName = "Alternative Name" + ": "
|
||||
|
||||
protected fun parseStatus(element: String?): Int = when {
|
||||
element == null -> SManga.UNKNOWN
|
||||
listOf("ongoing", "publishing").any { it.contains(element, ignoreCase = true) } -> SManga.ONGOING
|
||||
listOf("completed").any { it.contains(element, ignoreCase = true) } -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.bxcl ul li, div.cl ul li, ul li:has(div.chbox):has(div.eph-num)"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val document = response.asJsoup()
|
||||
val chapters = document.select(chapterListSelector()).map { chapterFromElement(it) }
|
||||
|
||||
// Add timestamp to latest chapter, taken from "Updated On". so source which not provide chapter timestamp will have atleast one
|
||||
val date = document.select(".fmed:contains(update) time ,span:contains(update) time").attr("datetime")
|
||||
val checkChapter = document.select(chapterListSelector()).firstOrNull()
|
||||
if (date != "" && checkChapter != null) chapters[0].date_upload = parseDate(date)
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
private fun parseDate(date: String): Long {
|
||||
return SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH).parse(date)?.time ?: 0L
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val urlElement = element.select(".lchx > a, span.leftoff a, div.eph-num > a").first()
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = if (urlElement.select("span.chapternum").isNotEmpty()) urlElement.select("span.chapternum").text() else urlElement.text()
|
||||
chapter.date_upload = element.select("span.rightoff, time, span.chapterdate").firstOrNull()?.text()?.let { parseChapterDate(it) } ?: 0
|
||||
return chapter
|
||||
}
|
||||
|
||||
fun parseChapterDate(date: String): Long {
|
||||
return if (date.contains("ago")) {
|
||||
val value = date.split(' ')[0].toInt()
|
||||
when {
|
||||
"min" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hour" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"day" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"week" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"month" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"year" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
0L
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
dateFormat.parse(date)?.time ?: 0
|
||||
} catch (_: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareNewChapter(chapter: SChapter, manga: SManga) {
|
||||
val basic = Regex("""Chapter\s([0-9]+)""")
|
||||
when {
|
||||
basic.containsMatchIn(chapter.name) -> {
|
||||
basic.find(chapter.name)?.let {
|
||||
chapter.chapter_number = it.groups[1]?.value!!.toFloat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open val pageSelector = "div#readerarea img"
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
var pages = mutableListOf<Page>()
|
||||
document.select(pageSelector)
|
||||
.filterNot { it.attr("src").isNullOrEmpty() }
|
||||
.mapIndexed { i, img -> pages.add(Page(i, "", img.attr("abs:src"))) }
|
||||
|
||||
// Some wpmangastream sites now load pages via javascript
|
||||
if (pages.isNotEmpty()) { return pages }
|
||||
|
||||
val docString = document.toString()
|
||||
val imageListRegex = Regex("\\\"images.*?:.*?(\\[.*?\\])")
|
||||
|
||||
val imageList = JSONArray(imageListRegex.find(docString)!!.destructured.toList()[0])
|
||||
|
||||
for (i in 0 until imageList.length()) {
|
||||
pages.add(Page(i, "", imageList.getString(i)))
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val headers = Headers.Builder()
|
||||
headers.apply {
|
||||
add("Referer", baseUrl)
|
||||
add("User-Agent", "Mozilla/5.0 (Linux; U; Android 4.4.2; en-us; LGMS323 Build/KOT49I.MS32310c) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/76.0.3809.100 Mobile Safari/537.36")
|
||||
}
|
||||
|
||||
if (page.imageUrl!!.contains(".wp.com")) {
|
||||
headers.apply {
|
||||
add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
|
||||
}
|
||||
}
|
||||
|
||||
return GET(getImageUrl(page.imageUrl!!, getShowThumbnail()), headers.build())
|
||||
}
|
||||
|
||||
private fun getImageUrl(originalUrl: String, quality: Int): String {
|
||||
val url = originalUrl.substringAfter("//")
|
||||
return when (quality) {
|
||||
LOW_QUALITY -> "https://images.weserv.nl/?w=300&q=70&url=$url"
|
||||
MID_QUALITY -> "https://images.weserv.nl/?w=600&q=70&url=$url"
|
||||
else -> originalUrl
|
||||
}
|
||||
}
|
||||
|
||||
private class AuthorFilter : Filter.Text("Author")
|
||||
|
||||
private class YearFilter : Filter.Text("Year")
|
||||
|
||||
protected class TypeFilter : UriPartFilter(
|
||||
"Type",
|
||||
arrayOf(
|
||||
Pair("Default", ""),
|
||||
Pair("Manga", "Manga"),
|
||||
Pair("Manhwa", "Manhwa"),
|
||||
Pair("Manhua", "Manhua"),
|
||||
Pair("Comic", "Comic")
|
||||
)
|
||||
)
|
||||
|
||||
protected class SortByFilter : UriPartFilter(
|
||||
"Sort By",
|
||||
arrayOf(
|
||||
Pair("Default", ""),
|
||||
Pair("A-Z", "title"),
|
||||
Pair("Z-A", "titlereverse"),
|
||||
Pair("Latest Update", "update"),
|
||||
Pair("Latest Added", "latest"),
|
||||
Pair("Popular", "popular")
|
||||
)
|
||||
)
|
||||
|
||||
protected class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("All", ""),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Completed", "completed")
|
||||
)
|
||||
)
|
||||
|
||||
protected class Genre(name: String, val id: String = name) : Filter.TriState(name)
|
||||
protected class GenreListFilter(genres: List<Genre>) : Filter.Group<Genre>("Genre", genres)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Header("Genre exclusion not available for all sources"),
|
||||
Filter.Separator(),
|
||||
AuthorFilter(),
|
||||
YearFilter(),
|
||||
StatusFilter(),
|
||||
TypeFilter(),
|
||||
SortByFilter(),
|
||||
GenreListFilter(getGenreList())
|
||||
)
|
||||
|
||||
protected open fun getGenreList(): List<Genre> = listOf(
|
||||
Genre("4 Koma", "4-koma"),
|
||||
Genre("Action", "action"),
|
||||
Genre("Adult", "adult"),
|
||||
Genre("Adventure", "adventure"),
|
||||
Genre("Comedy", "comedy"),
|
||||
Genre("Completed", "completed"),
|
||||
Genre("Cooking", "cooking"),
|
||||
Genre("Crime", "crime"),
|
||||
Genre("Demon", "demon"),
|
||||
Genre("Demons", "demons"),
|
||||
Genre("Doujinshi", "doujinshi"),
|
||||
Genre("Drama", "drama"),
|
||||
Genre("Ecchi", "ecchi"),
|
||||
Genre("Fantasy", "fantasy"),
|
||||
Genre("Game", "game"),
|
||||
Genre("Games", "games"),
|
||||
Genre("Gender Bender", "gender-bender"),
|
||||
Genre("Gore", "gore"),
|
||||
Genre("Harem", "harem"),
|
||||
Genre("Historical", "historical"),
|
||||
Genre("Horror", "horror"),
|
||||
Genre("Isekai", "isekai"),
|
||||
Genre("Josei", "josei"),
|
||||
Genre("Magic", "magic"),
|
||||
Genre("Manga", "manga"),
|
||||
Genre("Manhua", "manhua"),
|
||||
Genre("Manhwa", "manhwa"),
|
||||
Genre("Martial Art", "martial-art"),
|
||||
Genre("Martial Arts", "martial-arts"),
|
||||
Genre("Mature", "mature"),
|
||||
Genre("Mecha", "mecha"),
|
||||
Genre("Military", "military"),
|
||||
Genre("Monster", "monster"),
|
||||
Genre("Monster Girls", "monster-girls"),
|
||||
Genre("Monsters", "monsters"),
|
||||
Genre("Music", "music"),
|
||||
Genre("Mystery", "mystery"),
|
||||
Genre("One-shot", "one-shot"),
|
||||
Genre("Oneshot", "oneshot"),
|
||||
Genre("Police", "police"),
|
||||
Genre("Pshycological", "pshycological"),
|
||||
Genre("Psychological", "psychological"),
|
||||
Genre("Reincarnation", "reincarnation"),
|
||||
Genre("Reverse Harem", "reverse-harem"),
|
||||
Genre("Romancce", "romancce"),
|
||||
Genre("Romance", "romance"),
|
||||
Genre("Samurai", "samurai"),
|
||||
Genre("School", "school"),
|
||||
Genre("School Life", "school-life"),
|
||||
Genre("Sci-fi", "sci-fi"),
|
||||
Genre("Seinen", "seinen"),
|
||||
Genre("Shoujo", "shoujo"),
|
||||
Genre("Shoujo Ai", "shoujo-ai"),
|
||||
Genre("Shounen", "shounen"),
|
||||
Genre("Shounen Ai", "shounen-ai"),
|
||||
Genre("Slice of Life", "slice-of-life"),
|
||||
Genre("Sports", "sports"),
|
||||
Genre("Super Power", "super-power"),
|
||||
Genre("Supernatural", "supernatural"),
|
||||
Genre("Thriller", "thriller"),
|
||||
Genre("Time Travel", "time-travel"),
|
||||
Genre("Tragedy", "tragedy"),
|
||||
Genre("Vampire", "vampire"),
|
||||
Genre("Webtoon", "webtoon"),
|
||||
Genre("Webtoons", "webtoons"),
|
||||
Genre("Yaoi", "yaoi"),
|
||||
Genre("Yuri", "yuri"),
|
||||
Genre("Zombies", "zombies")
|
||||
)
|
||||
|
||||
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.wpmangastream
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class WPMangaStreamGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "wpmangastream"
|
||||
|
||||
override val themeClass = "WPMangaStream"
|
||||
|
||||
override val baseVersionCode: Int = 4
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("KlanKomik", "https://klankomik.com", "id"),
|
||||
SingleLang("ChiOtaku", "https://chiotaku.com", "id"),
|
||||
SingleLang("MangaShiro", "https://mangashiro.co", "id"),
|
||||
SingleLang("MasterKomik", "https://masterkomik.com", "id"),
|
||||
SingleLang("Kaisar Komik", "https://kaisarkomik.com", "id"),
|
||||
SingleLang("Rawkuma", "https://rawkuma.com/", "ja"),
|
||||
SingleLang("MangaP", "https://mangap.me", "ar"),
|
||||
SingleLang("Boosei", "https://boosei.com", "id"),
|
||||
SingleLang("Mangakyo", "https://www.mangakyo.me", "id"),
|
||||
SingleLang("Sekte Komik", "https://sektekomik.com", "id"),
|
||||
SingleLang("Komik Station", "https://komikstation.com", "id"),
|
||||
SingleLang("Komik Indo", "https://www.komikindo.web.id", "id", className = "KomikIndoWPMS"),
|
||||
SingleLang("Non-Stop Scans", "https://www.nonstopscans.com", "en", className = "NonStopScans"),
|
||||
SingleLang("KomikIndo.co", "https://komikindo.co", "id", className = "KomikindoCo"),
|
||||
SingleLang("Readkomik", "https://readkomik.com", "en", className = "ReadKomik"),
|
||||
SingleLang("MangaIndonesia", "https://mangaindonesia.net", "id"),
|
||||
SingleLang("Liebe Schnee Hiver", "https://www.liebeschneehiver.com", "tr"),
|
||||
SingleLang("KomikRu", "https://komikru.com", "id"),
|
||||
SingleLang("GURU Komik", "https://gurukomik.com", "id"),
|
||||
SingleLang("Shea Manga", "https://sheamanga.my.id", "id"),
|
||||
SingleLang("Kiryuu", "https://kiryuu.co", "id"),
|
||||
SingleLang("Komik AV", "https://komikav.com", "id"),
|
||||
SingleLang("Komik Cast", "https://komikcast.com", "id", overrideVersionCode = 3), // make it from v0 to v3 to force update user who still use old standalone ext, they will need to migrate
|
||||
SingleLang("West Manga", "https://westmanga.info", "id"),
|
||||
SingleLang("Komik GO", "https://komikgo.com", "id", overrideVersionCode = 1),
|
||||
SingleLang("MangaSwat", "https://mangaswat.com", "ar"),
|
||||
SingleLang("Manga Raw.org", "https://mangaraw.org", "ja", className = "MangaRawOrg", overrideVersionCode = 1),
|
||||
SingleLang("Matakomik", "https://matakomik.com", "id"),
|
||||
SingleLang("Manga Pro Z", "https://mangaproz.com", "ar"),
|
||||
SingleLang("Silence Scan", "https://silencescan.net", "pt-BR"),
|
||||
SingleLang("Kuma Scans (Kuma Translation)", "https://kumascans.com", "en", className = "KumaScans"),
|
||||
SingleLang("Tempest Manga", "https://manga.tempestfansub.com", "tr"),
|
||||
SingleLang("xCaliBR Scans", "https://xcalibrscans.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("NoxSubs", "https://noxsubs.com", "tr"),
|
||||
SingleLang("World Romance Translation", "https://wrt.my.id/", "id", overrideVersionCode = 1),
|
||||
SingleLang("The Apollo Team", "https://theapollo.team", "en"),
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
WPMangaStreamGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,254 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.zbulu
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class Zbulu(
|
||||
override val name: String,
|
||||
override val baseUrl: String,
|
||||
override val lang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0")
|
||||
.add("Content-Encoding", "identity")
|
||||
|
||||
// Decreases calls, helps with Cloudflare
|
||||
private fun String.addTrailingSlash() = if (!this.endsWith("/")) "$this/" else this
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/manga-list/page-$page/", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = "div.comics-grid > div.entry"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
return SManga.create().apply {
|
||||
element.select("h3 a").let {
|
||||
setUrlWithoutDomain(it.attr("href").addTrailingSlash())
|
||||
title = it.text()
|
||||
}
|
||||
thumbnail_url = element.select("img").first().attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "a.next:has(i.fa-angle-right)"
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/latest-update/page-$page/", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = if (query.isNotBlank()) {
|
||||
"$baseUrl/?s=$query"
|
||||
} else {
|
||||
lateinit var ret: String
|
||||
lateinit var genre: String
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is AuthorField -> {
|
||||
if (filter.state.isNotBlank()) {
|
||||
ret = "$baseUrl/author/${filter.state.replace(" ", "-")}/page-$page"
|
||||
}
|
||||
}
|
||||
is GenreFilter -> {
|
||||
if (filter.toUriPart().isNotBlank() && filter.state != 0) {
|
||||
filter.toUriPart().let { genre = if (it == "completed") "completed" else "genre/$it" }
|
||||
ret = "$baseUrl/$genre/page-$page"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
return GET(url, headers)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = latestUpdatesFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
// Manga summary page
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div.single-comic").first()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = infoElement.select("h1").first().text()
|
||||
author = infoElement.select("div.author a").text()
|
||||
status = parseStatus(infoElement.select("div.update span[style]").text())
|
||||
genre = infoElement.select("div.genre a").joinToString { it.text() }
|
||||
description = infoElement.select("div.comic-description p").text()
|
||||
thumbnail_url = infoElement.select("img").attr("abs:src")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
// Chapters
|
||||
|
||||
override fun chapterListSelector() = "div.go-border"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
||||
// Chapter list may be paginated, get recursively
|
||||
fun addChapters(document: Document) {
|
||||
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||
document.select("${latestUpdatesNextPageSelector()}:not([id])").firstOrNull()
|
||||
?.let { addChapters(client.newCall(GET(it.attr("abs:href").addTrailingSlash(), headers)).execute().asJsoup()) }
|
||||
}
|
||||
|
||||
addChapters(response.asJsoup())
|
||||
return chapters
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
return SChapter.create().apply {
|
||||
element.select("a").let {
|
||||
setUrlWithoutDomain(it.attr("href").addTrailingSlash())
|
||||
name = it.text()
|
||||
}
|
||||
date_upload = element.select("div.chapter-date")?.text().toDate()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val dateFormat by lazy {
|
||||
SimpleDateFormat("MM/dd/yyyy", Locale.US)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.toDate(): Long {
|
||||
return if (this.isNullOrEmpty()) {
|
||||
0
|
||||
} else {
|
||||
dateFormat.parse(this)?.time ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
// Pages
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div.chapter-content img").mapIndexed { i, img ->
|
||||
Page(i, "", img.attr("abs:src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
// Filters
|
||||
|
||||
private class AuthorField : Filter.Text("Author")
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("Cannot combine search types!"),
|
||||
Filter.Header("Author name must be exact."),
|
||||
Filter.Separator("-----------------"),
|
||||
AuthorField(),
|
||||
GenreFilter()
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll('.sub-menu li a')].map(a => `Pair("${a.textContent}", "${a.getAttribute('href')}")`).join(',\n')
|
||||
// from $baseUrl
|
||||
private class GenreFilter : UriPartFilter(
|
||||
"Genres",
|
||||
arrayOf(
|
||||
Pair("Choose a genre", ""),
|
||||
Pair("Action", "action"),
|
||||
Pair("Adult", "adult"),
|
||||
Pair("Anime", "anime"),
|
||||
Pair("Adventure", "adventure"),
|
||||
Pair("Comedy", "comedy"),
|
||||
Pair("Comic", "comic"),
|
||||
Pair("Completed", "completed"),
|
||||
Pair("Cooking", "cooking"),
|
||||
Pair("Doraemon", "doraemon"),
|
||||
Pair("Doujinshi", "doujinshi"),
|
||||
Pair("Drama", "drama"),
|
||||
Pair("Ecchi", "ecchi"),
|
||||
Pair("Fantasy", "fantasy"),
|
||||
Pair("Full Color", "full-color"),
|
||||
Pair("Gender Bender", "gender-bender"),
|
||||
Pair("Harem", "harem"),
|
||||
Pair("Historical", "historical"),
|
||||
Pair("Horror", "horror"),
|
||||
Pair("Josei", "josei"),
|
||||
Pair("Live action", "live-action"),
|
||||
Pair("Magic", "magic"),
|
||||
Pair("Manga", "manga"),
|
||||
Pair("Manhua", "manhua"),
|
||||
Pair("Manhwa", "manhwa"),
|
||||
Pair("Martial Arts", "martial-arts"),
|
||||
Pair("Mature", "mature"),
|
||||
Pair("Mecha", "mecha"),
|
||||
Pair("Mystery", "mystery"),
|
||||
Pair("One shot", "one-shot"),
|
||||
Pair("Psychological", "psychological"),
|
||||
Pair("Romance", "romance"),
|
||||
Pair("School Life", "school-life"),
|
||||
Pair("Sci-fi", "sci-fi"),
|
||||
Pair("Seinen", "seinen"),
|
||||
Pair("Shoujo", "shoujo"),
|
||||
Pair("Shoujo Ai", "shoujo-ai"),
|
||||
Pair("Shounen", "shounen"),
|
||||
Pair("Shounen Ai", "shounen-ai"),
|
||||
Pair("Slice of life", "slice-of-life"),
|
||||
Pair("Smut", "smut"),
|
||||
Pair("Yaoi", "yaoi"),
|
||||
Pair("Yuri", "yuri"),
|
||||
Pair("Sports", "sports"),
|
||||
Pair("Supernatural", "supernatural"),
|
||||
Pair("Tragedy", "tragedy"),
|
||||
Pair("Trap", "trap"),
|
||||
Pair("Webtoons", "webtoons")
|
||||
)
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package eu.kanade.tachiyomi.multisrc.zbulu
|
||||
|
||||
import generator.ThemeSourceData.SingleLang
|
||||
import generator.ThemeSourceGenerator
|
||||
|
||||
class ZbuluGenerator : ThemeSourceGenerator {
|
||||
|
||||
override val themePkg = "zbulu"
|
||||
|
||||
override val themeClass = "Zbulu"
|
||||
|
||||
override val baseVersionCode: Int = 1
|
||||
|
||||
override val sources = listOf(
|
||||
SingleLang("HolyManga", "https://w15.holymanga.net", "en", overrideVersionCode = 1),
|
||||
SingleLang("My Toon", "https://mytoon.net", "en"),
|
||||
SingleLang("Koo Manga", "https://ww9.koomanga.com", "en", overrideVersionCode = 1),
|
||||
SingleLang("Bulu Manga", "https://ww8.bulumanga.net", "en")
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
ZbuluGenerator().createAll()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
@ -1,13 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Bato.to'
|
||||
pkgNameSuffix = 'all.batoto'
|
||||
extClass = '.BatoToFactory'
|
||||
extVersionCode = 7
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 67 KiB |
@ -1,518 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.batoto
|
||||
|
||||
import com.squareup.duktape.Duktape
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
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 okhttp3.HttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
open class BatoTo(
|
||||
override val lang: String,
|
||||
private val siteLang: String
|
||||
) : ParsedHttpSource() {
|
||||
|
||||
override val name: String = "Bato.to"
|
||||
override val baseUrl: String = "https://bato.to"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector(): String {
|
||||
return when (siteLang) {
|
||||
"" -> "div#series-list div.col"
|
||||
"en" -> "div#series-list div.col.no-flag"
|
||||
else -> "div#series-list div.col:has([data-lang=\"$siteLang\"])"
|
||||
}
|
||||
}
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
val item = element.select("a.item-cover")
|
||||
val imgurl = item.select("img").attr("abs:src")
|
||||
manga.setUrlWithoutDomain(item.attr("href"))
|
||||
manga.title = element.select("a.item-title").text()
|
||||
manga.thumbnail_url = imgurl
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesNextPageSelector() = "div#mainer .pagination .page-item:not(.disabled) a.page-link:contains(»)"
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/browse?langs=$siteLang&sort=views_w&page=$page")
|
||||
}
|
||||
|
||||
override fun popularMangaSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
|
||||
override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
return if (query.isNotBlank()) {
|
||||
GET("$baseUrl/search?word=$query&page=$page")
|
||||
} else {
|
||||
val url = HttpUrl.parse("$baseUrl/browse")!!.newBuilder()
|
||||
url.addQueryParameter("page", page.toString())
|
||||
url.addQueryParameter("langs", siteLang)
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OriginFilter -> {
|
||||
val originToInclude = mutableListOf<String>()
|
||||
filter.state.forEach { content ->
|
||||
if (content.state) {
|
||||
originToInclude.add(content.name)
|
||||
}
|
||||
}
|
||||
if (originToInclude.isNotEmpty()) {
|
||||
url.addQueryParameter(
|
||||
"origs",
|
||||
originToInclude
|
||||
.joinToString(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
is StatusFilter -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("release", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
is GenreFilter -> {
|
||||
val genreToInclude = filter.state
|
||||
.filter { it.isIncluded() }
|
||||
.map { it.name }
|
||||
|
||||
val genreToExclude = filter.state
|
||||
.filter { it.isExcluded() }
|
||||
.map { it.name }
|
||||
|
||||
if (genreToInclude.isNotEmpty() || genreToExclude.isNotEmpty()) {
|
||||
url.addQueryParameter(
|
||||
"genres",
|
||||
genreToInclude
|
||||
.joinToString(",") +
|
||||
"|" +
|
||||
genreToExclude
|
||||
.joinToString(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
is ChapterFilter -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("chapters", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
is SortBy -> {
|
||||
if (filter.state != 0) {
|
||||
url.addQueryParameter("sort", filter.toUriPart())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
GET(url.build().toString(), headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = latestUpdatesSelector()
|
||||
|
||||
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
|
||||
|
||||
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
if (manga.url.startsWith("http")) {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
return super.mangaDetailsRequest(manga)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val infoElement = document.select("div#mainer div.container-fluid")
|
||||
val manga = SManga.create()
|
||||
val genres = mutableListOf<String>()
|
||||
val status = infoElement.select("div.attr-item:contains(status) span").text()
|
||||
infoElement.select("div.attr-item:contains(genres) span").text().split(
|
||||
" / "
|
||||
.toRegex()
|
||||
).forEach { element ->
|
||||
genres.add(element)
|
||||
}
|
||||
manga.title = infoElement.select("h3").text()
|
||||
manga.author = infoElement.select("div.attr-item:contains(author) a:first-child").text()
|
||||
manga.artist = infoElement.select("div.attr-item:contains(author) a:last-child").text()
|
||||
manga.status = parseStatus(status)
|
||||
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
|
||||
manga.description = infoElement.select("h5:contains(summary) + pre").text()
|
||||
manga.thumbnail_url = document.select("div.attr-cover img")
|
||||
.attr("abs:src")
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String?) = when {
|
||||
status == null -> SManga.UNKNOWN
|
||||
status.contains("Ongoing") -> SManga.ONGOING
|
||||
status.contains("Completed") -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
if (manga.url.startsWith("http")) {
|
||||
return GET(manga.url, headers)
|
||||
}
|
||||
return super.chapterListRequest(manga)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.main div.p-2"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
val urlElement = element.select("a.chapt")
|
||||
val group = element.select("div.extra > a:not(.ps-3)").text()
|
||||
val time = element.select("i").text()
|
||||
.replace("a ", "1 ")
|
||||
.replace("an ", "1 ")
|
||||
chapter.setUrlWithoutDomain(urlElement.attr("href"))
|
||||
chapter.name = urlElement.text()
|
||||
if (group != "") {
|
||||
chapter.scanlator = group
|
||||
}
|
||||
if (time != "") {
|
||||
chapter.date_upload = parseChapterDate(time)
|
||||
}
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val value = date.split(' ')[0].toInt()
|
||||
|
||||
return when {
|
||||
"secs" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, value * -1)
|
||||
}.timeInMillis
|
||||
"mins" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hours" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"days" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"weeks" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"months" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"years" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
"sec" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.SECOND, value * -1)
|
||||
}.timeInMillis
|
||||
"min" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MINUTE, value * -1)
|
||||
}.timeInMillis
|
||||
"hour" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.HOUR_OF_DAY, value * -1)
|
||||
}.timeInMillis
|
||||
"day" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * -1)
|
||||
}.timeInMillis
|
||||
"week" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.DATE, value * 7 * -1)
|
||||
}.timeInMillis
|
||||
"month" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.MONTH, value * -1)
|
||||
}.timeInMillis
|
||||
"year" in date -> Calendar.getInstance().apply {
|
||||
add(Calendar.YEAR, value * -1)
|
||||
}.timeInMillis
|
||||
else -> {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
if (chapter.url.startsWith("http")) {
|
||||
return GET(chapter.url, headers)
|
||||
}
|
||||
return super.pageListRequest(chapter)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
val script = document.select("script").html()
|
||||
|
||||
if (script.contains("var images =")) {
|
||||
val imgJson = JSONObject(script.substringAfter("var images = ").substringBefore(";"))
|
||||
val imgNames = imgJson.names()
|
||||
|
||||
if (imgNames != null) {
|
||||
for (i in 0 until imgNames.length()) {
|
||||
val imgKey = imgNames.getString(i)
|
||||
val imgUrl = imgJson.getString(imgKey)
|
||||
pages.add(Page(i, "", imgUrl))
|
||||
}
|
||||
}
|
||||
} else if (script.contains("const server =")) { // bato.to
|
||||
val duktape = Duktape.create()
|
||||
val encryptedServer = script.substringAfter("const server = ").substringBefore(";")
|
||||
val batojs = duktape.evaluate(script.substringAfter("const batojs = ").substringBefore(";")).toString()
|
||||
val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($encryptedServer, \"$batojs\").toString(CryptoJS.enc.Utf8);"
|
||||
val server = duktape.evaluate(decryptScript).toString().replace("\"", "")
|
||||
duktape.close()
|
||||
|
||||
val imgArray = JSONArray(script.substringAfter("const images = ").substringBefore(";"))
|
||||
if (imgArray != null) {
|
||||
if (script.contains("bato.to/images")) {
|
||||
for (i in 0 until imgArray.length()) {
|
||||
val imgUrl = imgArray.get(i)
|
||||
pages.add(Page(i, "", "$imgUrl"))
|
||||
}
|
||||
} else {
|
||||
for (i in 0 until imgArray.length()) {
|
||||
val imgUrl = imgArray.get(i)
|
||||
if (server.startsWith("http"))
|
||||
pages.add(Page(i, "", "${server}$imgUrl"))
|
||||
else
|
||||
pages.add(Page(i, "", "https:${server}$imgUrl"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
private val cryptoJS by lazy {
|
||||
client.newCall(
|
||||
GET(
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js",
|
||||
headers
|
||||
)
|
||||
).execute().body()!!.string()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
|
||||
|
||||
private class OriginFilter(genres: List<Tag>) : Filter.Group<Tag>("Origin", genres)
|
||||
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
|
||||
|
||||
private class ChapterFilter : UriPartFilter(
|
||||
"Chapters",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("1 ~ 9", "1-9"),
|
||||
Pair("10 ~ 29", "10-29"),
|
||||
Pair("30 ~ 99", "30-99"),
|
||||
Pair("100 ~ 199", "100-199"),
|
||||
Pair("200+", "200"),
|
||||
Pair("100+", "100"),
|
||||
Pair("50+", "50"),
|
||||
Pair("10+", "10"),
|
||||
Pair("1+", "1")
|
||||
)
|
||||
)
|
||||
|
||||
private class SortBy : UriPartFilter(
|
||||
"Sorts By",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("A-Z", "title.az"),
|
||||
Pair("Z-A", "title"),
|
||||
Pair("Last Updated", "update"),
|
||||
Pair("Oldest Updated", "updated.az"),
|
||||
Pair("Newest Added", "create"),
|
||||
Pair("Oldest Added", "create.az"),
|
||||
Pair("Most Views Totally", "views_a"),
|
||||
Pair("Most Views 365 days", "views_y"),
|
||||
Pair("Most Views 30 days", "views_m"),
|
||||
Pair("Most Views 7 days", "views_w"),
|
||||
Pair("Most Views 24 hours", "views_d"),
|
||||
Pair("Most Views 60 minutes", "views_h"),
|
||||
Pair("Least Views Totally", "views_a.az"),
|
||||
Pair("Least Views 365 days", "views_y.az"),
|
||||
Pair("Least Views 30 days", "views_m.az"),
|
||||
Pair("Least Views 7 days", "views_w.az"),
|
||||
Pair("Least Views 24 hours", "views_d.az"),
|
||||
Pair("Least Views 60 minutes", "views_h.az")
|
||||
)
|
||||
)
|
||||
|
||||
private class StatusFilter : UriPartFilter(
|
||||
"Status",
|
||||
arrayOf(
|
||||
Pair("<select>", ""),
|
||||
Pair("Pending", "pending"),
|
||||
Pair("Ongoing", "ongoing"),
|
||||
Pair("Completed", "completed"),
|
||||
Pair("Hiatus", "hiatus"),
|
||||
Pair("Cancelled", "cancelled")
|
||||
)
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("NOTE: Ignored if using text search!"),
|
||||
Filter.Separator(),
|
||||
ChapterFilter(),
|
||||
SortBy(),
|
||||
StatusFilter(),
|
||||
OriginFilter(getOriginList()),
|
||||
GenreFilter(getGenreList())
|
||||
)
|
||||
|
||||
private fun getOriginList() = listOf(
|
||||
Tag("my"),
|
||||
Tag("ceb"),
|
||||
Tag("zh"),
|
||||
Tag("zh_hk"),
|
||||
Tag("en"),
|
||||
Tag("en_us"),
|
||||
Tag("fil"),
|
||||
Tag("id"),
|
||||
Tag("it"),
|
||||
Tag("ja"),
|
||||
Tag("ko"),
|
||||
Tag("ms"),
|
||||
Tag("pt_br"),
|
||||
Tag("th"),
|
||||
Tag("vi")
|
||||
)
|
||||
|
||||
private fun getGenreList() = listOf(
|
||||
Genre("Artbook"),
|
||||
Genre("Cartoon"),
|
||||
Genre("Comic"),
|
||||
Genre("Doujinshi"),
|
||||
Genre("Imageset"),
|
||||
Genre("Manga"),
|
||||
Genre("Manhua"),
|
||||
Genre("Manhwa"),
|
||||
Genre("Webtoon"),
|
||||
Genre("Western"),
|
||||
Genre("Josei"),
|
||||
Genre("Seinen"),
|
||||
Genre("Shoujo"),
|
||||
Genre("Shoujo_Ai"),
|
||||
Genre("Shounen"),
|
||||
Genre("Shounen_Ai"),
|
||||
Genre("Yaoi"),
|
||||
Genre("Yuri"),
|
||||
Genre("Ecchi"),
|
||||
Genre("Mature"),
|
||||
Genre("Adult"),
|
||||
Genre("Gore"),
|
||||
Genre("Violence"),
|
||||
Genre("Smut"),
|
||||
Genre("Hentai"),
|
||||
Genre("4_Koma"),
|
||||
Genre("Action"),
|
||||
Genre("Adaptation"),
|
||||
Genre("Adventure"),
|
||||
Genre("Aliens"),
|
||||
Genre("Animals"),
|
||||
Genre("Anthology"),
|
||||
Genre("Comedy"),
|
||||
Genre("Cooking"),
|
||||
Genre("Crime"),
|
||||
Genre("Crossdressing"),
|
||||
Genre("Delinquents"),
|
||||
Genre("Dementia"),
|
||||
Genre("Demons"),
|
||||
Genre("Drama"),
|
||||
Genre("Fantasy"),
|
||||
Genre("Fan_Colored"),
|
||||
Genre("Full_Color"),
|
||||
Genre("Game"),
|
||||
Genre("Gender_Bender"),
|
||||
Genre("Genderswap"),
|
||||
Genre("Ghosts"),
|
||||
Genre("Gyaru"),
|
||||
Genre("Harem"),
|
||||
Genre("Harlequin"),
|
||||
Genre("Historical"),
|
||||
Genre("Horror"),
|
||||
Genre("Incest"),
|
||||
Genre("Isekai"),
|
||||
Genre("Kids"),
|
||||
Genre("Loli"),
|
||||
Genre("Lolicon"),
|
||||
Genre("Magic"),
|
||||
Genre("Magical_Girls"),
|
||||
Genre("Martial_Arts"),
|
||||
Genre("Mecha"),
|
||||
Genre("Medical"),
|
||||
Genre("Military"),
|
||||
Genre("Monster_Girls"),
|
||||
Genre("Monsters"),
|
||||
Genre("Music"),
|
||||
Genre("Mystery"),
|
||||
Genre("Netorare"),
|
||||
Genre("Ninja"),
|
||||
Genre("Office_Workers"),
|
||||
Genre("Oneshot"),
|
||||
Genre("Parody"),
|
||||
Genre("Philosophical"),
|
||||
Genre("Police"),
|
||||
Genre("Post_Apocalyptic"),
|
||||
Genre("Psychological"),
|
||||
Genre("Reincarnation"),
|
||||
Genre("Reverse_Harem"),
|
||||
Genre("Romance"),
|
||||
Genre("Samurai"),
|
||||
Genre("School_Life"),
|
||||
Genre("Sci_Fi"),
|
||||
Genre("Shota"),
|
||||
Genre("Shotacon"),
|
||||
Genre("Slice_Of_Life"),
|
||||
Genre("SM_BDSM"),
|
||||
Genre("Space"),
|
||||
Genre("Sports"),
|
||||
Genre("Super_Power"),
|
||||
Genre("Superhero"),
|
||||
Genre("Supernatural"),
|
||||
Genre("Survival"),
|
||||
Genre("Thriller"),
|
||||
Genre("Time_Travel"),
|
||||
Genre("Tragedy"),
|
||||
Genre("Vampires"),
|
||||
Genre("Video_Games"),
|
||||
Genre("Virtual_Reality"),
|
||||
Genre("Wuxia"),
|
||||
Genre("Xianxia"),
|
||||
Genre("Xuanhuan"),
|
||||
Genre("Zombies"),
|
||||
Genre("award_winning"),
|
||||
Genre("youkai"),
|
||||
Genre("uncategorized")
|
||||
)
|
||||
|
||||
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||
fun toUriPart() = vals[state].second
|
||||
}
|
||||
|
||||
private class Tag(name: String) : Filter.CheckBox(name)
|
||||
private class Genre(name: String) : Filter.TriState(name)
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.batoto
|
||||
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
@Nsfw
|
||||
class BatoToFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = languages.map { BatoTo(it.first, it.second) }
|
||||
}
|
||||
|
||||
private val languages = listOf(
|
||||
//commented langueges do currently not exist on Bato.to but haven in the past
|
||||
Pair("all",""),
|
||||
Pair("ar", "ar"),
|
||||
Pair("bg", "bg"),
|
||||
Pair("cs", "cs"),
|
||||
Pair("da", "da"),
|
||||
Pair("de", "de"),
|
||||
Pair("el", "el"),
|
||||
Pair("en", "en"),
|
||||
Pair("en-US", "en_us"),
|
||||
Pair("es", "es"),
|
||||
Pair("es-419", "es_419"),
|
||||
Pair("eu", "eu"),
|
||||
Pair("fa", "fa"),
|
||||
Pair("fi", "fi"),
|
||||
Pair("fil", "fil"),
|
||||
Pair("fr", "fr"),
|
||||
Pair("he", "he"),
|
||||
//Pair("hi", "hi"),
|
||||
Pair("hr", "hr"),
|
||||
Pair("hu", "hu"),
|
||||
Pair("id", "id"),
|
||||
Pair("it", "it"),
|
||||
Pair("ja", "ja"),
|
||||
Pair("ko", "ko"),
|
||||
//Pair("ku", "ku"),
|
||||
Pair("ml", "ml"),
|
||||
Pair("mn", "mn"),
|
||||
Pair("ms", "ms"),
|
||||
Pair("my", "my"),
|
||||
Pair("nl", "nl"),
|
||||
Pair("no", "no"),
|
||||
Pair("pl", "pl"),
|
||||
Pair("pt", "pt"),
|
||||
Pair("pt-BR", "pt_br"),
|
||||
Pair("pt-PT", "pt_pt"),
|
||||
Pair("ro", "ro"),
|
||||
Pair("ru", "ru"),
|
||||
Pair("th", "th"),
|
||||
Pair("tr", "tr"),
|
||||
Pair("uk", "uk"),
|
||||
Pair("vi", "vi"),
|
||||
//Pair("xh", "xh"),
|
||||
Pair("zh", "zh"),
|
||||
Pair("zh-rHK", "zh_hk"),
|
||||
Pair("zh-rTW", "zh_tw"),
|
||||
Pair("zu", "zu"),
|
||||
)
|
@ -1,57 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.extension">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.cubari.CubariUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<!-- We need another intent filter so the /a/..* shortcut -->
|
||||
<!-- doesn't pollute the cubari one, since they work in any combination -->
|
||||
<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="*cubari.moe"
|
||||
android:pathPattern="/read/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="*cubari.moe"
|
||||
android:pathPattern="/proxy/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<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="*guya.moe"
|
||||
android:pathPattern="/proxy/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
<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="*imgur.com"
|
||||
android:pathPattern="/a/..*"
|
||||
android:scheme="https" />
|
||||
|
||||
<data
|
||||
android:host="*imgur.com"
|
||||
android:pathPattern="/gallery/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,12 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Cubari'
|
||||
pkgNameSuffix = "all.cubari"
|
||||
extClass = '.CubariFactory'
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB |
@ -1,354 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.cubari
|
||||
|
||||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
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 okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import rx.Observable
|
||||
|
||||
open class Cubari(override val lang: String) : HttpSource() {
|
||||
|
||||
final override val name = "Cubari"
|
||||
final override val baseUrl = "https://cubari.moe"
|
||||
final override val supportsLatest = true
|
||||
|
||||
override fun headersBuilder() = Headers.Builder().apply {
|
||||
add(
|
||||
"User-Agent",
|
||||
"(Android ${Build.VERSION.RELEASE}; " +
|
||||
"${Build.MANUFACTURER} ${Build.MODEL}) " +
|
||||
"Tachiyomi/${BuildConfig.VERSION_NAME} " +
|
||||
Build.ID
|
||||
)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/", headers)
|
||||
}
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
return client.newBuilder()
|
||||
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
|
||||
.build()!!
|
||||
.newCall(latestUpdatesRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response -> latestUpdatesParse(response) }
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
return parseMangaList(JSONArray(response.body()!!.string()), SortType.UNPINNED)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/", headers)
|
||||
}
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newBuilder()
|
||||
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
|
||||
.build()!!
|
||||
.newCall(popularMangaRequest(page))
|
||||
.asObservableSuccess()
|
||||
.map { response -> popularMangaParse(response) }
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
return parseMangaList(JSONArray(response.body()!!.string()), SortType.PINNED)
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response -> mangaDetailsParse(response, manga) }
|
||||
}
|
||||
|
||||
// Called when the series is loaded, or when opening in browser
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
return GET("$baseUrl${manga.url}", headers)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
throw Exception("Unused")
|
||||
}
|
||||
|
||||
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
|
||||
return parseMangaFromApi(JSONObject(response.body()!!.string()), manga)
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(chapterListRequest(manga))
|
||||
.asObservableSuccess()
|
||||
.map { response -> chapterListParse(response, manga) }
|
||||
}
|
||||
|
||||
// Gets the chapter list based on the series being viewed
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val urlComponents = manga.url.split("/")
|
||||
val source = urlComponents[2]
|
||||
val slug = urlComponents[3]
|
||||
|
||||
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
throw Exception("Unused")
|
||||
}
|
||||
|
||||
// Called after the request
|
||||
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
|
||||
val res = response.body()!!.string()
|
||||
return parseChapterList(res, manga)
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return when {
|
||||
chapter.url.contains("/chapter/") -> {
|
||||
client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
directPageListParse(response)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
seriesJsonPageListParse(response, chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
return when {
|
||||
chapter.url.contains("/chapter/") -> {
|
||||
GET("$baseUrl${chapter.url}", headers)
|
||||
}
|
||||
else -> {
|
||||
var url = chapter.url.split("/")
|
||||
val source = url[2]
|
||||
val slug = url[3]
|
||||
|
||||
GET("$baseUrl/read/api/$source/series/$slug/", headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun directPageListParse(response: Response): List<Page> {
|
||||
val res = response.body()!!.string()
|
||||
val pages = JSONArray(res)
|
||||
val pageArray = ArrayList<Page>()
|
||||
|
||||
for (i in 0 until pages.length()) {
|
||||
val page = if (pages.optJSONObject(i) != null) {
|
||||
pages.getJSONObject(i).getString("src")
|
||||
} else {
|
||||
pages[i]
|
||||
}
|
||||
pageArray.add(Page(i + 1, "", page.toString()))
|
||||
}
|
||||
return pageArray
|
||||
}
|
||||
|
||||
private fun seriesJsonPageListParse(response: Response, chapter: SChapter): List<Page> {
|
||||
val res = response.body()!!.string()
|
||||
val json = JSONObject(res)
|
||||
val groups = json.getJSONObject("groups")
|
||||
val groupIter = groups.keys()
|
||||
val groupMap = HashMap<String, String>()
|
||||
|
||||
while (groupIter.hasNext()) {
|
||||
val groupKey = groupIter.next()
|
||||
groupMap[groups.getString(groupKey)] = groupKey
|
||||
}
|
||||
|
||||
val chapters = json.getJSONObject("chapters")
|
||||
|
||||
val pages = if (chapters.has(chapter.chapter_number.toString())) {
|
||||
chapters
|
||||
.getJSONObject(chapter.chapter_number.toString())
|
||||
.getJSONObject("groups")
|
||||
.getJSONArray(groupMap[chapter.scanlator])
|
||||
} else {
|
||||
chapters
|
||||
.getJSONObject(chapter.chapter_number.toInt().toString())
|
||||
.getJSONObject("groups")
|
||||
.getJSONArray(groupMap[chapter.scanlator])
|
||||
}
|
||||
val pageArray = ArrayList<Page>()
|
||||
for (i in 0 until pages.length()) {
|
||||
val page = if (pages.optJSONObject(i) != null) {
|
||||
pages.getJSONObject(i).getString("src")
|
||||
} else {
|
||||
pages[i]
|
||||
}
|
||||
pageArray.add(Page(i + 1, "", page.toString()))
|
||||
}
|
||||
return pageArray
|
||||
}
|
||||
|
||||
// Stub
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
throw Exception("Unused")
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return when {
|
||||
query.startsWith(PROXY_PREFIX) -> {
|
||||
val trimmedQuery = query.removePrefix(PROXY_PREFIX)
|
||||
// Only tag for recently read on search
|
||||
client.newBuilder()
|
||||
.addInterceptor(RemoteStorageUtils.TagInterceptor())
|
||||
.build()!!
|
||||
.newCall(searchMangaRequest(page, trimmedQuery, filters))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, trimmedQuery)
|
||||
}
|
||||
}
|
||||
else -> throw Exception(SEARCH_FALLBACK_MSG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
try {
|
||||
val queryFragments = query.split("/")
|
||||
val source = queryFragments[0]
|
||||
val slug = queryFragments[1]
|
||||
|
||||
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
|
||||
} catch (e: Exception) {
|
||||
throw Exception(SEARCH_FALLBACK_MSG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
throw Exception("Unused")
|
||||
}
|
||||
|
||||
private fun searchMangaParse(response: Response, query: String): MangasPage {
|
||||
return parseSearchList(JSONObject(response.body()!!.string()), query)
|
||||
}
|
||||
|
||||
// ------------- Helpers and whatnot ---------------
|
||||
|
||||
private fun parseChapterList(payload: String, manga: SManga): List<SChapter> {
|
||||
val json = JSONObject(payload)
|
||||
val groups = json.getJSONObject("groups")
|
||||
val chapters = json.getJSONObject("chapters")
|
||||
|
||||
val chapterList = ArrayList<SChapter>()
|
||||
|
||||
val iter = chapters.keys()
|
||||
|
||||
while (iter.hasNext()) {
|
||||
val chapterNum = iter.next()
|
||||
val chapterObj = chapters.getJSONObject(chapterNum)
|
||||
val chapterGroups = chapterObj.getJSONObject("groups")
|
||||
val groupsIter = chapterGroups.keys()
|
||||
|
||||
while (groupsIter.hasNext()) {
|
||||
val groupNum = groupsIter.next()
|
||||
val chapter = SChapter.create()
|
||||
|
||||
chapter.scanlator = groups.getString(groupNum)
|
||||
if (chapterObj.has("release_date")) {
|
||||
chapter.date_upload =
|
||||
chapterObj.getJSONObject("release_date").getLong(groupNum) * 1000
|
||||
}
|
||||
chapter.name = chapterNum + " - " + chapterObj.getString("title")
|
||||
chapter.chapter_number = chapterNum.toFloat()
|
||||
chapter.url =
|
||||
if (chapterGroups.optJSONArray(groupNum) != null) {
|
||||
"${manga.url}/$chapterNum/$groupNum"
|
||||
} else {
|
||||
chapterGroups.getString(groupNum)
|
||||
}
|
||||
chapterList.add(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
return chapterList.reversed()
|
||||
}
|
||||
|
||||
private fun parseMangaList(payload: JSONArray, sortType: SortType): MangasPage {
|
||||
val mangas = ArrayList<SManga>()
|
||||
|
||||
for (i in 0 until payload.length()) {
|
||||
val json = payload.getJSONObject(i)
|
||||
val pinned = json.getBoolean("pinned")
|
||||
|
||||
if (sortType == SortType.PINNED && pinned) {
|
||||
mangas.add(parseMangaFromRemoteStorage(json))
|
||||
} else if (sortType == SortType.UNPINNED && !pinned) {
|
||||
mangas.add(parseMangaFromRemoteStorage(json))
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private fun parseSearchList(payload: JSONObject, query: String): MangasPage {
|
||||
val mangas = ArrayList<SManga>()
|
||||
val tempManga = SManga.create()
|
||||
tempManga.url = "/read/$query"
|
||||
mangas.add(parseMangaFromApi(payload, tempManga))
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private fun parseMangaFromRemoteStorage(json: JSONObject): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.title = json.getString("title")
|
||||
manga.artist = json.optString("artist", ARTIST_FALLBACK)
|
||||
manga.author = json.optString("author", AUTHOR_FALLBACK)
|
||||
manga.description = json.optString("description", DESCRIPTION_FALLBACK)
|
||||
manga.url = json.getString("url")
|
||||
manga.thumbnail_url = json.getString("coverUrl")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseMangaFromApi(json: JSONObject, mangaReference: SManga): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.title = json.getString("title")
|
||||
manga.artist = json.optString("artist", ARTIST_FALLBACK)
|
||||
manga.author = json.optString("author", AUTHOR_FALLBACK)
|
||||
manga.description = json.optString("description", DESCRIPTION_FALLBACK)
|
||||
manga.url = mangaReference.url
|
||||
manga.thumbnail_url = json.optString("cover", "")
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
// ----------------- Things we aren't supporting -----------------
|
||||
|
||||
override fun imageUrlParse(response: Response): String {
|
||||
throw Exception("imageUrlParse not supported.")
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PROXY_PREFIX = "cubari:"
|
||||
|
||||
const val AUTHOR_FALLBACK = "Unknown"
|
||||
const val ARTIST_FALLBACK = "Unknown"
|
||||
const val DESCRIPTION_FALLBACK = "No description."
|
||||
|
||||
const val SEARCH_FALLBACK_MSG = "Unable to parse. Is your query in the format of $PROXY_PREFIX<source>/<slug>?"
|
||||
|
||||
enum class SortType {
|
||||
PINNED,
|
||||
UNPINNED
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.cubari
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class CubariFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
Cubari("en"),
|
||||
Cubari("all"),
|
||||
Cubari("other")
|
||||
)
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.cubari
|
||||
|
||||
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 CubariUrlActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val host = intent?.data?.host
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
|
||||
if (host != null && pathSegments != null) {
|
||||
val query = when (host) {
|
||||
"m.imgur.com", "imgur.com" -> fromImgur(pathSegments)
|
||||
else -> fromCubari(pathSegments)
|
||||
}
|
||||
|
||||
if (query == null) {
|
||||
Log.e("CubariUrlActivity", "Unable to parse URI from intent $intent")
|
||||
finish()
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", query)
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("CubariUrlActivity", e.toString())
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun fromImgur(pathSegments: List<String>): String? {
|
||||
if (pathSegments.size >= 2) {
|
||||
val id = pathSegments[1]
|
||||
|
||||
return "${Cubari.PROXY_PREFIX}imgur/$id"
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun fromCubari(pathSegments: MutableList<String>): String? {
|
||||
return if (pathSegments.size >= 3) {
|
||||
val source = pathSegments[1]
|
||||
val slug = pathSegments[2]
|
||||
"${Cubari.PROXY_PREFIX}$source/$slug"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.cubari
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class RemoteStorageUtils {
|
||||
abstract class GenericInterceptor(private val transparent: Boolean) : Interceptor {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
abstract val jsScript: String
|
||||
|
||||
abstract fun urlModifier(originalUrl: String): String
|
||||
|
||||
internal class JsInterface(private val latch: CountDownLatch, var payload: String = "") {
|
||||
@JavascriptInterface
|
||||
fun passPayload(passedPayload: String) {
|
||||
payload = passedPayload
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
try {
|
||||
val originalRequest = chain.request()
|
||||
val originalResponse = chain.proceed(originalRequest)
|
||||
return proceedWithWebView(originalRequest, originalResponse)
|
||||
} catch (e: Exception) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
||||
private fun proceedWithWebView(request: Request, response: Response): Response {
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
var webView: WebView? = null
|
||||
|
||||
val origRequestUrl = request.url().toString()
|
||||
val headers = request.headers().toMultimap().mapValues {
|
||||
it.value.getOrNull(0) ?: ""
|
||||
}.toMutableMap()
|
||||
val jsInterface = JsInterface(latch)
|
||||
|
||||
handler.post {
|
||||
val webview = WebView(Injekt.get<Application>())
|
||||
webView = webview
|
||||
with(webview.settings) {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
databaseEnabled = true
|
||||
useWideViewPort = false
|
||||
loadWithOverviewMode = false
|
||||
userAgentString = request.header("User-Agent")
|
||||
}
|
||||
|
||||
webview.addJavascriptInterface(jsInterface, "android")
|
||||
|
||||
webview.webViewClient = object : WebViewClient() {
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
view.evaluateJavascript(jsScript) {}
|
||||
}
|
||||
if (transparent) {
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
webview.loadUrl(urlModifier(origRequestUrl), headers)
|
||||
}
|
||||
|
||||
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
|
||||
|
||||
handler.postDelayed(
|
||||
{ webView?.destroy() },
|
||||
DELAY_MILLIS * (if (transparent) 2 else 1)
|
||||
)
|
||||
|
||||
return if (transparent) {
|
||||
response
|
||||
} else {
|
||||
response.newBuilder().body(ResponseBody.create(response.body()?.contentType(), jsInterface.payload)).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TagInterceptor : GenericInterceptor(true) {
|
||||
override val jsScript: String = """
|
||||
let dispatched = false;
|
||||
window.addEventListener('history-ready', function () {
|
||||
if (!dispatched) {
|
||||
dispatched = true;
|
||||
Promise.all(
|
||||
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
|
||||
).then(e => {
|
||||
window.android.passPayload(JSON.stringify(e.flatMap(e => e)))
|
||||
});
|
||||
}
|
||||
});
|
||||
tag();
|
||||
"""
|
||||
|
||||
override fun urlModifier(originalUrl: String): String {
|
||||
return originalUrl.replace("/api/", "/").replace("/series/", "/")
|
||||
}
|
||||
}
|
||||
|
||||
class HomeInterceptor : GenericInterceptor(false) {
|
||||
override val jsScript: String = """
|
||||
let dispatched = false;
|
||||
(function () {
|
||||
if (!dispatched) {
|
||||
dispatched = true;
|
||||
Promise.all(
|
||||
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
|
||||
).then(e => {
|
||||
window.android.passPayload(JSON.stringify(e.flatMap(e => e) ) )
|
||||
});
|
||||
}
|
||||
})();
|
||||
"""
|
||||
|
||||
override fun urlModifier(originalUrl: String): String {
|
||||
return originalUrl
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TIMEOUT_SEC: Long = 10
|
||||
const val DELAY_MILLIS: Long = 10000
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
@ -1,12 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'Dragon Ball Multiverse'
|
||||
pkgNameSuffix = 'all.dragonball_multiverse'
|
||||
extClass = '.DbMFactory'
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 408 KiB |
@ -1,83 +0,0 @@
|
||||
@file:Suppress("ClassName")
|
||||
|
||||
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class DbMFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
DbMultiverseEN(),
|
||||
DbMultiverseFR(),
|
||||
DbMultiverseJP(),
|
||||
DbMultiverseCN(),
|
||||
DbMultiverseES(),
|
||||
DbMultiverseIT(),
|
||||
DbMultiversePT(),
|
||||
DbMultiverseDE(),
|
||||
DbMultiversePL(),
|
||||
DbMultiverseNL(),
|
||||
DbMultiverseFR_PA(),
|
||||
DbMultiverseTR_TR(),
|
||||
DbMultiversePT_BR(),
|
||||
DbMultiverseHU_HU(),
|
||||
DbMultiverseGA_ES(),
|
||||
DbMultiverseCT_CT(),
|
||||
DbMultiverseNO_NO(),
|
||||
DbMultiverseRU_RU(),
|
||||
DbMultiverseRO_RO(),
|
||||
DbMultiverseEU_EH(),
|
||||
DbMultiverseLT_LT(),
|
||||
DbMultiverseHR_HR(),
|
||||
DbMultiverseKR_KR(),
|
||||
DbMultiverseFI_FI(),
|
||||
DbMultiverseHE_HE(),
|
||||
DbMultiverseBG_BG(),
|
||||
DbMultiverseSV_SE(),
|
||||
DbMultiverseGR_GR(),
|
||||
DbMultiverseES_CO(),
|
||||
DbMultiverseAR_JO(),
|
||||
DbMultiverseTL_PI(),
|
||||
DbMultiverseLA_LA(),
|
||||
DbMultiverseDA_DK(),
|
||||
DbMultiverseCO_FR(),
|
||||
DbMultiverseBR_FR(),
|
||||
DbMultiverseXX_VE()
|
||||
)
|
||||
}
|
||||
|
||||
class DbMultiverseFR : DbMultiverse("fr", "fr")
|
||||
class DbMultiverseJP : DbMultiverse("ja", "jp")
|
||||
class DbMultiverseCN : DbMultiverse("zh", "cn")
|
||||
class DbMultiverseES : DbMultiverse("es", "es")
|
||||
class DbMultiverseIT : DbMultiverse("it", "it")
|
||||
class DbMultiversePT : DbMultiverse("pt", "pt")
|
||||
class DbMultiverseDE : DbMultiverse("de", "de")
|
||||
class DbMultiversePL : DbMultiverse("pl", "pl")
|
||||
class DbMultiverseNL : DbMultiverse("nl", "nl")
|
||||
class DbMultiverseFR_PA : DbMultiverse("fr", "fr_PA")
|
||||
class DbMultiverseTR_TR : DbMultiverse("tr", "tr_TR")
|
||||
class DbMultiversePT_BR : DbMultiverse("pt-BR", "pt_BR")
|
||||
class DbMultiverseHU_HU : DbMultiverse("hu", "hu_HU")
|
||||
class DbMultiverseGA_ES : DbMultiverse("ga", "ga_ES")
|
||||
class DbMultiverseCT_CT : DbMultiverse("ca", "ct_CT")
|
||||
class DbMultiverseNO_NO : DbMultiverse("no", "no_NO")
|
||||
class DbMultiverseRU_RU : DbMultiverse("ru", "ru_RU")
|
||||
class DbMultiverseRO_RO : DbMultiverse("ro", "ro_RO")
|
||||
class DbMultiverseEU_EH : DbMultiverse("eu", "eu_EH")
|
||||
class DbMultiverseLT_LT : DbMultiverse("lt", "lt_LT")
|
||||
class DbMultiverseHR_HR : DbMultiverse("hr", "hr_HR")
|
||||
class DbMultiverseKR_KR : DbMultiverse("ko", "kr_KR")
|
||||
class DbMultiverseFI_FI : DbMultiverse("fi", "fi_FI")
|
||||
class DbMultiverseHE_HE : DbMultiverse("he", "he_HE")
|
||||
class DbMultiverseBG_BG : DbMultiverse("bg", "bg_BG")
|
||||
class DbMultiverseSV_SE : DbMultiverse("sv", "sv_SE")
|
||||
class DbMultiverseGR_GR : DbMultiverse("el", "gr_GR")
|
||||
class DbMultiverseES_CO : DbMultiverse("es-419", "es_CO")
|
||||
class DbMultiverseAR_JO : DbMultiverse("ar", "ar_JO")
|
||||
class DbMultiverseTL_PI : DbMultiverse("fil", "tl_PI")
|
||||
class DbMultiverseLA_LA : DbMultiverse("la", "la_LA")
|
||||
class DbMultiverseDA_DK : DbMultiverse("da", "da_DK")
|
||||
class DbMultiverseCO_FR : DbMultiverse("co", "co_FR")
|
||||
class DbMultiverseBR_FR : DbMultiverse("br", "br_FR")
|
||||
class DbMultiverseXX_VE : DbMultiverse("vec", "xx_VE")
|
@ -1,103 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
|
||||
|
||||
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.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
abstract class DbMultiverse(override val lang: String, private val internalLang: String) : ParsedHttpSource() {
|
||||
|
||||
override val name =
|
||||
if (internalLang.endsWith("_PA")) "Dragon Ball Multiverse Parody"
|
||||
else "Dragon Ball Multiverse"
|
||||
override val baseUrl = "https://www.dragonball-multiverse.com"
|
||||
override val supportsLatest = false
|
||||
|
||||
private fun chapterFromElement(element: Element, name: String): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(element.attr("abs:href"))
|
||||
chapter.name = name + element.text().let { num ->
|
||||
if (num.contains("-")) {
|
||||
"Pages $num"
|
||||
} else {
|
||||
"Page $num"
|
||||
}
|
||||
}
|
||||
return chapter
|
||||
}
|
||||
|
||||
override fun chapterListSelector(): String = "div.cadrelect.chapters a[href*=page-]"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
val document = response.asJsoup()
|
||||
|
||||
document.select("div[ch]").forEach { container ->
|
||||
container.select(chapterListSelector()).mapIndexed { i, chapter ->
|
||||
// Each page is its own chapter, add chapter name when a first page is mapped
|
||||
val name = if (i == 0) container.select("h4").text() + " - " else ""
|
||||
chapters.add(chapterFromElement(chapter, name))
|
||||
}
|
||||
}
|
||||
|
||||
return chapters.reversed()
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
return document.select("div#h_read img").mapIndexed { index, element ->
|
||||
Page(index, "", element.attr("abs:src"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga = createManga(document)
|
||||
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return Observable.just(MangasPage(listOf(createManga(null)), hasNextPage = false))
|
||||
}
|
||||
|
||||
private fun createManga(document: Document?) = SManga.create().apply {
|
||||
title = name
|
||||
status = SManga.ONGOING
|
||||
url = "/$internalLang/chapters.html"
|
||||
description = "Dragon Ball Multiverse (DBM) is a free online comic, made by a whole team of fans. It's our personal sequel to DBZ."
|
||||
thumbnail_url = document?.select("div[ch=\"1\"] img")?.attr("abs:src")
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
|
||||
|
||||
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun popularMangaNextPageSelector(): String? = throw UnsupportedOperationException()
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.just(MangasPage(emptyList(), false))
|
||||
|
||||
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
|
||||
|
||||
class DbMultiverseEN : DbMultiverse("en", "en")
|
@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="eu.kanade.tachiyomi.extension">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name=".all.ehentai.EHUrlActivity"
|
||||
android:excludeFromRecents="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="e-hentai.org"
|
||||
android:pathPattern="/g/..*/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@ -1,13 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'E-Hentai'
|
||||
pkgNameSuffix = 'all.ehentai'
|
||||
extClass = '.EHFactory'
|
||||
extVersionCode = 13
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 9.8 KiB |
@ -1,28 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
@Nsfw
|
||||
class EHFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = listOf(
|
||||
EHentai("ja", "japanese"),
|
||||
EHentai("en", "english"),
|
||||
EHentai("zh", "chinese"),
|
||||
EHentai("nl", "dutch"),
|
||||
EHentai("fr", "french"),
|
||||
EHentai("de", "german"),
|
||||
EHentai("hu", "hungarian"),
|
||||
EHentai("it", "italian"),
|
||||
EHentai("ko", "korean"),
|
||||
EHentai("pl", "polish"),
|
||||
EHentai("pt", "portuguese"),
|
||||
EHentai("ru", "russian"),
|
||||
EHentai("es", "spanish"),
|
||||
EHentai("th", "thai"),
|
||||
EHentai("vi", "vietnamese"),
|
||||
EHentai("none", "n/a"),
|
||||
EHentai("other", "other")
|
||||
)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
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://e-hentai.net/g/xxxxx/yyyyy/ intents and redirects them to
|
||||
* the main Tachiyomi process.
|
||||
*/
|
||||
class EHUrlActivity : Activity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 2) {
|
||||
val id = pathSegments[1]
|
||||
val key = pathSegments[2]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${EHentai.PREFIX_ID_SEARCH}$id/$key")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("EHUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("EHUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* Various utility methods used in the E-Hentai source
|
||||
*/
|
||||
|
||||
/**
|
||||
* Return null if String is blank, otherwise returns the original String
|
||||
* @returns null if the String is blank, otherwise returns the original String
|
||||
*/
|
||||
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
|
||||
null
|
||||
else
|
||||
this
|
||||
|
||||
/**
|
||||
* Ignores any exceptions thrown inside a block
|
||||
*/
|
||||
fun <T> ignore(expr: () -> T): T? {
|
||||
return try {
|
||||
expr()
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use '+' to append Strings onto a StringBuilder
|
||||
*/
|
||||
operator fun StringBuilder.plusAssign(other: String) {
|
||||
append(other)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts bytes into a human readable String
|
||||
*/
|
||||
fun humanReadableByteCount(bytes: Long, si: Boolean): String {
|
||||
val unit = if (si) 1000 else 1024
|
||||
if (bytes < unit) return "$bytes B"
|
||||
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
|
||||
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
|
||||
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
|
||||
}
|
||||
|
||||
private const val KB_FACTOR = 1000
|
||||
private const val KIB_FACTOR = 1024
|
||||
private const val MB_FACTOR = 1000 * KB_FACTOR
|
||||
private const val MIB_FACTOR = 1024 * KIB_FACTOR
|
||||
private const val GB_FACTOR = 1000 * MB_FACTOR
|
||||
private const val GIB_FACTOR = 1024 * MIB_FACTOR
|
||||
|
||||
/**
|
||||
* Parse human readable size Strings
|
||||
*/
|
||||
fun parseHumanReadableByteCount(arg0: String): Double? {
|
||||
val spaceNdx = arg0.indexOf(" ")
|
||||
val ret = arg0.substring(0 until spaceNdx).toDouble()
|
||||
when (arg0.substring(spaceNdx + 1)) {
|
||||
"GB" -> return ret * GB_FACTOR
|
||||
"GiB" -> return ret * GIB_FACTOR
|
||||
"MB" -> return ret * MB_FACTOR
|
||||
"MiB" -> return ret * MIB_FACTOR
|
||||
"KB" -> return ret * KB_FACTOR
|
||||
"KiB" -> return ret * KIB_FACTOR
|
||||
}
|
||||
return null
|
||||
}
|
@ -1,533 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import androidx.preference.CheckBoxPreference
|
||||
import androidx.preference.PreferenceScreen
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.Filter.CheckBox
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Group
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Select
|
||||
import eu.kanade.tachiyomi.source.model.Filter.Text
|
||||
import eu.kanade.tachiyomi.source.model.Filter.TriState
|
||||
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.CacheControl
|
||||
import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.net.URLEncoder
|
||||
import android.support.v7.preference.CheckBoxPreference as LegacyCheckBoxPreference
|
||||
import android.support.v7.preference.PreferenceScreen as LegacyPreferenceScreen
|
||||
|
||||
open class EHentai(override val lang: String, private val ehLang: String) : ConfigurableSource, HttpSource() {
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
override val name = "E-Hentai"
|
||||
|
||||
override val baseUrl = "https://e-hentai.org"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// true if lang is a "natural human language"
|
||||
private fun isLangNatural(): Boolean = lang !in listOf("none", "other")
|
||||
|
||||
private fun genericMangaParse(response: Response): MangasPage {
|
||||
val doc = response.asJsoup()
|
||||
val parsedMangas = doc.select("table.itg td.glname")
|
||||
.let { elements ->
|
||||
if (isLangNatural() && getEnforceLanguagePref()) {
|
||||
elements.filter { element ->
|
||||
// only accept elements with a language tag matching ehLang or without a language tag
|
||||
// could make this stricter and not accept elements without a language tag, possibly add a sharedpreference for it
|
||||
element.select("div[title^=language]").firstOrNull()?.let { it.text() == ehLang } ?: true
|
||||
}
|
||||
} else {
|
||||
elements
|
||||
}
|
||||
}
|
||||
.map {
|
||||
SManga.create().apply {
|
||||
// Get title
|
||||
it.select("a")?.first()?.apply {
|
||||
title = this.select(".glink").text()
|
||||
url = ExGalleryMetadata.normalizeUrl(attr("href"))
|
||||
}
|
||||
// Get image
|
||||
it.parent().select(".glthumb img")?.first().apply {
|
||||
thumbnail_url = this?.attr("data-src")?.nullIfBlank()
|
||||
?: this?.attr("src")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add to page if required
|
||||
val hasNextPage = doc.select("a[onclick=return false]").last()?.text() == ">"
|
||||
|
||||
return MangasPage(parsedMangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Chapter"
|
||||
chapter_number = 1f
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, "$baseUrl/${chapter.url}").map {
|
||||
it.mapIndexed { i, s ->
|
||||
Page(i, s)
|
||||
}
|
||||
}!!
|
||||
|
||||
/**
|
||||
* Recursively fetch chapter pages
|
||||
*/
|
||||
private fun fetchChapterPage(
|
||||
chapter: SChapter,
|
||||
np: String,
|
||||
pastUrls: List<String> = emptyList()
|
||||
): Observable<List<String>> {
|
||||
val urls = ArrayList(pastUrls)
|
||||
return chapterPageCall(np).flatMap {
|
||||
val jsoup = it.asJsoup()
|
||||
urls += parseChapterPage(jsoup)
|
||||
nextPageUrl(jsoup)?.let { string ->
|
||||
fetchChapterPage(chapter, string, urls)
|
||||
} ?: Observable.just(urls)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterPage(response: Element) = with(response) {
|
||||
select(".gdtm a").map {
|
||||
Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
|
||||
}.sortedBy(Pair<Int, String>::first).map { it.second }
|
||||
}
|
||||
|
||||
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
|
||||
private fun chapterPageRequest(np: String) = exGet(np, null, headers)
|
||||
|
||||
private fun nextPageUrl(element: Element) = element.select("a[onclick=return false]").last()?.let {
|
||||
if (it.text() == ">") it.attr("href") else null
|
||||
}
|
||||
|
||||
private fun languageTag(enforceLanguageFilter: Boolean = false): String {
|
||||
return if (enforceLanguageFilter || getEnforceLanguagePref()) "language:$ehLang" else ""
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int) = if (isLangNatural()) {
|
||||
exGet("$baseUrl/?f_search=${languageTag()}&f_srdd=5&f_sr=on", page)
|
||||
} else {
|
||||
latestUpdatesRequest(page)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val enforceLanguageFilter = filters.find { it is EnforceLanguageFilter }?.state == true
|
||||
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
|
||||
var modifiedQuery = when {
|
||||
!isLangNatural() -> query
|
||||
query.isBlank() -> languageTag(enforceLanguageFilter)
|
||||
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
|
||||
}
|
||||
modifiedQuery += filters.filterIsInstance<TagFilter>()
|
||||
.flatMap { it.markedTags() }
|
||||
.joinToString(",")
|
||||
.let { if (it.isNotEmpty()) ",$it" else it }
|
||||
uri.appendQueryParameter("f_search", modifiedQuery)
|
||||
filters.forEach {
|
||||
if (it is UriFilter) it.addToUri(uri)
|
||||
}
|
||||
return exGet(uri.toString(), page)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page)
|
||||
|
||||
override fun popularMangaParse(response: Response) = genericMangaParse(response)
|
||||
override fun searchMangaParse(response: Response) = genericMangaParse(response)
|
||||
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
|
||||
|
||||
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
|
||||
return GET(
|
||||
page?.let {
|
||||
addParam(url, "page", (page - 1).toString())
|
||||
} ?: url,
|
||||
additionalHeaders?.let { header ->
|
||||
val headers = headers.newBuilder()
|
||||
header.toMultimap().forEach { (t, u) ->
|
||||
u.forEach {
|
||||
headers.add(t, it)
|
||||
}
|
||||
}
|
||||
headers.build()
|
||||
} ?: headers
|
||||
).let {
|
||||
if (!cache) {
|
||||
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse gallery page to metadata model
|
||||
*/
|
||||
@SuppressLint("DefaultLocale")
|
||||
override fun mangaDetailsParse(response: Response) = with(response.asJsoup()) {
|
||||
with(ExGalleryMetadata()) {
|
||||
url = response.request().url().encodedPath()
|
||||
title = select("#gn").text().nullIfBlank()?.trim()
|
||||
|
||||
altTitle = select("#gj").text().nullIfBlank()?.trim()
|
||||
|
||||
// Thumbnail is set as background of element in style attribute
|
||||
thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
|
||||
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
|
||||
}
|
||||
genre = select("#gdc div").text().nullIfBlank()?.trim()?.toLowerCase()
|
||||
|
||||
uploader = select("#gdn").text().nullIfBlank()?.trim()
|
||||
|
||||
// Parse the table
|
||||
select("#gdd tr").forEach {
|
||||
it.select(".gdt1")
|
||||
.text()
|
||||
.nullIfBlank()
|
||||
?.trim()
|
||||
?.let { left ->
|
||||
it.select(".gdt2")
|
||||
.text()
|
||||
.nullIfBlank()
|
||||
?.trim()
|
||||
?.let { right ->
|
||||
ignore {
|
||||
when (
|
||||
left.removeSuffix(":")
|
||||
.toLowerCase()
|
||||
) {
|
||||
"posted" -> datePosted = EX_DATE_FORMAT.parse(right)?.time ?: 0
|
||||
"visible" -> visible = right.nullIfBlank()
|
||||
"language" -> {
|
||||
language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
|
||||
translated = right.endsWith(TR_SUFFIX, true)
|
||||
}
|
||||
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
|
||||
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
|
||||
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse ratings
|
||||
ignore {
|
||||
averageRating = select("#rating_label")
|
||||
.text()
|
||||
.removePrefix("Average:")
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toDouble()
|
||||
ratingCount = select("#rating_count")
|
||||
.text()
|
||||
.trim()
|
||||
.nullIfBlank()
|
||||
?.toInt()
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
tags.clear()
|
||||
select("#taglist tr").forEach {
|
||||
val namespace = it.select(".tc").text().removeSuffix(":")
|
||||
val currentTags = it.select("div").map { element ->
|
||||
Tag(
|
||||
element.text().trim(),
|
||||
element.hasClass("gtl")
|
||||
)
|
||||
}
|
||||
tags[namespace] = currentTags
|
||||
}
|
||||
|
||||
// Copy metadata to manga
|
||||
SManga.create().apply {
|
||||
copyTo(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/g/$id", headers)
|
||||
|
||||
private fun searchMangaByIdParse(response: Response, id: String): MangasPage {
|
||||
val details = mangaDetailsParse(response)
|
||||
details.url = "/g/$id/"
|
||||
return MangasPage(listOf(details), false)
|
||||
}
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
return if (query.startsWith(PREFIX_ID_SEARCH)) {
|
||||
val id = query.removePrefix(PREFIX_ID_SEARCH)
|
||||
client.newCall(searchMangaByIdRequest(id))
|
||||
.asObservableSuccess()
|
||||
.map { response -> searchMangaByIdParse(response, id) }
|
||||
} else {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
|
||||
|
||||
override fun pageListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
|
||||
|
||||
override fun imageUrlParse(response: Response): String = response.asJsoup().select("#img").attr("abs:src")
|
||||
|
||||
private val cookiesHeader by lazy {
|
||||
val cookies = mutableMapOf<String, String>()
|
||||
|
||||
// Setup settings
|
||||
val settings = mutableListOf<String>()
|
||||
|
||||
// Do not show popular right now pane as we can't parse it
|
||||
settings += "prn_n"
|
||||
|
||||
// Exclude every other language except the one we have selected
|
||||
settings += "xl_" + languageMappings.filter { it.first != ehLang }
|
||||
.flatMap { it.second }
|
||||
.joinToString("x")
|
||||
|
||||
cookies["uconfig"] = buildSettings(settings)
|
||||
|
||||
// Bypass "Offensive For Everyone" content warning
|
||||
cookies["nw"] = "1"
|
||||
|
||||
buildCookies(cookies)
|
||||
}
|
||||
|
||||
// Headers
|
||||
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader)!!
|
||||
|
||||
private fun buildSettings(settings: List<String?>) = settings.filterNotNull().joinToString(separator = "-")
|
||||
|
||||
private fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ", postfix = ";") {
|
||||
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
|
||||
}
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
private fun addParam(url: String, param: String, value: String) = Uri.parse(url)
|
||||
.buildUpon()
|
||||
.appendQueryParameter(param, value)
|
||||
.toString()
|
||||
|
||||
override val client = network.client.newBuilder()
|
||||
.cookieJar(CookieJar.NO_COOKIES)
|
||||
.addInterceptor { chain ->
|
||||
val newReq = chain
|
||||
.request()
|
||||
.newBuilder()
|
||||
.removeHeader("Cookie")
|
||||
.addHeader("Cookie", cookiesHeader)
|
||||
.build()
|
||||
|
||||
chain.proceed(newReq)
|
||||
}.build()!!
|
||||
|
||||
// Filters
|
||||
override fun getFilterList() = FilterList(
|
||||
EnforceLanguageFilter(getEnforceLanguagePref()),
|
||||
Watched(),
|
||||
GenreGroup(),
|
||||
TagFilter("Misc Tags", triStateBoxesFrom(miscTags), ""),
|
||||
TagFilter("Female Tags", triStateBoxesFrom(femaleTags), "female"),
|
||||
TagFilter("Male Tags", triStateBoxesFrom(maleTags), "male"),
|
||||
AdvancedGroup()
|
||||
)
|
||||
|
||||
class Watched : CheckBox("Watched List"), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state)
|
||||
builder.appendPath("watched")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
class GenreGroup : UriGroup<GenreOption>(
|
||||
"Genres",
|
||||
listOf(
|
||||
GenreOption("Dōjinshi", "doujinshi"),
|
||||
GenreOption("Manga", "manga"),
|
||||
GenreOption("Artist CG", "artistcg"),
|
||||
GenreOption("Game CG", "gamecg"),
|
||||
GenreOption("Western", "western"),
|
||||
GenreOption("Non-H", "non-h"),
|
||||
GenreOption("Image Set", "imageset"),
|
||||
GenreOption("Cosplay", "cosplay"),
|
||||
GenreOption("Asian Porn", "asianporn"),
|
||||
GenreOption("Misc", "misc")
|
||||
)
|
||||
)
|
||||
|
||||
class AdvancedOption(name: String, private val param: String, defValue: Boolean = false) : CheckBox(name, defValue), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state)
|
||||
builder.appendQueryParameter(param, "on")
|
||||
}
|
||||
}
|
||||
|
||||
open class PageOption(name: String, private val queryKey: String) : Text(name), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state.isNotBlank()) {
|
||||
if (builder.build().getQueryParameters("f_sp").isEmpty()) {
|
||||
builder.appendQueryParameter("f_sp", "on")
|
||||
}
|
||||
|
||||
builder.appendQueryParameter(queryKey, state.trim())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
|
||||
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
|
||||
|
||||
class RatingOption :
|
||||
Select<String>(
|
||||
"Minimum Rating",
|
||||
arrayOf(
|
||||
"Any",
|
||||
"2 stars",
|
||||
"3 stars",
|
||||
"4 stars",
|
||||
"5 stars"
|
||||
)
|
||||
),
|
||||
UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
if (state > 0) {
|
||||
builder.appendQueryParameter("f_srdd", (state + 1).toString())
|
||||
builder.appendQueryParameter("f_sr", "on")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit type arg for listOf() to workaround this: KT-16570
|
||||
class AdvancedGroup : UriGroup<Filter<*>>(
|
||||
"Advanced Options",
|
||||
listOf(
|
||||
AdvancedOption("Search Gallery Name", "f_sname", true),
|
||||
AdvancedOption("Search Gallery Tags", "f_stags", true),
|
||||
AdvancedOption("Search Gallery Description", "f_sdesc"),
|
||||
AdvancedOption("Search Torrent Filenames", "f_storr"),
|
||||
AdvancedOption("Only Show Galleries With Torrents", "f_sto"),
|
||||
AdvancedOption("Search Low-Power Tags", "f_sdt1"),
|
||||
AdvancedOption("Search Downvoted Tags", "f_sdt2"),
|
||||
AdvancedOption("Show Expunged Galleries", "f_sh"),
|
||||
RatingOption(),
|
||||
MinPagesOption(),
|
||||
MaxPagesOption()
|
||||
)
|
||||
)
|
||||
|
||||
private class EnforceLanguageFilter(default: Boolean) : CheckBox("Enforce language", default)
|
||||
|
||||
private val miscTags = "3d, already uploaded, anaglyph, animal on animal, animated, anthology, arisa mizuhara, artbook, ashiya noriko, bailey jay, body swap, caption, chouzuki maryou, christian godard, comic, compilation, dakimakura, fe galvao, ffm threesome, figure, forbidden content, full censorship, full color, game sprite, goudoushi, group, gunyou mikan, harada shigemitsu, hardcore, helly von valentine, higurashi rin, hololive, honey select, how to, incest, incomplete, ishiba yoshikazu, jessica nigri, kalinka fox, kanda midori, kira kira, kitami eri, kuroi hiroki, lenfried, lincy leaw, marie claude bourbonnais, matsunaga ayaka, me me me, missing cover, mmf threesome, mmt threesome, mosaic censorship, mtf threesome, multi-work series, no penetration, non-nude, novel, nudity only, oakazaki joe, out of order, paperchild, pm02 colon 20, poor grammar, radio comix, realporn, redraw, replaced, sakaki kasa, sample, saotome love, scanmark, screenshots, sinful goddesses, sketch lines, stereoscopic, story arc, takeuti ken, tankoubon, themeless, tikuma jukou, time stop, tsubaki zakuro, ttm threesome, twins, uncensored, vandych alex, variant set, watermarked, webtoon, western cg, western imageset, western non-h, yamato nadeshiko club, yui okada, yukkuri, zappa go"
|
||||
private val femaleTags = "ahegao, anal, angel, apron, bandages, bbw, bdsm, beauty mark, big areolae, big ass, big breasts, big clit, big lips, big nipples, bikini, blackmail, bloomers, blowjob, bodysuit, bondage, breast expansion, bukkake, bunny girl, business suit, catgirl, centaur, cheating, chinese dress, christmas, collar, corset, cosplaying, cowgirl, crossdressing, cunnilingus, dark skin, daughter, deepthroat, defloration, demon girl, double penetration, dougi, dragon, drunk, elf, exhibitionism, farting, females only, femdom, filming, fingering, fishnets, footjob, fox girl, furry, futanari, garter belt, ghost, giantess, glasses, gloves, goblin, gothic lolita, growth, guro, gyaru, hair buns, hairy, hairy armpits, handjob, harem, hidden sex, horns, huge breasts, humiliation, impregnation, incest, inverted nipples, kemonomimi, kimono, kissing, lactation, latex, leg lock, leotard, lingerie, lizard girl, maid, masked face, masturbation, midget, miko, milf, mind break, mind control, monster girl, mother, muscle, nakadashi, netorare, nose hook, nun, nurse, oil, paizuri, panda girl, pantyhose, piercing, pixie cut, policewoman, ponytail, pregnant, rape, rimjob, robot, scat, schoolgirl uniform, sex toys, shemale, sister, small breasts, smell, sole dickgirl, sole female, squirting, stockings, sundress, sweating, swimsuit, swinging, tail, tall girl, teacher, tentacles, thigh high boots, tomboy, transformation, twins, twintails, unusual pupils, urination, vore, vtuber, widow, wings, witch, wolf girl, x-ray, yuri, zombie"
|
||||
private val maleTags = "anal, bbm, big ass, big penis, bikini, blood, blowjob, bondage, catboy, cheating, chikan, condom, crab, crossdressing, dark skin, deepthroat, demon, dickgirl on male, dilf, dog boy, double anal, double penetration, dragon, drunk, exhibitionism, facial hair, feminization, footjob, fox boy, furry, glasses, group, guro, hairy, handjob, hidden sex, horns, huge penis, human on furry, kimono, lingerie, lizard guy, machine, maid, males only, masturbation, mmm threesome, monster, muscle, nakadashi, ninja, octopus, oni, pillory, policeman, possession, prostate massage, public use, schoolboy uniform, schoolgirl uniform, sex toys, shotacon, sleeping, snuff, sole male, stockings, sunglasses, swimsuit, tall man, tentacles, tomgirl, unusual pupils, virginity, waiter, x-ray, yaoi, zombie"
|
||||
|
||||
private fun triStateBoxesFrom(tagString: String): List<TagTriState> = tagString.split(", ").map { TagTriState(it) }
|
||||
|
||||
class TagTriState(tag: String) : TriState(tag)
|
||||
class TagFilter(name: String, private val triStateBoxes: List<TagTriState>, private val nameSpace: String) : Group<TagTriState>(name, triStateBoxes) {
|
||||
fun markedTags() = triStateBoxes.filter { it.isIncluded() }.map { "$nameSpace:${it.name}" } + triStateBoxes.filter { it.isExcluded() }.map { "-$nameSpace:${it.name}" }
|
||||
}
|
||||
|
||||
// map languages to their internal ids
|
||||
private val languageMappings = listOf(
|
||||
Pair("japanese", listOf("0", "1024", "2048")),
|
||||
Pair("english", listOf("1", "1025", "2049")),
|
||||
Pair("chinese", listOf("10", "1034", "2058")),
|
||||
Pair("dutch", listOf("20", "1044", "2068")),
|
||||
Pair("french", listOf("30", "1054", "2078")),
|
||||
Pair("german", listOf("40", "1064", "2088")),
|
||||
Pair("hungarian", listOf("50", "1074", "2098")),
|
||||
Pair("italian", listOf("60", "1084", "2108")),
|
||||
Pair("korean", listOf("70", "1094", "2118")),
|
||||
Pair("polish", listOf("80", "1104", "2128")),
|
||||
Pair("portuguese", listOf("90", "1114", "2138")),
|
||||
Pair("russian", listOf("100", "1124", "2148")),
|
||||
Pair("spanish", listOf("110", "1134", "2158")),
|
||||
Pair("thai", listOf("120", "1144", "2168")),
|
||||
Pair("vietnamese", listOf("130", "1154", "2178")),
|
||||
Pair("n/a", listOf("254", "1278", "2302")),
|
||||
Pair("other", listOf("255", "1279", "2303"))
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val QUERY_PREFIX = "?f_apply=Apply+Filter"
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
const val TR_SUFFIX = "TR"
|
||||
|
||||
// Preferences vals
|
||||
private const val ENFORCE_LANGUAGE_PREF_KEY = "ENFORCE_LANGUAGE"
|
||||
private const val ENFORCE_LANGUAGE_PREF_TITLE = "Enforce Language"
|
||||
private const val ENFORCE_LANGUAGE_PREF_SUMMARY = "If checked, forces browsing of manga matching a language tag"
|
||||
private const val ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE = false
|
||||
}
|
||||
|
||||
// Preferences
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
screen.addPreference(enforceLanguagePref)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: LegacyPreferenceScreen) {
|
||||
val enforceLanguagePref = LegacyCheckBoxPreference(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()
|
||||
}
|
||||
}
|
||||
screen.addPreference(enforceLanguagePref)
|
||||
}
|
||||
|
||||
private fun getEnforceLanguagePref(): Boolean = preferences.getBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* Gallery metadata storage model
|
||||
*/
|
||||
|
||||
class ExGalleryMetadata {
|
||||
var url: String? = null
|
||||
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
var title: String? = null
|
||||
var altTitle: String? = null
|
||||
|
||||
var genre: String? = null
|
||||
|
||||
var datePosted: Long? = null
|
||||
var parent: String? = null
|
||||
var visible: String? = null // Not a boolean
|
||||
var language: String? = null
|
||||
var translated: Boolean? = null
|
||||
var size: Long? = null
|
||||
var length: Int? = null
|
||||
var favorites: Int? = null
|
||||
var ratingCount: Int? = null
|
||||
var averageRating: Double? = null
|
||||
|
||||
var uploader: String? = null
|
||||
|
||||
val tags: MutableMap<String, List<Tag>> = mutableMapOf()
|
||||
|
||||
companion object {
|
||||
private fun splitGalleryUrl(url: String) = url.let {
|
||||
// Only parse URL if is full URL
|
||||
val pathSegments = if (it.startsWith("http"))
|
||||
Uri.parse(it).pathSegments
|
||||
else
|
||||
it.split('/')
|
||||
pathSegments.filterNot(String::isNullOrBlank)
|
||||
}
|
||||
|
||||
private fun galleryId(url: String) = splitGalleryUrl(url)[1]
|
||||
|
||||
private fun galleryToken(url: String) = splitGalleryUrl(url)[2]
|
||||
|
||||
private fun normalizeUrl(id: String, token: String) = "/g/$id/$token/?nw=always"
|
||||
|
||||
fun normalizeUrl(url: String) = normalizeUrl(galleryId(url), galleryToken(url))
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
private const val EH_ARTIST_NAMESPACE = "artist"
|
||||
private const val EH_AUTHOR_NAMESPACE = "author"
|
||||
|
||||
private val ONGOING_SUFFIX = arrayOf(
|
||||
"[ongoing]",
|
||||
"(ongoing)",
|
||||
"{ongoing}"
|
||||
)
|
||||
|
||||
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
|
||||
|
||||
fun ExGalleryMetadata.copyTo(manga: SManga) {
|
||||
url?.let { manga.url = it }
|
||||
thumbnailUrl?.let { manga.thumbnail_url = it }
|
||||
|
||||
(title ?: altTitle)?.let { manga.title = it }
|
||||
|
||||
// Set artist (if we can find one)
|
||||
tags[EH_ARTIST_NAMESPACE]?.let {
|
||||
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
|
||||
}
|
||||
// Set author (if we can find one)
|
||||
tags[EH_AUTHOR_NAMESPACE]?.let {
|
||||
if (it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
|
||||
}
|
||||
// Set genre
|
||||
genre?.let { manga.genre = it }
|
||||
|
||||
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
|
||||
// We default to completed
|
||||
manga.status = SManga.COMPLETED
|
||||
title?.let { t ->
|
||||
if (ONGOING_SUFFIX.any {
|
||||
t.endsWith(it, ignoreCase = true)
|
||||
}
|
||||
) manga.status = SManga.ONGOING
|
||||
}
|
||||
|
||||
// Build a nice looking description out of what we know
|
||||
val titleDesc = StringBuilder()
|
||||
title?.let { titleDesc += "Title: $it\n" }
|
||||
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
|
||||
|
||||
val detailsDesc = StringBuilder()
|
||||
uploader?.let { detailsDesc += "Uploader: $it\n" }
|
||||
datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
|
||||
visible?.let { detailsDesc += "Visible: $it\n" }
|
||||
language?.let {
|
||||
detailsDesc += "Language: $it"
|
||||
if (translated == true) detailsDesc += " TR"
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" }
|
||||
length?.let { detailsDesc += "Length: $it pages\n" }
|
||||
favorites?.let { detailsDesc += "Favorited: $it times\n" }
|
||||
averageRating?.let {
|
||||
detailsDesc += "Rating: $it"
|
||||
ratingCount?.let { count -> detailsDesc += " ($count)" }
|
||||
detailsDesc += "\n"
|
||||
}
|
||||
|
||||
val tagsDesc = buildTagsDescription(this)
|
||||
|
||||
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
|
||||
.filter(String::isNotBlank)
|
||||
.joinToString(separator = "\n")
|
||||
}
|
||||
|
||||
private fun buildTagsDescription(metadata: ExGalleryMetadata) = StringBuilder("Tags:\n").apply {
|
||||
// BiConsumer only available in Java 8, we have to use destructuring here
|
||||
metadata.tags.forEach { (namespace, tags) ->
|
||||
if (tags.isNotEmpty()) {
|
||||
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
|
||||
this += "▪ $namespace: $joinedTags\n"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
/**
|
||||
* Simple tag model
|
||||
*/
|
||||
data class Tag(val name: String, val light: Boolean)
|
@ -1,10 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import android.net.Uri
|
||||
|
||||
/**
|
||||
* Uri filter
|
||||
*/
|
||||
interface UriFilter {
|
||||
fun addToUri(builder: Uri.Builder)
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
package eu.kanade.tachiyomi.extension.all.ehentai
|
||||
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
/**
|
||||
* UriGroup
|
||||
*/
|
||||
open class UriGroup<V>(name: String, state: List<V>) : Filter.Group<V>(name, state), UriFilter {
|
||||
override fun addToUri(builder: Uri.Builder) {
|
||||
state.forEach {
|
||||
if (it is UriFilter) it.addToUri(builder)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="eu.kanade.tachiyomi.extension" />
|
@ -1,13 +0,0 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'HentaiHand'
|
||||
pkgNameSuffix = 'all.hentaihand'
|
||||
extClass = '.HentaiHandFactory'
|
||||
extVersionCode = 2
|
||||
libVersion = '1.2'
|
||||
containsNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Before Width: | Height: | Size: 2.8 KiB |