diff --git a/src/all/genkan/build.gradle b/src/all/genkan/build.gradle new file mode 100644 index 000000000..c4cff2947 --- /dev/null +++ b/src/all/genkan/build.gradle @@ -0,0 +1,12 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Genkan (multiple sources)' + pkgNameSuffix = 'all.genkan' + extClass = '.GenkanFactory' + extVersionCode = 1 + libVersion = '1.2' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/all/genkan/res/mipmap-hdpi/ic_launcher.png b/src/all/genkan/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..ca5915a22 Binary files /dev/null and b/src/all/genkan/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/all/genkan/res/mipmap-mdpi/ic_launcher.png b/src/all/genkan/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..0af540701 Binary files /dev/null and b/src/all/genkan/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/all/genkan/res/mipmap-xhdpi/ic_launcher.png b/src/all/genkan/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..8b7687bbf Binary files /dev/null and b/src/all/genkan/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/all/genkan/res/mipmap-xxhdpi/ic_launcher.png b/src/all/genkan/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..1ccf5e340 Binary files /dev/null and b/src/all/genkan/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/all/genkan/res/mipmap-xxxhdpi/ic_launcher.png b/src/all/genkan/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..fdb5c4de0 Binary files /dev/null and b/src/all/genkan/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/all/genkan/res/web_hi_res_512.png b/src/all/genkan/res/web_hi_res_512.png new file mode 100644 index 000000000..a89333b39 Binary files /dev/null and b/src/all/genkan/res/web_hi_res_512.png differ diff --git a/src/all/genkan/src/eu/kanade/tachiyomi/extension/all/genkan/Genkan.kt b/src/all/genkan/src/eu/kanade/tachiyomi/extension/all/genkan/Genkan.kt new file mode 100644 index 000000000..939fce2c8 --- /dev/null +++ b/src/all/genkan/src/eu/kanade/tachiyomi/extension/all/genkan/Genkan.kt @@ -0,0 +1,211 @@ +package eu.kanade.tachiyomi.extension.all.genkan + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +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 org.jsoup.select.Elements +import java.text.SimpleDateFormat +import java.util.* + +abstract 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" + + private val popularMangaUrl = "$baseUrl/comics?page=" // Search is also based off this val + override fun popularMangaRequest(page: Int): Request { + return GET("$popularMangaUrl$page") + } + + override fun latestUpdatesSelector() = popularMangaSelector() + + // Track manga in latest updates page + private val latestUpdatesTitles = mutableSetOf() + + override fun latestUpdatesRequest(page: Int): Request { + if (page == 1) latestUpdatesTitles.clear() + return GET("$baseUrl/latest?page=$page") + } + + // To prevent dupes + override fun latestUpdatesParse(response: Response): MangasPage { + val latestManga = mutableListOf() + 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 = element.select("a.media-content").first().attr("style").substringAfter("(").substringBefore(")") + return manga + } + + override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element) + + override fun popularMangaNextPageSelector() = "[rel=next]" + + override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() + + // Sources' websites don't appear to have a search function; so searching locally + 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.toLowerCase() + return GET("$popularMangaUrl$page") + } + + override fun searchMangaParse(response: Response): MangasPage { + val searchMatches = mutableListOf() + 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 { + val searchMatches = mutableListOf() + document.select(searchMangaSelector()).forEach { + if (it.text().toLowerCase().contains(searchQuery)) { + searchMatches.add(searchMangaFromElement(it)) + } + } + return searchMatches + } + + // search additional pages if called + private fun searchMorePages(): MutableList { + searchPage++ + val nextPage = client.newCall(GET("$popularMangaUrl$searchPage", headers)).execute().asJsoup() + val searchMatches = mutableListOf() + 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() + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div#content").first() + + val manga = SManga.create() + manga.title = infoElement.select("h5").first().text() + + manga.description = document.select("div.col-lg-9").text().substringAfter("Description ").substringBefore(" Volume") + manga.thumbnail_url = document.select("div.media a").first().attr("style").substringAfter("(").substringBefore(")") + return manga + } + + override fun chapterListSelector() = "div.col-lg-9 div.flex" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a.item-author") + + val chapNum = urlElement.attr("href").split("/").last() + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + if (urlElement.text().contains("Chapter $chapNum")) { + chapter.name = urlElement.text() + } else { + chapter.name = "Ch. " + chapNum + ": " + urlElement.text() + } + chapter.date_upload = parseChapterDate(element.select("a.item-company").first().text()) ?: 0 + return chapter + } + + 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? { + if ("ago" in string) { + return parseRelativeDate(string) ?: 0 + } else { + return dateFormat.parse(string).time + } + } + + // 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]){ + "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.MONTH, -trimmedDate[0].toInt())} + "second" -> calendar.apply{add(Calendar.SECOND, 0)} + } + + return calendar.timeInMillis + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + + val allImages = document.select("div#pages-container + script").first().data() + .substringAfter("[").substringBefore("];") + .replace(Regex("""["\\]"""), "") + .split(",") + + for (i in 0 until allImages.size) { + pages.add(Page(i, "", allImages[i])) + } + + return pages + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") + + override fun getFilterList() = FilterList() + +} diff --git a/src/all/genkan/src/eu/kanade/tachiyomi/extension/all/genkan/GenkanFactory.kt b/src/all/genkan/src/eu/kanade/tachiyomi/extension/all/genkan/GenkanFactory.kt new file mode 100644 index 000000000..214ab211b --- /dev/null +++ b/src/all/genkan/src/eu/kanade/tachiyomi/extension/all/genkan/GenkanFactory.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.extension.all.genkan + +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory + +class GenkanFactory : SourceFactory { + override fun createSources(): List = listOf( + LeviatanScans(), + PsychoPlay(), + OneShotScans(), + KaguyaDex(), + KomiScans(), + HunlightScans()) +} + +class LeviatanScans : Genkan("Leviatan Scans", "https://leviatanscans.com", "en") +class PsychoPlay : Genkan("Psycho Play", "https://psychoplay.co", "en") +class OneShotScans : Genkan("One Shot Scans", "https://oneshotscans.com", "en") +class KaguyaDex : Genkan("KaguyaDex", " https://kaguyadex.com", "en") +class KomiScans : Genkan("Komi Scans", " https://komiscans.com", "en") +class HunlightScans : Genkan("Hunlight Scans", "https://hunlight-scans.info", "en")