Update four "all" sources (#1314)

* ComiCake: Some cleanup work

Signed-off-by: TacoTheDank <SkytkRSfan3895@gmail.com>

* E-Hentai: Some cleanup work

Signed-off-by: TacoTheDank <SkytkRSfan3895@gmail.com>

* FoolSlide: Some cleanup work and source updates

Signed-off-by: TacoTheDank <SkytkRSfan3895@gmail.com>

* Genkan: Some cleanup work

Signed-off-by: TacoTheDank <SkytkRSfan3895@gmail.com>
This commit is contained in:
TacoTheDank 2019-08-03 21:38:05 -04:00 committed by Eugene
parent 27175b3dee
commit c94cc58e7d
16 changed files with 188 additions and 214 deletions

View File

@ -3,9 +3,9 @@ apply plugin: 'kotlin-android'
ext { ext {
appName = 'Tachiyomi: ComiCake' appName = 'Tachiyomi: ComiCake'
pkgNameSuffix = "all.comicake" pkgNameSuffix = 'all.comicake'
extClass = '.ComiCakeFactory' extClass = '.ComiCakeFactory'
extVersionCode = 3 extVersionCode = 4
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -2,21 +2,24 @@ package eu.kanade.tachiyomi.extension.all.comicake
import android.os.Build import android.os.Build
import eu.kanade.tachiyomi.extension.BuildConfig import eu.kanade.tachiyomi.extension.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import eu.kanade.tachiyomi.network.GET
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import kotlin.collections.ArrayList
const val COMICAKE_DEFAULT_API_ENDPOINT = "/api" // Highly unlikely to change const val COMICAKE_DEFAULT_API_ENDPOINT = "/api" // Highly unlikely to change
const val COMICAKE_DEFAULT_READER_ENDPOINT = "/r" // Can change based on CC config const val COMICAKE_DEFAULT_READER_ENDPOINT = "/r" // Can change based on CC config
open class ComiCake(override val name: String, override val baseUrl: String, override val lang: String, val readerEndpoint: String = COMICAKE_DEFAULT_READER_ENDPOINT, val apiEndpoint: String = COMICAKE_DEFAULT_API_ENDPOINT) : HttpSource() { open 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 versionId = 1
override val supportsLatest = true override val supportsLatest = true
private val readerBase = baseUrl + readerEndpoint private val readerBase = baseUrl + readerEndpoint
@ -40,18 +43,18 @@ open class ComiCake(override val name: String, override val baseUrl: String, ove
return getMangasPageFromComicsResponse(res) return getMangasPageFromComicsResponse(res)
} }
private fun getMangasPageFromComicsResponse(json: String, nested: Boolean = false) : MangasPage { private fun getMangasPageFromComicsResponse(json: String, nested: Boolean = false): MangasPage {
var response = JSONObject(json) val response = JSONObject(json)
var results = response.getJSONArray("results") val results = response.getJSONArray("results")
val mangas = ArrayList<SManga>() val mangas = ArrayList<SManga>()
val ids = mutableListOf<Int>(); val ids = mutableListOf<Int>()
for (i in 0 until results.length()) { for (i in 0 until results.length()) {
val obj = results.getJSONObject(i) val obj = results.getJSONObject(i)
if(nested) { if (nested) {
val nestedComic = obj.getJSONObject("comic"); val nestedComic = obj.getJSONObject("comic")
val id = nestedComic.getInt("id") val id = nestedComic.getInt("id")
if(ids.contains(id)) if (ids.contains(id))
continue continue
ids.add(id) ids.add(id)
val manga = SManga.create() val manga = SManga.create()
@ -60,13 +63,13 @@ open class ComiCake(override val name: String, override val baseUrl: String, ove
mangas.add(manga) mangas.add(manga)
} else { } else {
val id = obj.getInt("id") val id = obj.getInt("id")
if(ids.contains(id)) if (ids.contains(id))
continue continue
ids.add(id) ids.add(id)
mangas.add(parseComicJson(obj)) mangas.add(parseComicJson(obj))
} }
} }
return MangasPage(mangas, if (response.getString("next").isNullOrEmpty() || response.getString("next") == "null") false else true) return MangasPage(mangas, !(response.getString("next").isNullOrEmpty() || response.getString("next") == "null"))
} }
override fun mangaDetailsRequest(manga: SManga): Request { override fun mangaDetailsRequest(manga: SManga): Request {
@ -78,11 +81,11 @@ open class ComiCake(override val name: String, override val baseUrl: String, ove
return parseComicJson(comicJson, true) return parseComicJson(comicJson, true)
} }
private fun parseComicJson(obj: JSONObject, human: Boolean = false) = SManga.create().apply { private fun parseComicJson(obj: JSONObject, human: Boolean = false) = SManga.create().apply {
if(human) { url = if (human) {
url = "$readerBase/series/${obj.getString("slug")}/" "$readerBase/series/${obj.getString("slug")}/"
} else { } else {
url = obj.getInt("id").toString() // Yeah, I know... Feel free to improve on this obj.getInt("id").toString() // Yeah, I know... Feel free to improve on this
} }
title = obj.getString("name") title = obj.getString("name")
thumbnail_url = obj.getString("cover") thumbnail_url = obj.getString("cover")
@ -93,9 +96,9 @@ open class ComiCake(override val name: String, override val baseUrl: String, ove
status = SManga.UNKNOWN status = SManga.UNKNOWN
} }
private fun parseListNames(arr: JSONArray) : String { private fun parseListNames(arr: JSONArray): String {
var hold = ArrayList<String>(arr.length()) val hold = ArrayList<String>(arr.length())
for(i in 0 until arr.length()) for (i in 0 until arr.length())
hold.add(arr.getJSONObject(i).getString("name")) hold.add(arr.getJSONObject(i).getString("name"))
return hold.sorted().joinToString(", ") return hold.sorted().joinToString(", ")
} }
@ -133,7 +136,7 @@ open class ComiCake(override val name: String, override val baseUrl: String, ove
override fun chapterListParse(response: Response): List<SChapter> { override fun chapterListParse(response: Response): List<SChapter> {
val chapterJson = JSONObject(response.body()!!.string()) val chapterJson = JSONObject(response.body()!!.string())
var results = chapterJson.getJSONArray("results") val results = chapterJson.getJSONArray("results")
val ret = ArrayList<SChapter>() val ret = ArrayList<SChapter>()
for (i in 0 until results.length()) { for (i in 0 until results.length()) {
ret.add(parseChapterJson(results.getJSONObject(i))) ret.add(parseChapterJson(results.getJSONObject(i)))
@ -144,13 +147,13 @@ open class ComiCake(override val name: String, override val baseUrl: String, ove
override fun pageListParse(response: Response): List<Page> { override fun pageListParse(response: Response): List<Page> {
val webPub = JSONObject(response.body()!!.string()) val webPub = JSONObject(response.body()!!.string())
val readingOrder = webPub.getJSONArray("readingOrder") val readingOrder = webPub.getJSONArray("readingOrder")
val ret = ArrayList<Page>(); val ret = ArrayList<Page>()
for (i in 0 until readingOrder.length()) { for (i in 0 until readingOrder.length()) {
var pageUrl = readingOrder.getJSONObject(i).getString("href") val pageUrl = readingOrder.getJSONObject(i).getString("href")
ret.add(Page(i, "", pageUrl)) ret.add(Page(i, "", pageUrl))
} }
return ret return ret
} }
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("This method should not be called!") override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("This method should not be called!")
} }

View File

@ -9,11 +9,11 @@ class ComiCakeFactory : SourceFactory {
fun getAllComiCake(): List<Source> { fun getAllComiCake(): List<Source> {
return listOf( return listOf(
WhimSubs(), WhimSubs(),
ChampionScans() ChampionScans()
) )
} }
class WhimSubs : ComiCake("WhimSubs", "https://whimsubs.xyz", "en") class WhimSubs : ComiCake("WhimSubs", "https://whimsubs.xyz", "en")
class ChampionScans : ComiCake("Champion Scans", "https://reader.championscans.com", "en", "/") class ChampionScans : ComiCake("Champion Scans", "https://reader.championscans.com", "en", "/")

View File

@ -4,8 +4,8 @@ apply plugin: 'kotlin-android'
ext { ext {
appName = 'Tachiyomi: E-Hentai' appName = 'Tachiyomi: E-Hentai'
pkgNameSuffix = 'all.ehentai' pkgNameSuffix = 'all.ehentai'
extClass = '.EHJapanese; .EHEnglish; .EHChinese; .EHDutch; .EHFrench; .EHGerman; .EHHungarian; .EHItalian; .EHKorean; .EHPolish; .EHPolish; .EHPortuguese; .EHRussian; .EHSpanish; .EHThai; .EHVietnamese; .EHSpeechless; .EHOther' extClass = '.EHJapanese; .EHEnglish; .EHChinese; .EHDutch; .EHFrench; .EHGerman; .EHHungarian; .EHItalian; .EHKorean; .EHPolish; .EHPortuguese; .EHRussian; .EHSpanish; .EHThai; .EHVietnamese; .EHSpeechless; .EHOther'
extVersionCode = 4 extVersionCode = 5
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -3,23 +3,23 @@ package eu.kanade.tachiyomi.extension.all.ehentai
/** /**
* E-Hentai languages * E-Hentai languages
*/ */
class EHJapanese: EHentai("ja", "japanese") class EHJapanese : EHentai("ja", "japanese")
class EHEnglish: EHentai("en", "english") class EHEnglish : EHentai("en", "english")
class EHChinese: EHentai("zh", "chinese") class EHChinese : EHentai("zh", "chinese")
class EHDutch: EHentai("nl", "dutch") class EHDutch : EHentai("nl", "dutch")
class EHFrench: EHentai("fr", "french") class EHFrench : EHentai("fr", "french")
class EHGerman: EHentai("de", "german") class EHGerman : EHentai("de", "german")
class EHHungarian: EHentai("hu", "hungarian") class EHHungarian : EHentai("hu", "hungarian")
class EHItalian: EHentai("it", "italian") class EHItalian : EHentai("it", "italian")
class EHKorean: EHentai("ko", "korean") class EHKorean : EHentai("ko", "korean")
class EHPolish: EHentai("pl", "polish") class EHPolish : EHentai("pl", "polish")
class EHPortuguese: EHentai("pt", "portuguese") class EHPortuguese : EHentai("pt", "portuguese")
class EHRussian: EHentai("ru", "russian") class EHRussian : EHentai("ru", "russian")
class EHSpanish: EHentai("es", "spanish") class EHSpanish : EHentai("es", "spanish")
class EHThai: EHentai("th", "thai") class EHThai : EHentai("th", "thai")
class EHVietnamese: EHentai("vi", "vietnamese") class EHVietnamese : EHentai("vi", "vietnamese")
class EHSpeechless: EHentai("none", "n/a") class EHSpeechless : EHentai("none", "n/a")
class EHOther: EHentai("other", "other") class EHOther : EHentai("other", "other")
fun getAllEHentaiLanguages() = listOf( fun getAllEHentaiLanguages() = listOf(
EHJapanese(), EHJapanese(),

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.extension.all.ehentai package eu.kanade.tachiyomi.extension.all.ehentai
import kotlin.math.ln
import kotlin.math.pow
/** /**
* Various utility methods used in the E-Hentai source * Various utility methods used in the E-Hentai source
*/ */
@ -36,18 +39,18 @@ operator fun StringBuilder.plusAssign(other: String) {
*/ */
fun humanReadableByteCount(bytes: Long, si: Boolean): String { fun humanReadableByteCount(bytes: Long, si: Boolean): String {
val unit = if (si) 1000 else 1024 val unit = if (si) 1000 else 1024
if (bytes < unit) return bytes.toString() + " B" if (bytes < unit) return "$bytes B"
val exp = (Math.log(bytes.toDouble()) / Math.log(unit.toDouble())).toInt() val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i" val pre = (if (si) "kMGTPE" else "KMGTPE")[exp - 1] + if (si) "" else "i"
return String.format("%.1f %sB", bytes / Math.pow(unit.toDouble(), exp.toDouble()), pre) return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
} }
private val KB_FACTOR = 1000 private const val KB_FACTOR = 1000
private val KIB_FACTOR = 1024 private const val KIB_FACTOR = 1024
private val MB_FACTOR = 1000 * KB_FACTOR private const val MB_FACTOR = 1000 * KB_FACTOR
private val MIB_FACTOR = 1024 * KIB_FACTOR private const val MIB_FACTOR = 1024 * KIB_FACTOR
private val GB_FACTOR = 1000 * MB_FACTOR private const val GB_FACTOR = 1000 * MB_FACTOR
private val GIB_FACTOR = 1024 * MIB_FACTOR private const val GIB_FACTOR = 1024 * MIB_FACTOR
/** /**
* Parse human readable size Strings * Parse human readable size Strings
@ -64,4 +67,4 @@ fun parseHumanReadableByteCount(arg0: String): Double? {
"KiB" -> return ret * KIB_FACTOR "KiB" -> return ret * KIB_FACTOR
} }
return null return null
} }

View File

@ -3,24 +3,15 @@ package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.*
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.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.CacheControl import okhttp3.*
import okhttp3.CookieJar
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
import java.net.URLEncoder import java.net.URLEncoder
open class EHentai(override val lang: String, val ehLang: String) : HttpSource() { open class EHentai(override val lang: String, private val ehLang: String) : HttpSource() {
override val name = "E-Hentai" override val name = "E-Hentai"
@ -51,15 +42,13 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
return MangasPage(parsedMangas, hasNextPage) return MangasPage(parsedMangas, hasNextPage)
} }
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = Observable.just(listOf(SChapter.create().apply {
= Observable.just(listOf(SChapter.create().apply {
url = manga.url url = manga.url
name = "Chapter" name = "Chapter"
chapter_number = 1f chapter_number = 1f
})) }))
override fun fetchPageList(chapter: SChapter) override fun fetchPageList(chapter: SChapter) = fetchChapterPage(chapter, "$baseUrl/${chapter.url}").map {
= fetchChapterPage(chapter, "$baseUrl/${chapter.url}").map {
it.mapIndexed { i, s -> it.mapIndexed { i, s ->
Page(i, s) Page(i, s)
} }
@ -80,8 +69,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
} }
} }
private fun parseChapterPage(response: Element) private fun parseChapterPage(response: Element) = with(response) {
= with(response) {
select(".gdtm a").map { select(".gdtm a").map {
Pair(it.child(0).attr("alt").toInt(), it.attr("href")) Pair(it.child(0).attr("alt").toInt(), it.attr("href"))
}.sortedBy(Pair<Int, String>::first).map { it.second } }.sortedBy(Pair<Int, String>::first).map { it.second }
@ -90,8 +78,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess() private fun chapterPageCall(np: String) = client.newCall(chapterPageRequest(np)).asObservableSuccess()
private fun chapterPageRequest(np: String) = exGet(np, null, headers) private fun chapterPageRequest(np: String) = exGet(np, null, headers)
private fun nextPageUrl(element: Element) private fun nextPageUrl(element: Element) = element.select("a[onclick=return false]").last()?.let {
= element.select("a[onclick=return false]").last()?.let {
if (it.text() == ">") it.attr("href") else null if (it.text() == ">") it.attr("href") else null
} }
@ -115,8 +102,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
override fun searchMangaParse(response: Response) = genericMangaParse(response) override fun searchMangaParse(response: Response) = genericMangaParse(response)
override fun latestUpdatesParse(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) private fun exGet(url: String, page: Int? = null, additionalHeaders: Headers? = null, cache: Boolean = true) = GET(page?.let {
= GET(page?.let {
addParam(url, "page", (it - 1).toString()) addParam(url, "page", (it - 1).toString())
} ?: url, additionalHeaders?.let { } ?: url, additionalHeaders?.let {
val headers = headers.newBuilder() val headers = headers.newBuilder()
@ -204,7 +190,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
Tag(it.text().trim(), Tag(it.text().trim(),
it.hasClass("gtl")) it.hasClass("gtl"))
} }
tags.put(namespace, currentTags) tags[namespace] = currentTags
} }
//Copy metadata to manga //Copy metadata to manga
@ -214,19 +200,15 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
} }
} }
override fun chapterListParse(response: Response) override fun chapterListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
= throw UnsupportedOperationException("Unused method was called somehow!")
override fun pageListParse(response: Response) override fun pageListParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
= throw UnsupportedOperationException("Unused method was called somehow!")
override fun fetchImageUrl(page: Page) override fun fetchImageUrl(page: Page) = client.newCall(imageUrlRequest(page))
= client.newCall(imageUrlRequest(page))
.asObservableSuccess() .asObservableSuccess()
.map { realImageUrlParse(it, page) }!! .map { realImageUrlParse(it, page) }!!
private fun realImageUrlParse(response: Response, page: Page) private fun realImageUrlParse(response: Response, page: Page) = with(response.asJsoup()) {
= with(response.asJsoup()) {
val currentImage = getElementById("img").attr("src") val currentImage = getElementById("img").attr("src")
//TODO We cannot currently do this as page.url is immutable //TODO We cannot currently do this as page.url is immutable
//Each press of the retry button will choose another server //Each press of the retry button will choose another server
@ -236,8 +218,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
currentImage currentImage
}!! }!!
override fun imageUrlParse(response: Response) override fun imageUrlParse(response: Response) = throw UnsupportedOperationException("Unused method was called somehow!")
= throw UnsupportedOperationException("Unused method was called somehow!")
private val cookiesHeader by lazy { private val cookiesHeader by lazy {
val cookies = mutableMapOf<String, String>() val cookies = mutableMapOf<String, String>()
@ -253,25 +234,21 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
.flatMap { it.second } .flatMap { it.second }
.joinToString("x") .joinToString("x")
cookies.put("uconfig", buildSettings(settings)) cookies["uconfig"] = buildSettings(settings)
buildCookies(cookies) buildCookies(cookies)
} }
//Headers //Headers
override fun headersBuilder() override fun headersBuilder() = super.headersBuilder().add("Cookie", cookiesHeader)!!
= super.headersBuilder().add("Cookie", cookiesHeader)!!
private fun buildSettings(settings: List<String?>) private fun buildSettings(settings: List<String?>) = settings.filterNotNull().joinToString(separator = "-")
= settings.filterNotNull().joinToString(separator = "-")
private fun buildCookies(cookies: Map<String, String>) private fun buildCookies(cookies: Map<String, String>) = cookies.entries.joinToString(separator = "; ", postfix = ";") {
= cookies.entries.map {
"${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}" "${URLEncoder.encode(it.key, "UTF-8")}=${URLEncoder.encode(it.value, "UTF-8")}"
}.joinToString(separator = "; ", postfix = ";") }
private fun addParam(url: String, param: String, value: String) private fun addParam(url: String, param: String, value: String) = Uri.parse(url)
= Uri.parse(url)
.buildUpon() .buildUpon()
.appendQueryParameter(param, value) .appendQueryParameter(param, value)
.toString() .toString()
@ -295,9 +272,9 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
AdvancedGroup() AdvancedGroup()
) )
class GenreOption(name: String, val genreId: String) : Filter.CheckBox(name, false), UriFilter { class GenreOption(name: String, private val genreId: String) : Filter.CheckBox(name, false), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
builder.appendQueryParameter("f_" + genreId, if (state) "1" else "0") builder.appendQueryParameter("f_$genreId", if (state) "1" else "0")
} }
} }
@ -314,7 +291,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
GenreOption("Misc", "misc") GenreOption("Misc", "misc")
)) ))
class AdvancedOption(name: String, val param: String, defValue: Boolean = false) : Filter.CheckBox(name, defValue), UriFilter { class AdvancedOption(name: String, private val param: String, defValue: Boolean = false) : Filter.CheckBox(name, defValue), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state) if (state)
builder.appendQueryParameter(param, "on") builder.appendQueryParameter(param, "on")
@ -329,7 +306,7 @@ open class EHentai(override val lang: String, val ehLang: String) : HttpSource()
"5 stars" "5 stars"
)), UriFilter { )), UriFilter {
override fun addToUri(builder: Uri.Builder) { override fun addToUri(builder: Uri.Builder) {
if (state > 0) builder.appendQueryParameter("f_srdd", Integer.toString(state + 1)) if (state > 0) builder.appendQueryParameter("f_srdd", (state + 1).toString())
} }
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.extension.all.ehentai; package eu.kanade.tachiyomi.extension.all.ehentai
import android.net.Uri import android.net.Uri
@ -32,25 +32,21 @@ class ExGalleryMetadata {
val tags: MutableMap<String, List<Tag>> = mutableMapOf() val tags: MutableMap<String, List<Tag>> = mutableMapOf()
companion object { companion object {
private fun splitGalleryUrl(url: String) private fun splitGalleryUrl(url: String) = url.let {
= url.let {
//Only parse URL if is full URL //Only parse URL if is full URL
val pathSegments = if(it.startsWith("http")) val pathSegments = if (it.startsWith("http"))
Uri.parse(it).pathSegments Uri.parse(it).pathSegments
else else
it.split('/') it.split('/')
pathSegments.filterNot(String::isNullOrBlank) pathSegments.filterNot(String::isNullOrBlank)
} }
fun galleryId(url: String) = splitGalleryUrl(url)[1] private fun galleryId(url: String) = splitGalleryUrl(url)[1]
fun galleryToken(url: String) = private fun galleryToken(url: String) = splitGalleryUrl(url)[2]
splitGalleryUrl(url)[2]
fun normalizeUrl(id: String, token: String) private fun normalizeUrl(id: String, token: String) = "/g/$id/$token/?nw=always"
= "/g/$id/$token/?nw=always"
fun normalizeUrl(url: String) fun normalizeUrl(url: String) = normalizeUrl(galleryId(url), galleryToken(url))
= normalizeUrl(galleryId(url), galleryToken(url))
} }
} }

View File

@ -37,8 +37,8 @@ fun ExGalleryMetadata.copyTo(manga: SManga) {
manga.status = SManga.COMPLETED manga.status = SManga.COMPLETED
title?.let { t -> title?.let { t ->
if (ONGOING_SUFFIX.any { if (ONGOING_SUFFIX.any {
t.endsWith(it, ignoreCase = true) t.endsWith(it, ignoreCase = true)
}) manga.status = SManga.ONGOING }) manga.status = SManga.ONGOING
} }
//Build a nice looking description out of what we know //Build a nice looking description out of what we know
@ -71,8 +71,7 @@ fun ExGalleryMetadata.copyTo(manga: SManga) {
.joinToString(separator = "\n") .joinToString(separator = "\n")
} }
private fun buildTagsDescription(metadata: ExGalleryMetadata) private fun buildTagsDescription(metadata: ExGalleryMetadata) = StringBuilder("Tags:\n").apply {
= StringBuilder("Tags:\n").apply {
//BiConsumer only available in Java 8, we have to use destructuring here //BiConsumer only available in Java 8, we have to use destructuring here
metadata.tags.forEach { (namespace, tags) -> metadata.tags.forEach { (namespace, tags) ->
if (tags.isNotEmpty()) { if (tags.isNotEmpty()) {

View File

@ -3,9 +3,9 @@ apply plugin: 'kotlin-android'
ext { ext {
appName = 'Tachiyomi: FoolSlide' appName = 'Tachiyomi: FoolSlide'
pkgNameSuffix = "all.foolslide" pkgNameSuffix = 'all.foolslide'
extClass = '.FoolSlideFactory' extClass = '.FoolSlideFactory'
extVersionCode = 21 extVersionCode = 22
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -16,7 +16,10 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
open class FoolSlide(override val name: String, override val baseUrl: String, override val lang: String, val urlModifier: String = "") : ParsedHttpSource() { open class FoolSlide(override val name: String,
override val baseUrl: String,
override val lang: String,
val urlModifier: String = "") : ParsedHttpSource() {
protected open val dedupeLatestUpdates = true protected open val dedupeLatestUpdates = true
@ -32,7 +35,7 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
override fun latestUpdatesParse(response: Response): MangasPage { override fun latestUpdatesParse(response: Response): MangasPage {
val mp = super.latestUpdatesParse(response) val mp = super.latestUpdatesParse(response)
return if(dedupeLatestUpdates) { return if (dedupeLatestUpdates) {
val mangas = mp.mangas.distinctBy { it.url }.filterNot { latestUpdatesUrls.contains(it.url) } val mangas = mp.mangas.distinctBy { it.url }.filterNot { latestUpdatesUrls.contains(it.url) }
latestUpdatesUrls.addAll(mangas.map { it.url }) latestUpdatesUrls.addAll(mangas.map { it.url })
MangasPage(mangas, mp.hasNextPage) MangasPage(mangas, mp.hasNextPage)
@ -96,8 +99,7 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
override fun searchMangaNextPageSelector() = "a:has(span.next)" override fun searchMangaNextPageSelector() = "a:has(span.next)"
override fun mangaDetailsRequest(manga: SManga) override fun mangaDetailsRequest(manga: SManga) = allowAdult(super.mangaDetailsRequest(manga))
= allowAdult(super.mangaDetailsRequest(manga))
open val mangaDetailsInfoSelector = "div.info" open val mangaDetailsInfoSelector = "div.info"
open val mangaDetailsThumbnailSelector = "div.thumbnail img" open val mangaDetailsThumbnailSelector = "div.thumbnail img"
@ -119,14 +121,13 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
*/ */
private fun allowAdult(request: Request) = allowAdult(request.url().toString()) private fun allowAdult(request: Request) = allowAdult(request.url().toString())
protected fun allowAdult(url: String): Request { private fun allowAdult(url: String): Request {
return POST(url, body = FormBody.Builder() return POST(url, body = FormBody.Builder()
.add("adult", "true") .add("adult", "true")
.build()) .build())
} }
override fun chapterListRequest(manga: SManga) override fun chapterListRequest(manga: SManga) = allowAdult(super.chapterListRequest(manga))
= allowAdult(super.chapterListRequest(manga))
override fun chapterListSelector() = "div.group div.element, div.list div.element" override fun chapterListSelector() = "div.group div.element, div.list div.element"
@ -140,7 +141,8 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
val chapter = SChapter.create() val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href")) chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text() chapter.name = urlElement.text()
chapter.date_upload = dateElement.text()?.let { parseChapterDate(it.substringAfter(", ")) } ?: 0 chapter.date_upload = dateElement.text()?.let { parseChapterDate(it.substringAfter(", ")) }
?: 0
return chapter return chapter
} }
@ -172,18 +174,18 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
var result = DATE_FORMAT_1.parseOrNull(date) var result = DATE_FORMAT_1.parseOrNull(date)
for(dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES) { for (dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES) {
if (result == null) if (result == null)
result = dateFormat.parseOrNull(date) result = dateFormat.parseOrNull(date)
else else
break break
} }
for(dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES_NO_YEAR) { for (dateFormat in DATE_FORMATS_WITH_ORDINAL_SUFFIXES_NO_YEAR) {
if (result == null) { if (result == null) {
result = dateFormat.parseOrNull(date) result = dateFormat.parseOrNull(date)
if(result != null) { if (result != null) {
// Result parsed but no year, copy current year over // Result parsed but no year, copy current year over
result = Calendar.getInstance().apply { result = Calendar.getInstance().apply {
time = result time = result
@ -230,13 +232,12 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
private fun SimpleDateFormat.parseOrNull(string: String): Date? { private fun SimpleDateFormat.parseOrNull(string: String): Date? {
return try { return try {
parse(string) parse(string)
} catch(e: ParseException) { } catch (e: ParseException) {
null null
} }
} }
override fun pageListRequest(chapter: SChapter) override fun pageListRequest(chapter: SChapter) = allowAdult(super.pageListRequest(chapter))
= allowAdult(super.pageListRequest(chapter))
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val doc = document.toString() val doc = document.toString()
@ -266,4 +267,4 @@ open class FoolSlide(override val name: String, override val baseUrl: String, ov
SimpleDateFormat("dd'$it' MMMM", Locale.US) SimpleDateFormat("dd'$it' MMMM", Locale.US)
} }
} }
} }

View File

@ -6,7 +6,8 @@ import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SManga
import okhttp3.Request import okhttp3.Request
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
@ -17,34 +18,33 @@ class FoolSlideFactory : SourceFactory {
fun getAllFoolSlide(): List<Source> { fun getAllFoolSlide(): List<Source> {
return listOf( return listOf(
JaminisBox(), JaminisBox(),
HelveticaScans(), HelveticaScans(),
SenseScans(), SenseScans(),
KireiCake(), KireiCake(),
SilentSky(), SilentSky(),
Mangatellers(), Mangatellers(),
IskultripScans(), IskultripScans(),
AnataNoMotokare(), AnataNoMotokare(),
DeathTollScans(), DeathTollScans(),
DKThias(), DKThias(),
WorldThree(), WorldThree(),
DokiFansubs(), DokiFansubs(),
YuriIsm(), YuriIsm(),
AjiaNoScantrad(), AjiaNoScantrad(),
OneTimeScans(), OneTimeScans(),
TsubasaSociety(), TsubasaSociety(),
MangaScouts(), MangaScouts(),
StormInHeaven(), StormInHeaven(),
Lilyreader(), Lilyreader(),
MidnightHaven(), Russification(),
Russification(), EvilFlowers(),
EvilFlowers(), AkaiYuhiMunTeam(),
AkaiYuhiMunTeam(), LupiTeam(),
LupiTeam(), HentaiCafe(),
HentaiCafe(), ShoujoSense(),
ShoujoSense(), TheCatScans(),
TheCatScans(), ShoujoHearts()
ShoujoHearts()
) )
} }
@ -74,7 +74,7 @@ class SenseScans : FoolSlide("Sense-Scans", "https://sensescans.com", "en", "/re
class KireiCake : FoolSlide("Kirei Cake", "https://reader.kireicake.com", "en") class KireiCake : FoolSlide("Kirei Cake", "https://reader.kireicake.com", "en")
class SilentSky : FoolSlide("Silent Sky", "http://reader.silentsky-scans.net", "en") class SilentSky : FoolSlide("Silent Sky", "https://reader.silentsky-scans.net", "en")
class Mangatellers : FoolSlide("Mangatellers", "http://www.mangatellers.gr", "en", "/reader/reader") { class Mangatellers : FoolSlide("Mangatellers", "http://www.mangatellers.gr", "en", "/reader/reader") {
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
@ -108,12 +108,10 @@ class TsubasaSociety : FoolSlide("Tsubasa Society", "https://www.tsubasasociety.
class MangaScouts : FoolSlide("MangaScouts", "http://onlinereader.mangascouts.org", "de") class MangaScouts : FoolSlide("MangaScouts", "http://onlinereader.mangascouts.org", "de")
class StormInHeaven : FoolSlide("Storm in Heaven", "http://www.storm-in-heaven.net", "it", "/reader-sih") class StormInHeaven : FoolSlide("Storm in Heaven", "https://www.storm-in-heaven.net", "it", "/reader-sih")
class Lilyreader : FoolSlide("Lilyreader", "https://manga.smuglo.li", "en") class Lilyreader : FoolSlide("Lilyreader", "https://manga.smuglo.li", "en")
class MidnightHaven : FoolSlide("Midnight Haven", "http://midnighthaven.shounen-ai.net", "en", "/reader")
class Russification : FoolSlide("Русификация", "https://rusmanga.ru", "ru") class Russification : FoolSlide("Русификация", "https://rusmanga.ru", "ru")
class EvilFlowers : FoolSlide("Evil Flowers", "http://reader.evilflowers.com", "en") class EvilFlowers : FoolSlide("Evil Flowers", "http://reader.evilflowers.com", "en")
@ -158,5 +156,3 @@ class ShoujoHearts : FoolSlide("ShoujoHearts", "http://shoujohearts.com", "en",
return manga return manga
} }
} }

View File

@ -63,7 +63,7 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
} }
filters.findInstance<ArtistFilter>()?.let { f -> filters.findInstance<ArtistFilter>()?.let { f ->
if(f.state.isNotBlank()) { if (f.state.isNotBlank()) {
requireNoUrl() requireNoUrl()
url = "/artist/${f.state url = "/artist/${f.state
.trim() .trim()
@ -72,19 +72,19 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
} }
} }
filters.findInstance<BookFilter>()?.let { f -> filters.findInstance<BookFilter>()?.let { f ->
if(f.state) { if (f.state) {
requireNoUrl() requireNoUrl()
url = "/category/book/" url = "/category/book/"
} }
} }
filters.findInstance<TagFilter>()?.let { f -> filters.findInstance<TagFilter>()?.let { f ->
if(f.state != 0) { if (f.state != 0) {
requireNoUrl() requireNoUrl()
url = "/tag/${f.values[f.state].name}/" url = "/tag/${f.values[f.state].name}/"
} }
} }
if(query.isNotBlank()) { if (query.isNotBlank()) {
requireNoUrl() requireNoUrl()
url = "/" url = "/"
queryString = "s=" + URLEncoder.encode(query, "UTF-8") queryString = "s=" + URLEncoder.encode(query, "UTF-8")
@ -97,8 +97,8 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
private fun pagedRequest(url: String, page: Int, queryString: String? = null): Request { private fun pagedRequest(url: String, page: Int, queryString: String? = null): Request {
// The site redirects page 1 -> url-without-page so we do this redirect early for optimization // The site redirects page 1 -> url-without-page so we do this redirect early for optimization
val builtUrl = if(page == 1) url else "${url}page/$page/" val builtUrl = if (page == 1) url else "${url}page/$page/"
return GET(if(queryString != null) "$builtUrl?$queryString" else builtUrl) return GET(if (queryString != null) "$builtUrl?$queryString" else builtUrl)
} }
override fun searchMangaParse(response: Response) = latestUpdatesParse(response) override fun searchMangaParse(response: Response) = latestUpdatesParse(response)
@ -106,7 +106,7 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return client.newCall(searchMangaRequest(page, query, filters)) return client.newCall(searchMangaRequest(page, query, filters))
.asObservable().doOnNext { response -> .asObservable().doOnNext { response ->
if(!response.isSuccessful) { if (!response.isSuccessful) {
response.close() response.close()
// Better error message for invalid artist // Better error message for invalid artist
if (response.code() == 404 if (response.code() == 404
@ -132,7 +132,6 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
class ArtistFilter : Filter.Text("Artist (must be exact match)") class ArtistFilter : Filter.Text("Artist (must be exact match)")
class BookFilter : Filter.CheckBox("Show books only", false) class BookFilter : Filter.CheckBox("Show books only", false)
class TagFilter : Filter.Select<Tag>("Tag", arrayOf( class TagFilter : Filter.Select<Tag>("Tag", arrayOf(
Tag("all", "All"),
Tag("ahegao", "Ahegao"), Tag("ahegao", "Ahegao"),
Tag("anal", "Anal"), Tag("anal", "Anal"),
Tag("big-ass", "Big ass"), Tag("big-ass", "Big ass"),
@ -155,9 +154,9 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
Tag("group", "Group"), Tag("group", "Group"),
Tag("hairy", "Hairy"), Tag("hairy", "Hairy"),
Tag("handjob", "Handjob"), Tag("handjob", "Handjob"),
Tag("heart-pupils", "Heart pupils"),
Tag("housewife", "Housewife"), Tag("housewife", "Housewife"),
Tag("incest", "Incest"), Tag("incest", "Incest"),
Tag("large-breast", "Large breast"),
Tag("lingerie", "Lingerie"), Tag("lingerie", "Lingerie"),
Tag("loli", "Loli"), Tag("loli", "Loli"),
Tag("masturbation", "Masturbation"), Tag("masturbation", "Masturbation"),
@ -167,7 +166,7 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
Tag("pettanko", "Pettanko"), Tag("pettanko", "Pettanko"),
Tag("rape", "Rape"), Tag("rape", "Rape"),
Tag("schoolgirl", "Schoolgirl"), Tag("schoolgirl", "Schoolgirl"),
Tag("sex-toys", "Sex Toys"), Tag("sex-toys", "Sex toys"),
Tag("shota", "Shota"), Tag("shota", "Shota"),
Tag("socks", "Socks"), Tag("socks", "Socks"),
Tag("stocking", "Stocking"), Tag("stocking", "Stocking"),
@ -175,11 +174,12 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
Tag("swimsuit", "Swimsuit"), Tag("swimsuit", "Swimsuit"),
Tag("teacher", "Teacher"), Tag("teacher", "Teacher"),
Tag("tsundere", "Tsundere"), Tag("tsundere", "Tsundere"),
Tag("uncensored", "uncensored"), Tag("uncensored", "Uncensored"),
Tag("vanilla", "Vanilla"), Tag("vanilla", "Vanilla"),
Tag("x-ray", "X-ray") Tag("x-ray", "X-Ray")
)) ))
class Tag(val name: String, val displayName: String) {
class Tag(val name: String, private val displayName: String) {
override fun toString() = displayName override fun toString() = displayName
} }
@ -189,4 +189,4 @@ class HentaiCafe : FoolSlide("Hentai Cafe", "https://hentai.cafe", "en", "/manga
} }
} }
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T

View File

@ -5,7 +5,7 @@ ext {
appName = 'Tachiyomi: Genkan (multiple sources)' appName = 'Tachiyomi: Genkan (multiple sources)'
pkgNameSuffix = 'all.genkan' pkgNameSuffix = 'all.genkan'
extClass = '.GenkanFactory' extClass = '.GenkanFactory'
extVersionCode = 2 extVersionCode = 3
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -14,9 +14,9 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
abstract class Genkan( abstract class Genkan(
override val name: String, override val name: String,
override val baseUrl: String, final override val baseUrl: String,
override val lang: String override val lang: String
) : ParsedHttpSource() { ) : ParsedHttpSource() {
override val supportsLatest = true override val supportsLatest = true
@ -27,7 +27,7 @@ abstract class Genkan(
private val popularMangaUrl = "$baseUrl/comics?page=" // Search is also based off this val private val popularMangaUrl = "$baseUrl/comics?page=" // Search is also based off this val
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$popularMangaUrl$page") return GET("$popularMangaUrl$page")
} }
override fun latestUpdatesSelector() = popularMangaSelector() override fun latestUpdatesSelector() = popularMangaSelector()
@ -36,8 +36,8 @@ abstract class Genkan(
private val latestUpdatesTitles = mutableSetOf<String>() private val latestUpdatesTitles = mutableSetOf<String>()
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
if (page == 1) latestUpdatesTitles.clear() if (page == 1) latestUpdatesTitles.clear()
return GET("$baseUrl/latest?page=$page") return GET("$baseUrl/latest?page=$page")
} }
// To prevent dupes // To prevent dupes
@ -165,10 +165,10 @@ abstract class Genkan(
// If the date string contains the word "ago" send it off for relative date parsing otherwise use dateFormat // If the date string contains the word "ago" send it off for relative date parsing otherwise use dateFormat
private fun parseChapterDate(string: String): Long? { private fun parseChapterDate(string: String): Long? {
if ("ago" in string) { return if ("ago" in string) {
return parseRelativeDate(string) ?: 0 parseRelativeDate(string) ?: 0
} else { } else {
return dateFormat.parse(string).time dateFormat.parse(string).time
} }
} }
@ -177,14 +177,14 @@ abstract class Genkan(
val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ") val trimmedDate = date.substringBefore(" ago").removeSuffix("s").split(" ")
val calendar = Calendar.getInstance() val calendar = Calendar.getInstance()
when (trimmedDate[1]){ when (trimmedDate[1]) {
"month" -> calendar.apply{add(Calendar.MONTH, -trimmedDate[0].toInt())} "month" -> calendar.apply { add(Calendar.MONTH, -trimmedDate[0].toInt()) }
"week" -> calendar.apply{add(Calendar.WEEK_OF_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())} "day" -> calendar.apply { add(Calendar.DAY_OF_MONTH, -trimmedDate[0].toInt()) }
"hour" -> calendar.apply{add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt())} "hour" -> calendar.apply { add(Calendar.HOUR_OF_DAY, -trimmedDate[0].toInt()) }
"minute" -> calendar.apply{add(Calendar.MINUTE, -trimmedDate[0].toInt())} "minute" -> calendar.apply { add(Calendar.MINUTE, -trimmedDate[0].toInt()) }
"second" -> calendar.apply{add(Calendar.SECOND, 0)} "second" -> calendar.apply { add(Calendar.SECOND, 0) }
} }
return calendar.timeInMillis return calendar.timeInMillis
} }
@ -192,10 +192,10 @@ abstract class Genkan(
override fun pageListParse(document: Document): List<Page> { override fun pageListParse(document: Document): List<Page> {
val pages = mutableListOf<Page>() val pages = mutableListOf<Page>()
val allImages = document.select("div#pages-container + script").first().data() val allImages = document.select("div#pages-container + script").first().data()
.substringAfter("[").substringBefore("];") .substringAfter("[").substringBefore("];")
.replace(Regex("""["\\]"""), "") .replace(Regex("""["\\]"""), "")
.split(",") .split(",")
for (i in 0 until allImages.size) { for (i in 0 until allImages.size) {
pages.add(Page(i, "", allImages[i])) pages.add(Page(i, "", allImages[i]))
@ -207,5 +207,4 @@ abstract class Genkan(
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used")
override fun getFilterList() = FilterList() override fun getFilterList() = FilterList()
} }

View File

@ -5,12 +5,12 @@ import eu.kanade.tachiyomi.source.SourceFactory
class GenkanFactory : SourceFactory { class GenkanFactory : SourceFactory {
override fun createSources(): List<Source> = listOf( override fun createSources(): List<Source> = listOf(
LeviatanScans(), LeviatanScans(),
PsychoPlay(), PsychoPlay(),
OneShotScans(), OneShotScans(),
KaguyaDex(), KaguyaDex(),
KomiScans(), KomiScans(),
HunlightScans()) HunlightScans())
} }
class LeviatanScans : Genkan("Leviatan Scans", "https://leviatanscans.com", "en") class LeviatanScans : Genkan("Leviatan Scans", "https://leviatanscans.com", "en")