added dummy extension
This commit is contained in:
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user