added dummy extension

This commit is contained in:
jmir1
2021-04-22 22:11:15 +02:00
parent ee098a3ea0
commit f45cd4397d
2192 changed files with 52 additions and 79571 deletions

View File

@ -1,7 +1,7 @@
// used both in common.gradle and themesources library // used both in common.gradle and themesources library
dependencies { dependencies {
// Lib 1.2, but using specific commit so we don't need to bump up the version // Lib 1.2, but using specific commit so we don't need to bump up the version
compileOnly "com.github.tachiyomiorg:extensions-lib:a596412" compileOnly "com.github.jmir1:extensions-lib:e6cc2ea"
// These are provided by the app itself // These are provided by the app itself
compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Bato.to'
pkgNameSuffix = 'all.batoto'
extClass = '.BatoToFactory'
extVersionCode = 7
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@ -1,518 +0,0 @@
package eu.kanade.tachiyomi.extension.all.batoto
import com.squareup.duktape.Duktape
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.util.Calendar
import java.util.concurrent.TimeUnit
open class BatoTo(
override val lang: String,
private val siteLang: String
) : ParsedHttpSource() {
override val name: String = "Bato.to"
override val baseUrl: String = "https://bato.to"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=update&page=$page")
}
override fun latestUpdatesSelector(): String {
return when (siteLang) {
"" -> "div#series-list div.col"
"en" -> "div#series-list div.col.no-flag"
else -> "div#series-list div.col:has([data-lang=\"$siteLang\"])"
}
}
override fun latestUpdatesFromElement(element: Element): SManga {
val manga = SManga.create()
val item = element.select("a.item-cover")
val imgurl = item.select("img").attr("abs:src")
manga.setUrlWithoutDomain(item.attr("href"))
manga.title = element.select("a.item-title").text()
manga.thumbnail_url = imgurl
return manga
}
override fun latestUpdatesNextPageSelector() = "div#mainer .pagination .page-item:not(.disabled) a.page-link:contains(»)"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/browse?langs=$siteLang&sort=views_w&page=$page")
}
override fun popularMangaSelector() = latestUpdatesSelector()
override fun popularMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun popularMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return if (query.isNotBlank()) {
GET("$baseUrl/search?word=$query&page=$page")
} else {
val url = HttpUrl.parse("$baseUrl/browse")!!.newBuilder()
url.addQueryParameter("page", page.toString())
url.addQueryParameter("langs", siteLang)
filters.forEach { filter ->
when (filter) {
is OriginFilter -> {
val originToInclude = mutableListOf<String>()
filter.state.forEach { content ->
if (content.state) {
originToInclude.add(content.name)
}
}
if (originToInclude.isNotEmpty()) {
url.addQueryParameter(
"origs",
originToInclude
.joinToString(",")
)
}
}
is StatusFilter -> {
if (filter.state != 0) {
url.addQueryParameter("release", filter.toUriPart())
}
}
is GenreFilter -> {
val genreToInclude = filter.state
.filter { it.isIncluded() }
.map { it.name }
val genreToExclude = filter.state
.filter { it.isExcluded() }
.map { it.name }
if (genreToInclude.isNotEmpty() || genreToExclude.isNotEmpty()) {
url.addQueryParameter(
"genres",
genreToInclude
.joinToString(",") +
"|" +
genreToExclude
.joinToString(",")
)
}
}
is ChapterFilter -> {
if (filter.state != 0) {
url.addQueryParameter("chapters", filter.toUriPart())
}
}
is SortBy -> {
if (filter.state != 0) {
url.addQueryParameter("sort", filter.toUriPart())
}
}
}
}
GET(url.build().toString(), headers)
}
}
override fun searchMangaSelector() = latestUpdatesSelector()
override fun searchMangaFromElement(element: Element) = latestUpdatesFromElement(element)
override fun searchMangaNextPageSelector() = latestUpdatesNextPageSelector()
override fun mangaDetailsRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
return GET(manga.url, headers)
}
return super.mangaDetailsRequest(manga)
}
override fun mangaDetailsParse(document: Document): SManga {
val infoElement = document.select("div#mainer div.container-fluid")
val manga = SManga.create()
val genres = mutableListOf<String>()
val status = infoElement.select("div.attr-item:contains(status) span").text()
infoElement.select("div.attr-item:contains(genres) span").text().split(
" / "
.toRegex()
).forEach { element ->
genres.add(element)
}
manga.title = infoElement.select("h3").text()
manga.author = infoElement.select("div.attr-item:contains(author) a:first-child").text()
manga.artist = infoElement.select("div.attr-item:contains(author) a:last-child").text()
manga.status = parseStatus(status)
manga.genre = infoElement.select(".attr-item b:contains(genres) + span ").joinToString { it.text() }
manga.description = infoElement.select("h5:contains(summary) + pre").text()
manga.thumbnail_url = document.select("div.attr-cover img")
.attr("abs:src")
return manga
}
private fun parseStatus(status: String?) = when {
status == null -> SManga.UNKNOWN
status.contains("Ongoing") -> SManga.ONGOING
status.contains("Completed") -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
override fun chapterListRequest(manga: SManga): Request {
if (manga.url.startsWith("http")) {
return GET(manga.url, headers)
}
return super.chapterListRequest(manga)
}
override fun chapterListSelector() = "div.main div.p-2"
override fun chapterFromElement(element: Element): SChapter {
val chapter = SChapter.create()
val urlElement = element.select("a.chapt")
val group = element.select("div.extra > a:not(.ps-3)").text()
val time = element.select("i").text()
.replace("a ", "1 ")
.replace("an ", "1 ")
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
if (group != "") {
chapter.scanlator = group
}
if (time != "") {
chapter.date_upload = parseChapterDate(time)
}
return chapter
}
private fun parseChapterDate(date: String): Long {
val value = date.split(' ')[0].toInt()
return when {
"secs" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, value * -1)
}.timeInMillis
"mins" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hours" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"days" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"weeks" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"months" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"years" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
"sec" in date -> Calendar.getInstance().apply {
add(Calendar.SECOND, value * -1)
}.timeInMillis
"min" in date -> Calendar.getInstance().apply {
add(Calendar.MINUTE, value * -1)
}.timeInMillis
"hour" in date -> Calendar.getInstance().apply {
add(Calendar.HOUR_OF_DAY, value * -1)
}.timeInMillis
"day" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * -1)
}.timeInMillis
"week" in date -> Calendar.getInstance().apply {
add(Calendar.DATE, value * 7 * -1)
}.timeInMillis
"month" in date -> Calendar.getInstance().apply {
add(Calendar.MONTH, value * -1)
}.timeInMillis
"year" in date -> Calendar.getInstance().apply {
add(Calendar.YEAR, value * -1)
}.timeInMillis
else -> {
return 0
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.url.startsWith("http")) {
return GET(chapter.url, headers)
}
return super.pageListRequest(chapter)
}
override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>()
val script = document.select("script").html()
if (script.contains("var images =")) {
val imgJson = JSONObject(script.substringAfter("var images = ").substringBefore(";"))
val imgNames = imgJson.names()
if (imgNames != null) {
for (i in 0 until imgNames.length()) {
val imgKey = imgNames.getString(i)
val imgUrl = imgJson.getString(imgKey)
pages.add(Page(i, "", imgUrl))
}
}
} else if (script.contains("const server =")) { // bato.to
val duktape = Duktape.create()
val encryptedServer = script.substringAfter("const server = ").substringBefore(";")
val batojs = duktape.evaluate(script.substringAfter("const batojs = ").substringBefore(";")).toString()
val decryptScript = cryptoJS + "CryptoJS.AES.decrypt($encryptedServer, \"$batojs\").toString(CryptoJS.enc.Utf8);"
val server = duktape.evaluate(decryptScript).toString().replace("\"", "")
duktape.close()
val imgArray = JSONArray(script.substringAfter("const images = ").substringBefore(";"))
if (imgArray != null) {
if (script.contains("bato.to/images")) {
for (i in 0 until imgArray.length()) {
val imgUrl = imgArray.get(i)
pages.add(Page(i, "", "$imgUrl"))
}
} else {
for (i in 0 until imgArray.length()) {
val imgUrl = imgArray.get(i)
if (server.startsWith("http"))
pages.add(Page(i, "", "${server}$imgUrl"))
else
pages.add(Page(i, "", "https:${server}$imgUrl"))
}
}
}
}
return pages
}
private val cryptoJS by lazy {
client.newCall(
GET(
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js",
headers
)
).execute().body()!!.string()
}
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
private class OriginFilter(genres: List<Tag>) : Filter.Group<Tag>("Origin", genres)
private class GenreFilter(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
private class ChapterFilter : UriPartFilter(
"Chapters",
arrayOf(
Pair("<select>", ""),
Pair("1 ~ 9", "1-9"),
Pair("10 ~ 29", "10-29"),
Pair("30 ~ 99", "30-99"),
Pair("100 ~ 199", "100-199"),
Pair("200+", "200"),
Pair("100+", "100"),
Pair("50+", "50"),
Pair("10+", "10"),
Pair("1+", "1")
)
)
private class SortBy : UriPartFilter(
"Sorts By",
arrayOf(
Pair("<select>", ""),
Pair("A-Z", "title.az"),
Pair("Z-A", "title"),
Pair("Last Updated", "update"),
Pair("Oldest Updated", "updated.az"),
Pair("Newest Added", "create"),
Pair("Oldest Added", "create.az"),
Pair("Most Views Totally", "views_a"),
Pair("Most Views 365 days", "views_y"),
Pair("Most Views 30 days", "views_m"),
Pair("Most Views 7 days", "views_w"),
Pair("Most Views 24 hours", "views_d"),
Pair("Most Views 60 minutes", "views_h"),
Pair("Least Views Totally", "views_a.az"),
Pair("Least Views 365 days", "views_y.az"),
Pair("Least Views 30 days", "views_m.az"),
Pair("Least Views 7 days", "views_w.az"),
Pair("Least Views 24 hours", "views_d.az"),
Pair("Least Views 60 minutes", "views_h.az")
)
)
private class StatusFilter : UriPartFilter(
"Status",
arrayOf(
Pair("<select>", ""),
Pair("Pending", "pending"),
Pair("Ongoing", "ongoing"),
Pair("Completed", "completed"),
Pair("Hiatus", "hiatus"),
Pair("Cancelled", "cancelled")
)
)
override fun getFilterList() = FilterList(
Filter.Header("NOTE: Ignored if using text search!"),
Filter.Separator(),
ChapterFilter(),
SortBy(),
StatusFilter(),
OriginFilter(getOriginList()),
GenreFilter(getGenreList())
)
private fun getOriginList() = listOf(
Tag("my"),
Tag("ceb"),
Tag("zh"),
Tag("zh_hk"),
Tag("en"),
Tag("en_us"),
Tag("fil"),
Tag("id"),
Tag("it"),
Tag("ja"),
Tag("ko"),
Tag("ms"),
Tag("pt_br"),
Tag("th"),
Tag("vi")
)
private fun getGenreList() = listOf(
Genre("Artbook"),
Genre("Cartoon"),
Genre("Comic"),
Genre("Doujinshi"),
Genre("Imageset"),
Genre("Manga"),
Genre("Manhua"),
Genre("Manhwa"),
Genre("Webtoon"),
Genre("Western"),
Genre("Josei"),
Genre("Seinen"),
Genre("Shoujo"),
Genre("Shoujo_Ai"),
Genre("Shounen"),
Genre("Shounen_Ai"),
Genre("Yaoi"),
Genre("Yuri"),
Genre("Ecchi"),
Genre("Mature"),
Genre("Adult"),
Genre("Gore"),
Genre("Violence"),
Genre("Smut"),
Genre("Hentai"),
Genre("4_Koma"),
Genre("Action"),
Genre("Adaptation"),
Genre("Adventure"),
Genre("Aliens"),
Genre("Animals"),
Genre("Anthology"),
Genre("Comedy"),
Genre("Cooking"),
Genre("Crime"),
Genre("Crossdressing"),
Genre("Delinquents"),
Genre("Dementia"),
Genre("Demons"),
Genre("Drama"),
Genre("Fantasy"),
Genre("Fan_Colored"),
Genre("Full_Color"),
Genre("Game"),
Genre("Gender_Bender"),
Genre("Genderswap"),
Genre("Ghosts"),
Genre("Gyaru"),
Genre("Harem"),
Genre("Harlequin"),
Genre("Historical"),
Genre("Horror"),
Genre("Incest"),
Genre("Isekai"),
Genre("Kids"),
Genre("Loli"),
Genre("Lolicon"),
Genre("Magic"),
Genre("Magical_Girls"),
Genre("Martial_Arts"),
Genre("Mecha"),
Genre("Medical"),
Genre("Military"),
Genre("Monster_Girls"),
Genre("Monsters"),
Genre("Music"),
Genre("Mystery"),
Genre("Netorare"),
Genre("Ninja"),
Genre("Office_Workers"),
Genre("Oneshot"),
Genre("Parody"),
Genre("Philosophical"),
Genre("Police"),
Genre("Post_Apocalyptic"),
Genre("Psychological"),
Genre("Reincarnation"),
Genre("Reverse_Harem"),
Genre("Romance"),
Genre("Samurai"),
Genre("School_Life"),
Genre("Sci_Fi"),
Genre("Shota"),
Genre("Shotacon"),
Genre("Slice_Of_Life"),
Genre("SM_BDSM"),
Genre("Space"),
Genre("Sports"),
Genre("Super_Power"),
Genre("Superhero"),
Genre("Supernatural"),
Genre("Survival"),
Genre("Thriller"),
Genre("Time_Travel"),
Genre("Tragedy"),
Genre("Vampires"),
Genre("Video_Games"),
Genre("Virtual_Reality"),
Genre("Wuxia"),
Genre("Xianxia"),
Genre("Xuanhuan"),
Genre("Zombies"),
Genre("award_winning"),
Genre("youkai"),
Genre("uncategorized")
)
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
fun toUriPart() = vals[state].second
}
private class Tag(name: String) : Filter.CheckBox(name)
private class Genre(name: String) : Filter.TriState(name)
}

View File

@ -1,60 +0,0 @@
package eu.kanade.tachiyomi.extension.all.batoto
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class BatoToFactory : SourceFactory {
override fun createSources(): List<Source> = languages.map { BatoTo(it.first, it.second) }
}
private val languages = listOf(
//commented langueges do currently not exist on Bato.to but haven in the past
Pair("all",""),
Pair("ar", "ar"),
Pair("bg", "bg"),
Pair("cs", "cs"),
Pair("da", "da"),
Pair("de", "de"),
Pair("el", "el"),
Pair("en", "en"),
Pair("en-US", "en_us"),
Pair("es", "es"),
Pair("es-419", "es_419"),
Pair("eu", "eu"),
Pair("fa", "fa"),
Pair("fi", "fi"),
Pair("fil", "fil"),
Pair("fr", "fr"),
Pair("he", "he"),
//Pair("hi", "hi"),
Pair("hr", "hr"),
Pair("hu", "hu"),
Pair("id", "id"),
Pair("it", "it"),
Pair("ja", "ja"),
Pair("ko", "ko"),
//Pair("ku", "ku"),
Pair("ml", "ml"),
Pair("mn", "mn"),
Pair("ms", "ms"),
Pair("my", "my"),
Pair("nl", "nl"),
Pair("no", "no"),
Pair("pl", "pl"),
Pair("pt", "pt"),
Pair("pt-BR", "pt_br"),
Pair("pt-PT", "pt_pt"),
Pair("ro", "ro"),
Pair("ru", "ru"),
Pair("th", "th"),
Pair("tr", "tr"),
Pair("uk", "uk"),
Pair("vi", "vi"),
//Pair("xh", "xh"),
Pair("zh", "zh"),
Pair("zh-rHK", "zh_hk"),
Pair("zh-rTW", "zh_tw"),
Pair("zu", "zu"),
)

View File

@ -1,57 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.cubari.CubariUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<!-- We need another intent filter so the /a/..* shortcut -->
<!-- doesn't pollute the cubari one, since they work in any combination -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*cubari.moe"
android:pathPattern="/read/..*"
android:scheme="https" />
<data
android:host="*cubari.moe"
android:pathPattern="/proxy/..*"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*guya.moe"
android:pathPattern="/proxy/..*"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*imgur.com"
android:pathPattern="/a/..*"
android:scheme="https" />
<data
android:host="*imgur.com"
android:pathPattern="/gallery/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Cubari'
pkgNameSuffix = "all.cubari"
extClass = '.CubariFactory'
extVersionCode = 2
libVersion = '1.2'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,354 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.os.Build
import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONObject
import rx.Observable
open class Cubari(override val lang: String) : HttpSource() {
final override val name = "Cubari"
final override val baseUrl = "https://cubari.moe"
final override val supportsLatest = true
override fun headersBuilder() = Headers.Builder().apply {
add(
"User-Agent",
"(Android ${Build.VERSION.RELEASE}; " +
"${Build.MANUFACTURER} ${Build.MODEL}) " +
"Tachiyomi/${BuildConfig.VERSION_NAME} " +
Build.ID
)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/", headers)
}
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()!!
.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response -> latestUpdatesParse(response) }
}
override fun latestUpdatesParse(response: Response): MangasPage {
return parseMangaList(JSONArray(response.body()!!.string()), SortType.UNPINNED)
}
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/", headers)
}
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newBuilder()
.addInterceptor(RemoteStorageUtils.HomeInterceptor())
.build()!!
.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response -> popularMangaParse(response) }
}
override fun popularMangaParse(response: Response): MangasPage {
return parseMangaList(JSONArray(response.body()!!.string()), SortType.PINNED)
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response -> mangaDetailsParse(response, manga) }
}
// Called when the series is loaded, or when opening in browser
override fun mangaDetailsRequest(manga: SManga): Request {
return GET("$baseUrl${manga.url}", headers)
}
override fun mangaDetailsParse(response: Response): SManga {
throw Exception("Unused")
}
private fun mangaDetailsParse(response: Response, manga: SManga): SManga {
return parseMangaFromApi(JSONObject(response.body()!!.string()), manga)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response -> chapterListParse(response, manga) }
}
// Gets the chapter list based on the series being viewed
override fun chapterListRequest(manga: SManga): Request {
val urlComponents = manga.url.split("/")
val source = urlComponents[2]
val slug = urlComponents[3]
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
}
override fun chapterListParse(response: Response): List<SChapter> {
throw Exception("Unused")
}
// Called after the request
private fun chapterListParse(response: Response, manga: SManga): List<SChapter> {
val res = response.body()!!.string()
return parseChapterList(res, manga)
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return when {
chapter.url.contains("/chapter/") -> {
client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
directPageListParse(response)
}
}
else -> {
client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
seriesJsonPageListParse(response, chapter)
}
}
}
}
override fun pageListRequest(chapter: SChapter): Request {
return when {
chapter.url.contains("/chapter/") -> {
GET("$baseUrl${chapter.url}", headers)
}
else -> {
var url = chapter.url.split("/")
val source = url[2]
val slug = url[3]
GET("$baseUrl/read/api/$source/series/$slug/", headers)
}
}
}
private fun directPageListParse(response: Response): List<Page> {
val res = response.body()!!.string()
val pages = JSONArray(res)
val pageArray = ArrayList<Page>()
for (i in 0 until pages.length()) {
val page = if (pages.optJSONObject(i) != null) {
pages.getJSONObject(i).getString("src")
} else {
pages[i]
}
pageArray.add(Page(i + 1, "", page.toString()))
}
return pageArray
}
private fun seriesJsonPageListParse(response: Response, chapter: SChapter): List<Page> {
val res = response.body()!!.string()
val json = JSONObject(res)
val groups = json.getJSONObject("groups")
val groupIter = groups.keys()
val groupMap = HashMap<String, String>()
while (groupIter.hasNext()) {
val groupKey = groupIter.next()
groupMap[groups.getString(groupKey)] = groupKey
}
val chapters = json.getJSONObject("chapters")
val pages = if (chapters.has(chapter.chapter_number.toString())) {
chapters
.getJSONObject(chapter.chapter_number.toString())
.getJSONObject("groups")
.getJSONArray(groupMap[chapter.scanlator])
} else {
chapters
.getJSONObject(chapter.chapter_number.toInt().toString())
.getJSONObject("groups")
.getJSONArray(groupMap[chapter.scanlator])
}
val pageArray = ArrayList<Page>()
for (i in 0 until pages.length()) {
val page = if (pages.optJSONObject(i) != null) {
pages.getJSONObject(i).getString("src")
} else {
pages[i]
}
pageArray.add(Page(i + 1, "", page.toString()))
}
return pageArray
}
// Stub
override fun pageListParse(response: Response): List<Page> {
throw Exception("Unused")
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PROXY_PREFIX) -> {
val trimmedQuery = query.removePrefix(PROXY_PREFIX)
// Only tag for recently read on search
client.newBuilder()
.addInterceptor(RemoteStorageUtils.TagInterceptor())
.build()!!
.newCall(searchMangaRequest(page, trimmedQuery, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, trimmedQuery)
}
}
else -> throw Exception(SEARCH_FALLBACK_MSG)
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
try {
val queryFragments = query.split("/")
val source = queryFragments[0]
val slug = queryFragments[1]
return GET("$baseUrl/read/api/$source/series/$slug/", headers)
} catch (e: Exception) {
throw Exception(SEARCH_FALLBACK_MSG)
}
}
override fun searchMangaParse(response: Response): MangasPage {
throw Exception("Unused")
}
private fun searchMangaParse(response: Response, query: String): MangasPage {
return parseSearchList(JSONObject(response.body()!!.string()), query)
}
// ------------- Helpers and whatnot ---------------
private fun parseChapterList(payload: String, manga: SManga): List<SChapter> {
val json = JSONObject(payload)
val groups = json.getJSONObject("groups")
val chapters = json.getJSONObject("chapters")
val chapterList = ArrayList<SChapter>()
val iter = chapters.keys()
while (iter.hasNext()) {
val chapterNum = iter.next()
val chapterObj = chapters.getJSONObject(chapterNum)
val chapterGroups = chapterObj.getJSONObject("groups")
val groupsIter = chapterGroups.keys()
while (groupsIter.hasNext()) {
val groupNum = groupsIter.next()
val chapter = SChapter.create()
chapter.scanlator = groups.getString(groupNum)
if (chapterObj.has("release_date")) {
chapter.date_upload =
chapterObj.getJSONObject("release_date").getLong(groupNum) * 1000
}
chapter.name = chapterNum + " - " + chapterObj.getString("title")
chapter.chapter_number = chapterNum.toFloat()
chapter.url =
if (chapterGroups.optJSONArray(groupNum) != null) {
"${manga.url}/$chapterNum/$groupNum"
} else {
chapterGroups.getString(groupNum)
}
chapterList.add(chapter)
}
}
return chapterList.reversed()
}
private fun parseMangaList(payload: JSONArray, sortType: SortType): MangasPage {
val mangas = ArrayList<SManga>()
for (i in 0 until payload.length()) {
val json = payload.getJSONObject(i)
val pinned = json.getBoolean("pinned")
if (sortType == SortType.PINNED && pinned) {
mangas.add(parseMangaFromRemoteStorage(json))
} else if (sortType == SortType.UNPINNED && !pinned) {
mangas.add(parseMangaFromRemoteStorage(json))
}
}
return MangasPage(mangas, false)
}
private fun parseSearchList(payload: JSONObject, query: String): MangasPage {
val mangas = ArrayList<SManga>()
val tempManga = SManga.create()
tempManga.url = "/read/$query"
mangas.add(parseMangaFromApi(payload, tempManga))
return MangasPage(mangas, false)
}
private fun parseMangaFromRemoteStorage(json: JSONObject): SManga {
val manga = SManga.create()
manga.title = json.getString("title")
manga.artist = json.optString("artist", ARTIST_FALLBACK)
manga.author = json.optString("author", AUTHOR_FALLBACK)
manga.description = json.optString("description", DESCRIPTION_FALLBACK)
manga.url = json.getString("url")
manga.thumbnail_url = json.getString("coverUrl")
return manga
}
private fun parseMangaFromApi(json: JSONObject, mangaReference: SManga): SManga {
val manga = SManga.create()
manga.title = json.getString("title")
manga.artist = json.optString("artist", ARTIST_FALLBACK)
manga.author = json.optString("author", AUTHOR_FALLBACK)
manga.description = json.optString("description", DESCRIPTION_FALLBACK)
manga.url = mangaReference.url
manga.thumbnail_url = json.optString("cover", "")
return manga
}
// ----------------- Things we aren't supporting -----------------
override fun imageUrlParse(response: Response): String {
throw Exception("imageUrlParse not supported.")
}
companion object {
const val PROXY_PREFIX = "cubari:"
const val AUTHOR_FALLBACK = "Unknown"
const val ARTIST_FALLBACK = "Unknown"
const val DESCRIPTION_FALLBACK = "No description."
const val SEARCH_FALLBACK_MSG = "Unable to parse. Is your query in the format of $PROXY_PREFIX<source>/<slug>?"
enum class SortType {
PINNED,
UNPINNED
}
}
}

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class CubariFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
Cubari("en"),
Cubari("all"),
Cubari("other")
)
}

View File

@ -1,64 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
class CubariUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val host = intent?.data?.host
val pathSegments = intent?.data?.pathSegments
if (host != null && pathSegments != null) {
val query = when (host) {
"m.imgur.com", "imgur.com" -> fromImgur(pathSegments)
else -> fromCubari(pathSegments)
}
if (query == null) {
Log.e("CubariUrlActivity", "Unable to parse URI from intent $intent")
finish()
exitProcess(1)
}
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", query)
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("CubariUrlActivity", e.toString())
}
}
finish()
exitProcess(0)
}
private fun fromImgur(pathSegments: List<String>): String? {
if (pathSegments.size >= 2) {
val id = pathSegments[1]
return "${Cubari.PROXY_PREFIX}imgur/$id"
}
return null
}
private fun fromCubari(pathSegments: MutableList<String>): String? {
return if (pathSegments.size >= 3) {
val source = pathSegments[1]
val slug = pathSegments[2]
"${Cubari.PROXY_PREFIX}$source/$slug"
} else {
null
}
}
}

View File

@ -1,147 +0,0 @@
package eu.kanade.tachiyomi.extension.all.cubari
import android.annotation.SuppressLint
import android.app.Application
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class RemoteStorageUtils {
abstract class GenericInterceptor(private val transparent: Boolean) : Interceptor {
private val handler = Handler(Looper.getMainLooper())
abstract val jsScript: String
abstract fun urlModifier(originalUrl: String): String
internal class JsInterface(private val latch: CountDownLatch, var payload: String = "") {
@JavascriptInterface
fun passPayload(passedPayload: String) {
payload = passedPayload
latch.countDown()
}
}
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
try {
val originalRequest = chain.request()
val originalResponse = chain.proceed(originalRequest)
return proceedWithWebView(originalRequest, originalResponse)
} catch (e: Exception) {
throw IOException(e)
}
}
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
private fun proceedWithWebView(request: Request, response: Response): Response {
val latch = CountDownLatch(1)
var webView: WebView? = null
val origRequestUrl = request.url().toString()
val headers = request.headers().toMultimap().mapValues {
it.value.getOrNull(0) ?: ""
}.toMutableMap()
val jsInterface = JsInterface(latch)
handler.post {
val webview = WebView(Injekt.get<Application>())
webView = webview
with(webview.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
useWideViewPort = false
loadWithOverviewMode = false
userAgentString = request.header("User-Agent")
}
webview.addJavascriptInterface(jsInterface, "android")
webview.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
view.evaluateJavascript(jsScript) {}
}
if (transparent) {
latch.countDown()
}
}
}
webview.loadUrl(urlModifier(origRequestUrl), headers)
}
latch.await(TIMEOUT_SEC, TimeUnit.SECONDS)
handler.postDelayed(
{ webView?.destroy() },
DELAY_MILLIS * (if (transparent) 2 else 1)
)
return if (transparent) {
response
} else {
response.newBuilder().body(ResponseBody.create(response.body()?.contentType(), jsInterface.payload)).build()
}
}
}
class TagInterceptor : GenericInterceptor(true) {
override val jsScript: String = """
let dispatched = false;
window.addEventListener('history-ready', function () {
if (!dispatched) {
dispatched = true;
Promise.all(
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
).then(e => {
window.android.passPayload(JSON.stringify(e.flatMap(e => e)))
});
}
});
tag();
"""
override fun urlModifier(originalUrl: String): String {
return originalUrl.replace("/api/", "/").replace("/series/", "/")
}
}
class HomeInterceptor : GenericInterceptor(false) {
override val jsScript: String = """
let dispatched = false;
(function () {
if (!dispatched) {
dispatched = true;
Promise.all(
[globalHistoryHandler.getAllPinnedSeries(), globalHistoryHandler.getAllUnpinnedSeries()]
).then(e => {
window.android.passPayload(JSON.stringify(e.flatMap(e => e) ) )
});
}
})();
"""
override fun urlModifier(originalUrl: String): String {
return originalUrl
}
}
companion object {
const val TIMEOUT_SEC: Long = 10
const val DELAY_MILLIS: Long = 10000
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,12 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'Dragon Ball Multiverse'
pkgNameSuffix = 'all.dragonball_multiverse'
extClass = '.DbMFactory'
extVersionCode = 2
libVersion = '1.2'
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 408 KiB

View File

@ -1,83 +0,0 @@
@file:Suppress("ClassName")
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
class DbMFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
DbMultiverseEN(),
DbMultiverseFR(),
DbMultiverseJP(),
DbMultiverseCN(),
DbMultiverseES(),
DbMultiverseIT(),
DbMultiversePT(),
DbMultiverseDE(),
DbMultiversePL(),
DbMultiverseNL(),
DbMultiverseFR_PA(),
DbMultiverseTR_TR(),
DbMultiversePT_BR(),
DbMultiverseHU_HU(),
DbMultiverseGA_ES(),
DbMultiverseCT_CT(),
DbMultiverseNO_NO(),
DbMultiverseRU_RU(),
DbMultiverseRO_RO(),
DbMultiverseEU_EH(),
DbMultiverseLT_LT(),
DbMultiverseHR_HR(),
DbMultiverseKR_KR(),
DbMultiverseFI_FI(),
DbMultiverseHE_HE(),
DbMultiverseBG_BG(),
DbMultiverseSV_SE(),
DbMultiverseGR_GR(),
DbMultiverseES_CO(),
DbMultiverseAR_JO(),
DbMultiverseTL_PI(),
DbMultiverseLA_LA(),
DbMultiverseDA_DK(),
DbMultiverseCO_FR(),
DbMultiverseBR_FR(),
DbMultiverseXX_VE()
)
}
class DbMultiverseFR : DbMultiverse("fr", "fr")
class DbMultiverseJP : DbMultiverse("ja", "jp")
class DbMultiverseCN : DbMultiverse("zh", "cn")
class DbMultiverseES : DbMultiverse("es", "es")
class DbMultiverseIT : DbMultiverse("it", "it")
class DbMultiversePT : DbMultiverse("pt", "pt")
class DbMultiverseDE : DbMultiverse("de", "de")
class DbMultiversePL : DbMultiverse("pl", "pl")
class DbMultiverseNL : DbMultiverse("nl", "nl")
class DbMultiverseFR_PA : DbMultiverse("fr", "fr_PA")
class DbMultiverseTR_TR : DbMultiverse("tr", "tr_TR")
class DbMultiversePT_BR : DbMultiverse("pt-BR", "pt_BR")
class DbMultiverseHU_HU : DbMultiverse("hu", "hu_HU")
class DbMultiverseGA_ES : DbMultiverse("ga", "ga_ES")
class DbMultiverseCT_CT : DbMultiverse("ca", "ct_CT")
class DbMultiverseNO_NO : DbMultiverse("no", "no_NO")
class DbMultiverseRU_RU : DbMultiverse("ru", "ru_RU")
class DbMultiverseRO_RO : DbMultiverse("ro", "ro_RO")
class DbMultiverseEU_EH : DbMultiverse("eu", "eu_EH")
class DbMultiverseLT_LT : DbMultiverse("lt", "lt_LT")
class DbMultiverseHR_HR : DbMultiverse("hr", "hr_HR")
class DbMultiverseKR_KR : DbMultiverse("ko", "kr_KR")
class DbMultiverseFI_FI : DbMultiverse("fi", "fi_FI")
class DbMultiverseHE_HE : DbMultiverse("he", "he_HE")
class DbMultiverseBG_BG : DbMultiverse("bg", "bg_BG")
class DbMultiverseSV_SE : DbMultiverse("sv", "sv_SE")
class DbMultiverseGR_GR : DbMultiverse("el", "gr_GR")
class DbMultiverseES_CO : DbMultiverse("es-419", "es_CO")
class DbMultiverseAR_JO : DbMultiverse("ar", "ar_JO")
class DbMultiverseTL_PI : DbMultiverse("fil", "tl_PI")
class DbMultiverseLA_LA : DbMultiverse("la", "la_LA")
class DbMultiverseDA_DK : DbMultiverse("da", "da_DK")
class DbMultiverseCO_FR : DbMultiverse("co", "co_FR")
class DbMultiverseBR_FR : DbMultiverse("br", "br_FR")
class DbMultiverseXX_VE : DbMultiverse("vec", "xx_VE")

View File

@ -1,103 +0,0 @@
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
abstract class DbMultiverse(override val lang: String, private val internalLang: String) : ParsedHttpSource() {
override val name =
if (internalLang.endsWith("_PA")) "Dragon Ball Multiverse Parody"
else "Dragon Ball Multiverse"
override val baseUrl = "https://www.dragonball-multiverse.com"
override val supportsLatest = false
private fun chapterFromElement(element: Element, name: String): SChapter {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(element.attr("abs:href"))
chapter.name = name + element.text().let { num ->
if (num.contains("-")) {
"Pages $num"
} else {
"Page $num"
}
}
return chapter
}
override fun chapterListSelector(): String = "div.cadrelect.chapters a[href*=page-]"
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = mutableListOf<SChapter>()
val document = response.asJsoup()
document.select("div[ch]").forEach { container ->
container.select(chapterListSelector()).mapIndexed { i, chapter ->
// Each page is its own chapter, add chapter name when a first page is mapped
val name = if (i == 0) container.select("h4").text() + " - " else ""
chapters.add(chapterFromElement(chapter, name))
}
}
return chapters.reversed()
}
override fun pageListParse(document: Document): List<Page> {
return document.select("div#h_read img").mapIndexed { index, element ->
Page(index, "", element.attr("abs:src"))
}
}
override fun mangaDetailsParse(document: Document): SManga = createManga(document)
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.just(MangasPage(listOf(createManga(null)), hasNextPage = false))
}
private fun createManga(document: Document?) = SManga.create().apply {
title = name
status = SManga.ONGOING
url = "/$internalLang/chapters.html"
description = "Dragon Ball Multiverse (DBM) is a free online comic, made by a whole team of fans. It's our personal sequel to DBZ."
thumbnail_url = document?.select("div[ch=\"1\"] img")?.attr("abs:src")
}
override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException()
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException()
override fun popularMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
override fun popularMangaSelector(): String = throw UnsupportedOperationException()
override fun popularMangaNextPageSelector(): String? = throw UnsupportedOperationException()
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = Observable.just(MangasPage(emptyList(), false))
override fun searchMangaFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun searchMangaNextPageSelector(): String? = throw UnsupportedOperationException()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = throw UnsupportedOperationException()
override fun searchMangaSelector(): String = throw UnsupportedOperationException()
override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException()
override fun latestUpdatesNextPageSelector(): String? = throw UnsupportedOperationException()
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesSelector(): String = throw UnsupportedOperationException()
}

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.extension.all.dragonball_multiverse
class DbMultiverseEN : DbMultiverse("en", "en")

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.extension">
<application>
<activity
android:name=".all.ehentai.EHUrlActivity"
android:excludeFromRecents="true"
android:theme="@android:style/Theme.NoDisplay">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="e-hentai.org"
android:pathPattern="/g/..*/..*"
android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'E-Hentai'
pkgNameSuffix = 'all.ehentai'
extClass = '.EHFactory'
extVersionCode = 13
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

View File

@ -1,28 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import eu.kanade.tachiyomi.annotations.Nsfw
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
@Nsfw
class EHFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
EHentai("ja", "japanese"),
EHentai("en", "english"),
EHentai("zh", "chinese"),
EHentai("nl", "dutch"),
EHentai("fr", "french"),
EHentai("de", "german"),
EHentai("hu", "hungarian"),
EHentai("it", "italian"),
EHentai("ko", "korean"),
EHentai("pl", "polish"),
EHentai("pt", "portuguese"),
EHentai("ru", "russian"),
EHentai("es", "spanish"),
EHentai("th", "thai"),
EHentai("vi", "vietnamese"),
EHentai("none", "n/a"),
EHentai("other", "other")
)
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.util.Log
import kotlin.system.exitProcess
/**
* Springboard that accepts https://e-hentai.net/g/xxxxx/yyyyy/ intents and redirects them to
* the main Tachiyomi process.
*/
class EHUrlActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val pathSegments = intent?.data?.pathSegments
if (pathSegments != null && pathSegments.size > 2) {
val id = pathSegments[1]
val key = pathSegments[2]
val mainIntent = Intent().apply {
action = "eu.kanade.tachiyomi.SEARCH"
putExtra("query", "${EHentai.PREFIX_ID_SEARCH}$id/$key")
putExtra("filter", packageName)
}
try {
startActivity(mainIntent)
} catch (e: ActivityNotFoundException) {
Log.e("EHUrlActivity", e.toString())
}
} else {
Log.e("EHUrlActivity", "could not parse uri from intent $intent")
}
finish()
exitProcess(0)
}
}

View File

@ -1,70 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import kotlin.math.ln
import kotlin.math.pow
/**
* Various utility methods used in the E-Hentai source
*/
/**
* Return null if String is blank, otherwise returns the original String
* @returns null if the String is blank, otherwise returns the original String
*/
fun String?.nullIfBlank(): String? = if (isNullOrBlank())
null
else
this
/**
* Ignores any exceptions thrown inside a block
*/
fun <T> ignore(expr: () -> T): T? {
return try {
expr()
} catch (t: Throwable) {
null
}
}
/**
* Use '+' to append Strings onto a StringBuilder
*/
operator fun StringBuilder.plusAssign(other: String) {
append(other)
}
/**
* Converts bytes into a human readable String
*/
fun humanReadableByteCount(bytes: Long, si: Boolean): String {
val unit = if (si) 1000 else 1024
if (bytes < unit) return "$bytes B"
val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}
private const val KB_FACTOR = 1000
private const val KIB_FACTOR = 1024
private const val MB_FACTOR = 1000 * KB_FACTOR
private const val MIB_FACTOR = 1024 * KIB_FACTOR
private const val GB_FACTOR = 1000 * MB_FACTOR
private const val GIB_FACTOR = 1024 * MIB_FACTOR
/**
* Parse human readable size Strings
*/
fun parseHumanReadableByteCount(arg0: String): Double? {
val spaceNdx = arg0.indexOf(" ")
val ret = arg0.substring(0 until spaceNdx).toDouble()
when (arg0.substring(spaceNdx + 1)) {
"GB" -> return ret * GB_FACTOR
"GiB" -> return ret * GIB_FACTOR
"MB" -> return ret * MB_FACTOR
"MiB" -> return ret * MIB_FACTOR
"KB" -> return ret * KB_FACTOR
"KiB" -> return ret * KIB_FACTOR
}
return null
}

View File

@ -1,533 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import androidx.preference.CheckBoxPreference
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.Filter.CheckBox
import eu.kanade.tachiyomi.source.model.Filter.Group
import eu.kanade.tachiyomi.source.model.Filter.Select
import eu.kanade.tachiyomi.source.model.Filter.Text
import eu.kanade.tachiyomi.source.model.Filter.TriState
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.CacheControl
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URLEncoder
import android.support.v7.preference.CheckBoxPreference as LegacyCheckBoxPreference
import android.support.v7.preference.PreferenceScreen as LegacyPreferenceScreen
open class EHentai(override val lang: String, private val ehLang: String) : ConfigurableSource, HttpSource() {
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override val name = "E-Hentai"
override val baseUrl = "https://e-hentai.org"
override val supportsLatest = true
// true if lang is a "natural human language"
private fun isLangNatural(): Boolean = lang !in listOf("none", "other")
private fun genericMangaParse(response: Response): MangasPage {
val doc = response.asJsoup()
val parsedMangas = doc.select("table.itg td.glname")
.let { elements ->
if (isLangNatural() && getEnforceLanguagePref()) {
elements.filter { element ->
// only accept elements with a language tag matching ehLang or without a language tag
// could make this stricter and not accept elements without a language tag, possibly add a sharedpreference for it
element.select("div[title^=language]").firstOrNull()?.let { it.text() == ehLang } ?: true
}
} else {
elements
}
}
.map {
SManga.create().apply {
// Get title
it.select("a")?.first()?.apply {
title = this.select(".glink").text()
url = ExGalleryMetadata.normalizeUrl(attr("href"))
}
// Get image
it.parent().select(".glthumb img")?.first().apply {
thumbnail_url = this?.attr("data-src")?.nullIfBlank()
?: this?.attr("src")
}
}
}
// Add to page if required
val hasNextPage = doc.select("a[onclick=return false]").last()?.text() == ">"
return MangasPage(parsedMangas, hasNextPage)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(
listOf(
SChapter.create().apply {
url = manga.url
name = "Chapter"
chapter_number = 1f
}
)
)
override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, "$baseUrl/${chapter.url}").map {
it.mapIndexed { i, s ->
Page(i, s)
}
}!!
/**
* Recursively fetch chapter pages
*/
private fun fetchChapterPage(
chapter: SChapter,
np: String,
pastUrls: List<String> = emptyList()
): Observable<List<String>> {
val urls = ArrayList(pastUrls)
return chapterPageCall(np).flatMap {
val jsoup = it.asJsoup()
urls += parseChapterPage(jsoup)
nextPageUrl(jsoup)?.let { string ->
fetchChapterPage(chapter, string, urls)
} ?: Observable.just(urls)
}
}
private fun parseChapterPage(response: Element) = with(response) {
select(".gdtm a").map {
Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
}.sortedBy(Pair<Int, String>::first).map { it.second }
}
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
private fun chapterPageRequest(np: String) = exGet(np, null, headers)
private fun nextPageUrl(element: Element) = element.select("a[onclick=return false]").last()?.let {
if (it.text() == ">") it.attr("href") else null
}
private fun languageTag(enforceLanguageFilter: Boolean = false): String {
return if (enforceLanguageFilter || getEnforceLanguagePref()) "language:$ehLang" else ""
}
override fun popularMangaRequest(page: Int) = if (isLangNatural()) {
exGet("$baseUrl/?f_search=${languageTag()}&f_srdd=5&f_sr=on", page)
} else {
latestUpdatesRequest(page)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val enforceLanguageFilter = filters.find { it is EnforceLanguageFilter }?.state == true
val uri = Uri.parse("$baseUrl$QUERY_PREFIX").buildUpon()
var modifiedQuery = when {
!isLangNatural() -> query
query.isBlank() -> languageTag(enforceLanguageFilter)
else -> languageTag(enforceLanguageFilter).let { if (it.isNotEmpty()) "$query,$it" else query }
}
modifiedQuery += filters.filterIsInstance<TagFilter>()
.flatMap { it.markedTags() }
.joinToString(",")
.let { if (it.isNotEmpty()) ",$it" else it }
uri.appendQueryParameter("f_search", modifiedQuery)
filters.forEach {
if (it is UriFilter) it.addToUri(uri)
}
return exGet(uri.toString(), page)
}
override fun latestUpdatesRequest(page: Int) = exGet(baseUrl, page)
override fun popularMangaParse(response: Response) = genericMangaParse(response)
override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(response: Response) = genericMangaParse(response)
private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true): Request {
return GET(
page?.let {
addParam(url, "page", (page - 1).toString())
} ?: url,
additionalHeaders?.let { header ->
val headers = headers.newBuilder()
header.toMultimap().forEach { (t, u) ->
u.forEach {
headers.add(t, it)
}
}
headers.build()
} ?: headers
).let {
if (!cache) {
it.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build()
} else {
it
}
}
}
/**
* Parse gallery page to metadata model
*/
@SuppressLint("DefaultLocale")
override fun mangaDetailsParse(response: Response) = with(response.asJsoup()) {
with(ExGalleryMetadata()) {
url = response.request().url().encodedPath()
title = select("#gn").text().nullIfBlank()?.trim()
altTitle = select("#gj").text().nullIfBlank()?.trim()
// Thumbnail is set as background of element in style attribute
thumbnailUrl = select("#gd1 div").attr("style").nullIfBlank()?.let {
it.substring(it.indexOf('(') + 1 until it.lastIndexOf(')'))
}
genre = select("#gdc div").text().nullIfBlank()?.trim()?.toLowerCase()
uploader = select("#gdn").text().nullIfBlank()?.trim()
// Parse the table
select("#gdd tr").forEach {
it.select(".gdt1")
.text()
.nullIfBlank()
?.trim()
?.let { left ->
it.select(".gdt2")
.text()
.nullIfBlank()
?.trim()
?.let { right ->
ignore {
when (
left.removeSuffix(":")
.toLowerCase()
) {
"posted" -> datePosted = EX_DATE_FORMAT.parse(right)?.time ?: 0
"visible" -> visible = right.nullIfBlank()
"language" -> {
language = right.removeSuffix(TR_SUFFIX).trim().nullIfBlank()
translated = right.endsWith(TR_SUFFIX, true)
}
"file size" -> size = parseHumanReadableByteCount(right)?.toLong()
"length" -> length = right.removeSuffix("pages").trim().nullIfBlank()?.toInt()
"favorited" -> favorites = right.removeSuffix("times").trim().nullIfBlank()?.toInt()
}
}
}
}
}
// Parse ratings
ignore {
averageRating = select("#rating_label")
.text()
.removePrefix("Average:")
.trim()
.nullIfBlank()
?.toDouble()
ratingCount = select("#rating_count")
.text()
.trim()
.nullIfBlank()
?.toInt()
}
// Parse tags
tags.clear()
select("#taglist tr").forEach {
val namespace = it.select(".tc").text().removeSuffix(":")
val currentTags = it.select("div").map { element ->
Tag(
element.text().trim(),
element.hasClass("gtl")
)
}
tags[namespace] = currentTags
}
// Copy metadata to manga
SManga.create().apply {
copyTo(this)
}
}
}
private fun searchMangaByIdRequest(id: String) = GET("$baseUrl/g/$id", headers)
private fun searchMangaByIdParse(response: Response, id: String): MangasPage {
val details = mangaDetailsParse(response)
details.url = "/g/$id/"
return MangasPage(listOf(details), false)
}
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val id = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(id))
.asObservableSuccess()
.map { response -> searchMangaByIdParse(response, id) }
} else {
super.fetchSearchManga(page, query, filters)
}
}
override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
override fun pageListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
override fun imageUrlParse(response: Response): String = response.asJsoup().select("#img").attr("abs:src")
private val cookiesHeader by lazy {
val cookies = mutableMapOf<String, String>()
// Setup settings
val settings = mutableListOf<String>()
// Do not show popular right now pane as we can't parse it
settings += "prn_n"
// Exclude every other language except the one we have selected
settings += "xl_" + languageMappings.filter { it.first != ehLang }
.flatMap { it.second }
.joinToString("x")
cookies["uconfig"] = buildSettings(settings)
// Bypass "Offensive For Everyone" content warning
cookies["nw"] = "1"
buildCookies(cookies)
}
// Headers
override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader)!!
private fun buildSettings(settings: List<String?>) = settings.filterNotNull().joinToString(separator = "-")
private fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ", postfix = ";") {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}
@Suppress("SameParameterValue")
private fun addParam(url: String, param: String, value: String) = Uri.parse(url)
.buildUpon()
.appendQueryParameter(param, value)
.toString()
override val client = network.client.newBuilder()
.cookieJar(CookieJar.NO_COOKIES)
.addInterceptor { chain ->
val newReq = chain
.request()
.newBuilder()
.removeHeader("Cookie")
.addHeader("Cookie", cookiesHeader)
.build()
chain.proceed(newReq)
}.build()!!
// Filters
override fun getFilterList() = FilterList(
EnforceLanguageFilter(getEnforceLanguagePref()),
Watched(),
GenreGroup(),
TagFilter("Misc Tags", triStateBoxesFrom(miscTags), ""),
TagFilter("Female Tags", triStateBoxesFrom(femaleTags), "female"),
TagFilter("Male Tags", triStateBoxesFrom(maleTags), "male"),
AdvancedGroup()
)
class Watched : CheckBox("Watched List"), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state)
builder.appendPath("watched")
}
}
class GenreOption(name: String, private val genreId: String) : CheckBox(name, false), UriFilter {
override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("f_$genreId", if (state) "1" else "0")
}
}
class GenreGroup : UriGroup<GenreOption>(
"Genres",
listOf(
GenreOption("Dōjinshi", "doujinshi"),
GenreOption("Manga", "manga"),
GenreOption("Artist CG", "artistcg"),
GenreOption("Game CG", "gamecg"),
GenreOption("Western", "western"),
GenreOption("Non-H", "non-h"),
GenreOption("Image Set", "imageset"),
GenreOption("Cosplay", "cosplay"),
GenreOption("Asian Porn", "asianporn"),
GenreOption("Misc", "misc")
)
)
class AdvancedOption(name: String, private val param: String, defValue: Boolean = false) : CheckBox(name, defValue), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state)
builder.appendQueryParameter(param, "on")
}
}
open class PageOption(name: String, private val queryKey: String) : Text(name), UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state.isNotBlank()) {
if (builder.build().getQueryParameters("f_sp").isEmpty()) {
builder.appendQueryParameter("f_sp", "on")
}
builder.appendQueryParameter(queryKey, state.trim())
}
}
}
class MinPagesOption : PageOption("Minimum Pages", "f_spf")
class MaxPagesOption : PageOption("Maximum Pages", "f_spt")
class RatingOption :
Select<String>(
"Minimum Rating",
arrayOf(
"Any",
"2 stars",
"3 stars",
"4 stars",
"5 stars"
)
),
UriFilter {
override fun addToUri(builder: Uri.Builder) {
if (state > 0) {
builder.appendQueryParameter("f_srdd", (state + 1).toString())
builder.appendQueryParameter("f_sr", "on")
}
}
}
// Explicit type arg for listOf() to workaround this: KT-16570
class AdvancedGroup : UriGroup<Filter<*>>(
"Advanced Options",
listOf(
AdvancedOption("Search Gallery Name", "f_sname", true),
AdvancedOption("Search Gallery Tags", "f_stags", true),
AdvancedOption("Search Gallery Description", "f_sdesc"),
AdvancedOption("Search Torrent Filenames", "f_storr"),
AdvancedOption("Only Show Galleries With Torrents", "f_sto"),
AdvancedOption("Search Low-Power Tags", "f_sdt1"),
AdvancedOption("Search Downvoted Tags", "f_sdt2"),
AdvancedOption("Show Expunged Galleries", "f_sh"),
RatingOption(),
MinPagesOption(),
MaxPagesOption()
)
)
private class EnforceLanguageFilter(default: Boolean) : CheckBox("Enforce language", default)
private val miscTags = "3d, already uploaded, anaglyph, animal on animal, animated, anthology, arisa mizuhara, artbook, ashiya noriko, bailey jay, body swap, caption, chouzuki maryou, christian godard, comic, compilation, dakimakura, fe galvao, ffm threesome, figure, forbidden content, full censorship, full color, game sprite, goudoushi, group, gunyou mikan, harada shigemitsu, hardcore, helly von valentine, higurashi rin, hololive, honey select, how to, incest, incomplete, ishiba yoshikazu, jessica nigri, kalinka fox, kanda midori, kira kira, kitami eri, kuroi hiroki, lenfried, lincy leaw, marie claude bourbonnais, matsunaga ayaka, me me me, missing cover, mmf threesome, mmt threesome, mosaic censorship, mtf threesome, multi-work series, no penetration, non-nude, novel, nudity only, oakazaki joe, out of order, paperchild, pm02 colon 20, poor grammar, radio comix, realporn, redraw, replaced, sakaki kasa, sample, saotome love, scanmark, screenshots, sinful goddesses, sketch lines, stereoscopic, story arc, takeuti ken, tankoubon, themeless, tikuma jukou, time stop, tsubaki zakuro, ttm threesome, twins, uncensored, vandych alex, variant set, watermarked, webtoon, western cg, western imageset, western non-h, yamato nadeshiko club, yui okada, yukkuri, zappa go"
private val femaleTags = "ahegao, anal, angel, apron, bandages, bbw, bdsm, beauty mark, big areolae, big ass, big breasts, big clit, big lips, big nipples, bikini, blackmail, bloomers, blowjob, bodysuit, bondage, breast expansion, bukkake, bunny girl, business suit, catgirl, centaur, cheating, chinese dress, christmas, collar, corset, cosplaying, cowgirl, crossdressing, cunnilingus, dark skin, daughter, deepthroat, defloration, demon girl, double penetration, dougi, dragon, drunk, elf, exhibitionism, farting, females only, femdom, filming, fingering, fishnets, footjob, fox girl, furry, futanari, garter belt, ghost, giantess, glasses, gloves, goblin, gothic lolita, growth, guro, gyaru, hair buns, hairy, hairy armpits, handjob, harem, hidden sex, horns, huge breasts, humiliation, impregnation, incest, inverted nipples, kemonomimi, kimono, kissing, lactation, latex, leg lock, leotard, lingerie, lizard girl, maid, masked face, masturbation, midget, miko, milf, mind break, mind control, monster girl, mother, muscle, nakadashi, netorare, nose hook, nun, nurse, oil, paizuri, panda girl, pantyhose, piercing, pixie cut, policewoman, ponytail, pregnant, rape, rimjob, robot, scat, schoolgirl uniform, sex toys, shemale, sister, small breasts, smell, sole dickgirl, sole female, squirting, stockings, sundress, sweating, swimsuit, swinging, tail, tall girl, teacher, tentacles, thigh high boots, tomboy, transformation, twins, twintails, unusual pupils, urination, vore, vtuber, widow, wings, witch, wolf girl, x-ray, yuri, zombie"
private val maleTags = "anal, bbm, big ass, big penis, bikini, blood, blowjob, bondage, catboy, cheating, chikan, condom, crab, crossdressing, dark skin, deepthroat, demon, dickgirl on male, dilf, dog boy, double anal, double penetration, dragon, drunk, exhibitionism, facial hair, feminization, footjob, fox boy, furry, glasses, group, guro, hairy, handjob, hidden sex, horns, huge penis, human on furry, kimono, lingerie, lizard guy, machine, maid, males only, masturbation, mmm threesome, monster, muscle, nakadashi, ninja, octopus, oni, pillory, policeman, possession, prostate massage, public use, schoolboy uniform, schoolgirl uniform, sex toys, shotacon, sleeping, snuff, sole male, stockings, sunglasses, swimsuit, tall man, tentacles, tomgirl, unusual pupils, virginity, waiter, x-ray, yaoi, zombie"
private fun triStateBoxesFrom(tagString: String): List<TagTriState> = tagString.split(", ").map { TagTriState(it) }
class TagTriState(tag: String) : TriState(tag)
class TagFilter(name: String, private val triStateBoxes: List<TagTriState>, private val nameSpace: String) : Group<TagTriState>(name, triStateBoxes) {
fun markedTags() = triStateBoxes.filter { it.isIncluded() }.map { "$nameSpace:${it.name}" } + triStateBoxes.filter { it.isExcluded() }.map { "-$nameSpace:${it.name}" }
}
// map languages to their internal ids
private val languageMappings = listOf(
Pair("japanese", listOf("0", "1024", "2048")),
Pair("english", listOf("1", "1025", "2049")),
Pair("chinese", listOf("10", "1034", "2058")),
Pair("dutch", listOf("20", "1044", "2068")),
Pair("french", listOf("30", "1054", "2078")),
Pair("german", listOf("40", "1064", "2088")),
Pair("hungarian", listOf("50", "1074", "2098")),
Pair("italian", listOf("60", "1084", "2108")),
Pair("korean", listOf("70", "1094", "2118")),
Pair("polish", listOf("80", "1104", "2128")),
Pair("portuguese", listOf("90", "1114", "2138")),
Pair("russian", listOf("100", "1124", "2148")),
Pair("spanish", listOf("110", "1134", "2158")),
Pair("thai", listOf("120", "1144", "2168")),
Pair("vietnamese", listOf("130", "1154", "2178")),
Pair("n/a", listOf("254", "1278", "2302")),
Pair("other", listOf("255", "1279", "2303"))
)
companion object {
const val QUERY_PREFIX = "?f_apply=Apply+Filter"
const val PREFIX_ID_SEARCH = "id:"
const val TR_SUFFIX = "TR"
// Preferences vals
private const val ENFORCE_LANGUAGE_PREF_KEY = "ENFORCE_LANGUAGE"
private const val ENFORCE_LANGUAGE_PREF_TITLE = "Enforce Language"
private const val ENFORCE_LANGUAGE_PREF_SUMMARY = "If checked, forces browsing of manga matching a language tag"
private const val ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE = false
}
// Preferences
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val enforceLanguagePref = CheckBoxPreference(screen.context).apply {
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
title = ENFORCE_LANGUAGE_PREF_TITLE
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(enforceLanguagePref)
}
override fun setupPreferenceScreen(screen: LegacyPreferenceScreen) {
val enforceLanguagePref = LegacyCheckBoxPreference(screen.context).apply {
key = "${ENFORCE_LANGUAGE_PREF_KEY}_$lang"
title = ENFORCE_LANGUAGE_PREF_TITLE
summary = ENFORCE_LANGUAGE_PREF_SUMMARY
setDefaultValue(ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", checkValue).commit()
}
}
screen.addPreference(enforceLanguagePref)
}
private fun getEnforceLanguagePref(): Boolean = preferences.getBoolean("${ENFORCE_LANGUAGE_PREF_KEY}_$lang", ENFORCE_LANGUAGE_PREF_DEFAULT_VALUE)
}

View File

@ -1,52 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri
/**
* Gallery metadata storage model
*/
class ExGalleryMetadata {
var url: String? = null
var thumbnailUrl: String? = null
var title: String? = null
var altTitle: String? = null
var genre: String? = null
var datePosted: Long? = null
var parent: String? = null
var visible: String? = null // Not a boolean
var language: String? = null
var translated: Boolean? = null
var size: Long? = null
var length: Int? = null
var favorites: Int? = null
var ratingCount: Int? = null
var averageRating: Double? = null
var uploader: String? = null
val tags: MutableMap<String, List<Tag>> = mutableMapOf()
companion object {
private fun splitGalleryUrl(url: String) = url.let {
// Only parse URL if is full URL
val pathSegments = if (it.startsWith("http"))
Uri.parse(it).pathSegments
else
it.split('/')
pathSegments.filterNot(String::isNullOrBlank)
}
private fun galleryId(url: String) = splitGalleryUrl(url)[1]
private fun galleryToken(url: String) = splitGalleryUrl(url)[2]
private fun normalizeUrl(id: String, token: String) = "/g/$id/$token/?nw=always"
fun normalizeUrl(url: String) = normalizeUrl(galleryId(url), galleryToken(url))
}
}

View File

@ -1,84 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import eu.kanade.tachiyomi.source.model.SManga
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private const val EH_ARTIST_NAMESPACE = "artist"
private const val EH_AUTHOR_NAMESPACE = "author"
private val ONGOING_SUFFIX = arrayOf(
"[ongoing]",
"(ongoing)",
"{ongoing}"
)
val EX_DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US)
fun ExGalleryMetadata.copyTo(manga: SManga) {
url?.let { manga.url = it }
thumbnailUrl?.let { manga.thumbnail_url = it }
(title ?: altTitle)?.let { manga.title = it }
// Set artist (if we can find one)
tags[EH_ARTIST_NAMESPACE]?.let {
if (it.isNotEmpty()) manga.artist = it.joinToString(transform = Tag::name)
}
// Set author (if we can find one)
tags[EH_AUTHOR_NAMESPACE]?.let {
if (it.isNotEmpty()) manga.author = it.joinToString(transform = Tag::name)
}
// Set genre
genre?.let { manga.genre = it }
// Try to automatically identify if it is ongoing, we try not to be too lenient here to avoid making mistakes
// We default to completed
manga.status = SManga.COMPLETED
title?.let { t ->
if (ONGOING_SUFFIX.any {
t.endsWith(it, ignoreCase = true)
}
) manga.status = SManga.ONGOING
}
// Build a nice looking description out of what we know
val titleDesc = StringBuilder()
title?.let { titleDesc += "Title: $it\n" }
altTitle?.let { titleDesc += "Alternate Title: $it\n" }
val detailsDesc = StringBuilder()
uploader?.let { detailsDesc += "Uploader: $it\n" }
datePosted?.let { detailsDesc += "Posted: ${EX_DATE_FORMAT.format(Date(it))}\n" }
visible?.let { detailsDesc += "Visible: $it\n" }
language?.let {
detailsDesc += "Language: $it"
if (translated == true) detailsDesc += " TR"
detailsDesc += "\n"
}
size?.let { detailsDesc += "File Size: ${humanReadableByteCount(it, true)}\n" }
length?.let { detailsDesc += "Length: $it pages\n" }
favorites?.let { detailsDesc += "Favorited: $it times\n" }
averageRating?.let {
detailsDesc += "Rating: $it"
ratingCount?.let { count -> detailsDesc += " ($count)" }
detailsDesc += "\n"
}
val tagsDesc = buildTagsDescription(this)
manga.description = listOf(titleDesc.toString(), detailsDesc.toString(), tagsDesc.toString())
.filter(String::isNotBlank)
.joinToString(separator = "\n")
}
private fun buildTagsDescription(metadata: ExGalleryMetadata) = StringBuilder("Tags:\n").apply {
// BiConsumer only available in Java 8, we have to use destructuring here
metadata.tags.forEach { (namespace, tags) ->
if (tags.isNotEmpty()) {
val joinedTags = tags.joinToString(separator = " ", transform = { "<${it.name}>" })
this += "$namespace: $joinedTags\n"
}
}
}

View File

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
/**
* Simple tag model
*/
data class Tag(val name: String, val light: Boolean)

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri
/**
* Uri filter
*/
interface UriFilter {
fun addToUri(builder: Uri.Builder)
}

View File

@ -1,15 +0,0 @@
package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri
import eu.kanade.tachiyomi.source.model.Filter
/**
* UriGroup
*/
open class UriGroup<V>(name: String, state: List<V>) : Filter.Group<V>(name, state), UriFilter {
override fun addToUri(builder: Uri.Builder) {
state.forEach {
if (it is UriFilter) it.addToUri(builder)
}
}
}

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -1,13 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
extName = 'HentaiHand'
pkgNameSuffix = 'all.hentaihand'
extClass = '.HentaiHandFactory'
extVersionCode = 2
libVersion = '1.2'
containsNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

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