From 3691fb5ee3a9ab40bfdc93311861577b8b9e65b3 Mon Sep 17 00:00:00 2001 From: TonyToscana Date: Sat, 3 Jun 2017 19:04:57 +0200 Subject: [PATCH] Add webtoons.com and Mangazuki.co (#48) (#44) * Add webtoons.com * Add suggested changes by null-dev * Add Mangazuki.co * Edit to fit suggested changes * Remove Webtoons source ID assignment * Change chapterListParse Change chapterListParse so it doesn't request the first page twice. --- src/en/mangazuki/build.gradle | 13 ++ .../extension/en/mangazuki/Mangazuki.kt | 167 ++++++++++++++++++ src/en/webtoons/build.gradle | 13 ++ .../extension/en/webtoons/Webtoons.kt | 148 ++++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 src/en/mangazuki/build.gradle create mode 100644 src/en/mangazuki/src/eu/kanade/tachiyomi/extension/en/mangazuki/Mangazuki.kt create mode 100644 src/en/webtoons/build.gradle create mode 100644 src/en/webtoons/src/eu/kanade/tachiyomi/extension/en/webtoons/Webtoons.kt diff --git a/src/en/mangazuki/build.gradle b/src/en/mangazuki/build.gradle new file mode 100644 index 000000000..79d5bf35a --- /dev/null +++ b/src/en/mangazuki/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Mangazuki' + pkgNameSuffix = "en.mangazuki" + extClass = '.Mangazuki' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/mangazuki/src/eu/kanade/tachiyomi/extension/en/mangazuki/Mangazuki.kt b/src/en/mangazuki/src/eu/kanade/tachiyomi/extension/en/mangazuki/Mangazuki.kt new file mode 100644 index 000000000..d2e25c9b9 --- /dev/null +++ b/src/en/mangazuki/src/eu/kanade/tachiyomi/extension/en/mangazuki/Mangazuki.kt @@ -0,0 +1,167 @@ +package eu.kanade.tachiyomi.extension.en.mangazuki +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.HttpUrl +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.* +import java.util.regex.Pattern + +class Mangazuki : ParsedHttpSource() { + + override val name = "Mangazuki" + + override val baseUrl = "https://mangazuki.co" + + override val lang = "en" + + override val supportsLatest = true + + private val datePattern = Pattern.compile("(\\d+)\\s([a-z]*?)s?\\s") + + private val dateFormat = SimpleDateFormat("MMM d yyyy", Locale.ENGLISH) + + private val dateFields = mutableMapOf( + Pair("second", Calendar.SECOND), + Pair("minute", Calendar.MINUTE), + Pair("hour", Calendar.HOUR), + Pair("day", Calendar.DATE), + Pair("week", Calendar.WEEK_OF_YEAR), + Pair("month", Calendar.MONTH), + Pair("year", Calendar.YEAR) + ) + + override fun popularMangaSelector() = "div.caption > h6" + + override fun latestUpdatesSelector() = "div.caption > h6" + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl/series?page=$page", headers) + } + + override fun latestUpdatesRequest(page: Int): Request { + return GET("$baseUrl/latest?page=$page", headers) + } + + private fun mangaFromElement(query: String, element: Element): SManga { + val manga = SManga.create() + element.select(query).first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.text() + } + return manga + } + + override fun popularMangaFromElement(element: Element): SManga { + return mangaFromElement("a", element) + } + + override fun latestUpdatesFromElement(element: Element): SManga { + return popularMangaFromElement(element) + } + + override fun popularMangaNextPageSelector() = "ul.pagination > li.next > a[rel=next]" + + override fun latestUpdatesNextPageSelector() = "ul.pagination > li.next > a[rel=next]" + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/series?q=$query&page=$page").newBuilder() + + return GET(url.toString(), headers) + } + override fun searchMangaSelector() = popularMangaSelector() + + override fun searchMangaFromElement(element: Element): SManga { + return mangaFromElement("a", element) + } + + override fun searchMangaNextPageSelector() = "ul.pagination > li.next > a[rel=next]" + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("#activity > div > div.panel > div.panel-body") + + val manga = SManga.create() + manga.author = "Unknown" + manga.artist = "Unknown" + manga.genre = "Unknown" + manga.description = infoElement.select("p").text() + manga.status = 0 + manga.thumbnail_url = baseUrl + infoElement.select("div.media > div.media-left img").attr("src") + return manga + } + + override fun chapterListSelector() = "div#activity > div > div > ul > li" + + override fun chapterFromElement(element: Element): SChapter { + val urlElement = element.select("a.media-link") + + val chapter = SChapter.create() + chapter.setUrlWithoutDomain(urlElement.attr("href")) + chapter.name = element.select("div.media-body > h6.media-heading").text() + chapter.date_upload = element.select("div.media-body > span").first().let { + parseDateFromElement(it) + } + return chapter + } + + private fun parseDateFromElement(dateElement: Element): Long { + val dateAsString = dateElement.text().filterNot { it == ','} + + var date: Date + try { + date = dateFormat.parse(dateAsString.substringAfter("Added on ")) + } catch (e: ParseException) { + val m = datePattern.matcher(dateAsString) + if (m.find()) { + val cal = Calendar.getInstance() + do { + val amount = m.group(1).toInt() + val unit = m.group(2) + + cal.add(dateFields[unit]!!, -amount) + } while (m.find()) + date = cal.time; + } else { + return 0 + } + } + + return date.time + } + + private fun chapterNextPageSelector() = "ul.pagination > li.next > a[rel=next]" + + override fun chapterListParse(response: Response): List { + val allChapters = mutableListOf() + var page = 1 + val urlBase = response.request().url().toString() + var document = response.asJsoup() + + while (true) { + val pageChapters = document.select(chapterListSelector()).map { chapterFromElement(it) } + if (pageChapters.isEmpty()) + break + + allChapters += pageChapters + + val hasNextPage = document.select(chapterNextPageSelector()).isNotEmpty() + if (!hasNextPage) + break + + val nextUrl = urlBase + "?page=${++page}" + document = client.newCall(GET(nextUrl, headers)).execute().asJsoup() + } + + return allChapters + } + + override fun pageListParse(document: Document) = document.select("div.row > img").mapIndexed { i, element -> Page(i, "", element.attr("src")) } + + override fun imageUrlParse(document: Document) = "" +} \ No newline at end of file diff --git a/src/en/webtoons/build.gradle b/src/en/webtoons/build.gradle new file mode 100644 index 000000000..78236f2ab --- /dev/null +++ b/src/en/webtoons/build.gradle @@ -0,0 +1,13 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Webtoons' + pkgNameSuffix = "en.webtoons" + extClass = '.Webtoons' + extVersionCode = 1 + extVersionSuffix = 1 + libVersion = '1.0' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/en/webtoons/src/eu/kanade/tachiyomi/extension/en/webtoons/Webtoons.kt b/src/en/webtoons/src/eu/kanade/tachiyomi/extension/en/webtoons/Webtoons.kt new file mode 100644 index 000000000..4e28269f5 --- /dev/null +++ b/src/en/webtoons/src/eu/kanade/tachiyomi/extension/en/webtoons/Webtoons.kt @@ -0,0 +1,148 @@ +package eu.kanade.tachiyomi.extension.en.webtoons + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.HttpUrl +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import java.text.SimpleDateFormat +import java.util.* + +/** + * Todo Cover -> crop right //possible? + */ +class Webtoons : ParsedHttpSource() { + + override val name = "Webtoons.com" + + override val baseUrl = "http://www.webtoons.com" + + override val lang = "en" + + override val supportsLatest = true + + 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() = "div.left_area > ul.lst_type1 > li" + + override fun latestUpdatesSelector() = "div#dailyList > $day li > a:has(span:contains(UP))" + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "http://www.webtoons.com/en/") + + private val mobileHeaders = super.headersBuilder() + .add("Referer", "http://m.webtoons.com") + .build() + + override fun popularMangaRequest(page: Int) = GET("$baseUrl/en/top", headers) + + override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/en/dailySchedule?sortOrder=UPDATE&webtoonCompleteType=ONGOING", headers) + + private fun mangaFromElement(query: String, element: Element): SManga { + val manga = SManga.create() + element.select(query).first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.select("p.subj").text() + } + return manga + } + + override fun popularMangaFromElement(element: Element) = mangaFromElement("a", element) + + override fun latestUpdatesFromElement(element: Element): SManga { + val manga = SManga.create() + element.let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.select("p.subj").text() + } + return manga + } + + override fun popularMangaNextPageSelector() = null + + override fun latestUpdatesNextPageSelector() = null + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val url = HttpUrl.parse("$baseUrl/search?keyword=$query").newBuilder() + + (if (filters.isEmpty()) getFilterList() else filters).forEach { filter -> + when (filter) { + is Type -> url.addQueryParameter("searchType", arrayOf("WEBTOON", "CHALLENGE")[filter.state]) + } + } + + url.addQueryParameter("page", page.toString()) + return GET(url.toString(), headers) + } + + override fun searchMangaSelector() = "#content > div.card_wrap.search li" + + override fun searchMangaFromElement(element: Element): SManga { + val manga = SManga.create() + element.select("a").first().let { + manga.setUrlWithoutDomain(it.attr("href")) + manga.title = it.select("p.subj").text() + } + return manga + } + + override fun searchMangaNextPageSelector() = "div.paginate > a[href=#] + a" + + 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 picElement = document.select("#content > div.cont_box > div.detail_body") + val discoverPic = document.select("#content > div.cont_box > div.detail_header > span.thmb") + + val manga = SManga.create() + manga.author = detailElement.select(".author:nth-of-type(1)").text().substringBefore("author info") + manga.artist = detailElement.select(".author:nth-of-type(2)").first()?.text()?.substringBefore("author info") ?: manga.author + manga.genre = detailElement.select(".genre").text() + manga.description = infoElement.select("p.summary").text() + manga.status = infoElement.select("p.day_info").text().orEmpty().let { parseStatus(it) } + manga.thumbnail_url = discoverPic.select("img").not("[alt=Representative image").first()?.attr("src") ?: picElement.attr("style")?.substringAfter("url(")?.substringBeforeLast(")") + return manga + } + + private fun parseStatus(status: String) = when { + status.contains("UP") -> SManga.ONGOING + status.contains("COMPLETED") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + override fun chapterListSelector() = "ul#_episodeList > li[id*=episode]" + + 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() + chapter.date_upload = element.select("a > div.row > div.info > p.date").text()?.let { SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH).parse(it).time } ?: 0 + return chapter + } + + override fun chapterListRequest(manga: SManga) = GET("http://m.webtoons.com" + manga.url, mobileHeaders) + + override fun pageListParse(document: Document) = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) } + + override fun imageUrlParse(document: Document) = document.select("img").first().attr("src") + + private class Type : Filter.Select("Type", arrayOf("Webtoon (default)", "Discover")) + + override fun getFilterList() = FilterList(Type()) +} \ No newline at end of file