diff --git a/src/zh/comico/build.gradle b/src/zh/comico/build.gradle new file mode 100644 index 000000000..e4fc99dd6 --- /dev/null +++ b/src/zh/comico/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' + +ext { + appName = 'Tachiyomi: Comico' + pkgNameSuffix = 'zh.comico' + extClass = '.ComicoFactory' + extVersionCode = 1 + libVersion = '1.2' +} + +dependencies { + compileOnly 'com.google.code.gson:gson:2.8.2' + compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0' +} + +apply from: "$rootDir/common.gradle" diff --git a/src/zh/comico/res/mipmap-hdpi/ic_launcher.png b/src/zh/comico/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..032fb371e Binary files /dev/null and b/src/zh/comico/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/zh/comico/res/mipmap-mdpi/ic_launcher.png b/src/zh/comico/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..3056e61ae Binary files /dev/null and b/src/zh/comico/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/zh/comico/res/mipmap-xhdpi/ic_launcher.png b/src/zh/comico/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..51dfe452b Binary files /dev/null and b/src/zh/comico/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/zh/comico/res/mipmap-xxhdpi/ic_launcher.png b/src/zh/comico/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..2891cf9a0 Binary files /dev/null and b/src/zh/comico/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/zh/comico/res/mipmap-xxxhdpi/ic_launcher.png b/src/zh/comico/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..eeccd9050 Binary files /dev/null and b/src/zh/comico/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/zh/comico/res/web_hi_res_512.png b/src/zh/comico/res/web_hi_res_512.png new file mode 100644 index 000000000..09b0f149e Binary files /dev/null and b/src/zh/comico/res/web_hi_res_512.png differ diff --git a/src/zh/comico/src/eu/kanade/tachiyomi/extension/zh/comico/Comico.kt b/src/zh/comico/src/eu/kanade/tachiyomi/extension/zh/comico/Comico.kt new file mode 100644 index 000000000..39f50a171 --- /dev/null +++ b/src/zh/comico/src/eu/kanade/tachiyomi/extension/zh/comico/Comico.kt @@ -0,0 +1,167 @@ +package eu.kanade.tachiyomi.extension.zh.comico + +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.get +import com.google.gson.Gson +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.model.* +import eu.kanade.tachiyomi.source.online.ParsedHttpSource +import okhttp3.OkHttpClient +import okhttp3.Request +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element +import okhttp3.FormBody +import okhttp3.Response +import java.text.SimpleDateFormat +import java.util.* + +abstract class Comico ( + override val name: String, + open val urlModifier: String, + override val supportsLatest: Boolean +) : ParsedHttpSource() { + + override val baseUrl = "http://www.comico.com.tw" + + override val lang = "zh" + + override val client: OkHttpClient = network.cloudflareClient + + val gson = Gson() + + // Popular + + override fun popularMangaRequest(page: Int): Request { + return GET("$baseUrl$urlModifier/official/finish/?order=ALLSALES", headers) + } + + override fun popularMangaSelector() = "ul.list-article02__list li.list-article02__item a" + + override fun popularMangaFromElement(element: Element): SManga { + val manga = SManga.create() + + manga.url = element.attr("href").substringAfter(baseUrl + urlModifier) + manga.title = element.attr("title") + manga.thumbnail_url = element.select("img").first().attr("abs:src") + + return manga + } + + override fun popularMangaNextPageSelector() = "No next page" + + // Latest + + override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesSelector() = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesFromElement(element: Element): SManga = throw UnsupportedOperationException("Not used") + + override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("Not used") + + // Search + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + return GET("$baseUrl/search/index.nhn?searchWord=$query", headers) + } + + override fun searchMangaSelector() = "div#officialList ul.list-article02__list li.list-article02__item a" + + override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element) + + override fun searchMangaNextPageSelector() = popularMangaNextPageSelector() + + // Manga details + + override fun mangaDetailsRequest(manga: SManga): Request { + return GET(baseUrl + urlModifier + manga.url, headers) + } + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.article-hero05") + + val manga = SManga.create() + manga.title = infoElement.select("h1").text() + manga.author = infoElement.select("p.article-hero05__author").text() + manga.description = infoElement.select("p.article-hero05__sub-description").text() + manga.genre = infoElement.select("div.article-hero05__meta a").text() + manga.status = parseStatus(infoElement.select("div.article-hero05__meta p:not(:has(a))").first().text()) + manga.thumbnail_url = infoElement.select("img").attr("src") + + return manga + } + + private fun parseStatus(status: String?) = when { + status == null -> SManga.UNKNOWN + status.contains("每") -> SManga.ONGOING + status.contains("完結作品") -> SManga.COMPLETED + else -> SManga.UNKNOWN + } + + // Chapters + + override fun chapterListSelector() = throw UnsupportedOperationException("Not used") + + override fun chapterFromElement(element: Element): SChapter = throw UnsupportedOperationException("Not used") + + override fun chapterListRequest(manga: SManga): Request { + val chapterListHeaders = headersBuilder() + .add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") + .build() + + val body = FormBody.Builder() + .add("titleNo", manga.url.replace("/", "")) + .build() + + return POST("$baseUrl/api/getArticleList.nhn", chapterListHeaders, body) + } + + fun chapterFromJson(jsonElement: JsonElement): SChapter { + val chapter = SChapter.create() + + chapter.name = jsonElement["subtitle"].asString + chapter.setUrlWithoutDomain(jsonElement["articleDetailUrl"].asString) + chapter.date_upload = parseDate(jsonElement["date"].asString) + + return chapter + } + + override fun chapterListParse(response: Response): List { + val chapters = mutableListOf() + + gson.fromJson(response.body()!!.string())["result"]["list"].asJsonArray.forEach{ + if (it["freeFlg"].asString == "Y") chapters.add(chapterFromJson(it)) + } + + return chapters.reversed() + } + + private fun parseDate(date: String): Long { + return SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()).parse(date).time + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + + // First image is in the body + document.select("div.comic-image img") + .map{ pages.add(Page(pages.size, "", it.attr("abs:src"))) } + // If there are more images, they're in a script + document.select("script:containsData(imageData)").first().data().let { + if (it.isNotEmpty()) { + it.substringAfter("imageData:[").substringBefore("]").trim().split(",") + .forEach { img -> pages.add(Page(pages.size, "", img.replace("\'", ""))) } + } + } + + return pages + } + + override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") + + override fun getFilterList() = FilterList() + +} diff --git a/src/zh/comico/src/eu/kanade/tachiyomi/extension/zh/comico/ComicoFactory.kt b/src/zh/comico/src/eu/kanade/tachiyomi/extension/zh/comico/ComicoFactory.kt new file mode 100644 index 000000000..f442c1619 --- /dev/null +++ b/src/zh/comico/src/eu/kanade/tachiyomi/extension/zh/comico/ComicoFactory.kt @@ -0,0 +1,103 @@ +package eu.kanade.tachiyomi.extension.zh.comico + +import com.github.salomonbrys.kotson.fromJson +import com.github.salomonbrys.kotson.get +import com.google.gson.JsonObject +import eu.kanade.tachiyomi.network.POST +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.SourceFactory +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.FormBody +import okhttp3.Request +import okhttp3.Response +import org.jsoup.nodes.Document + +class ComicoFactory : SourceFactory { + override fun createSources(): List = listOf( + ComicoOfficial(), + ComicoChallenge() + ) +} + +class ComicoOfficial : Comico("Comico Official (Limited free chapters)", "", false) +class ComicoChallenge : Comico("Comico Challenge", "/challenge", true) { + override fun popularMangaRequest(page: Int): Request { + val body = FormBody.Builder() + .add("page", page.toString()) + .build() + + return POST("$baseUrl$urlModifier/updateList.nhn?order=new", headers, body) + } + + override fun popularMangaParse(response: Response): MangasPage { + val mangas = mutableListOf() + val body = response.body()!!.string() + + gson.fromJson(body)["result"]["list"].asJsonArray.forEach{ + val manga = SManga.create() + + manga.thumbnail_url = it["img_url"].asString + manga.title = it["article_title"].asString + manga.author = it["author"].asString + manga.description = it["description"].asString + manga.url = it["article_url"].asString.substringAfter(urlModifier) + manga.status = if (it["is_end"].asString == "false") SManga.ONGOING else SManga.COMPLETED + + mangas.add(manga) + } + + val lastPage = gson.fromJson(body)["result"]["totalPageCnt"].asString + val currentPage = gson.fromJson(body)["result"]["currentPageNo"].asString + + return MangasPage(mangas, currentPage < lastPage) + } + + override fun latestUpdatesRequest(page: Int): Request { + val body = FormBody.Builder() + .add("page", page.toString()) + .build() + + return POST("$baseUrl$urlModifier/updateList.nhn?order=update", headers, body) + } + + override fun latestUpdatesParse(response: Response) = popularMangaParse(response) + + override fun searchMangaSelector() = "div#challengeList ul.list-article02__list li.list-article02__item a" + + override fun mangaDetailsParse(document: Document): SManga { + val infoElement = document.select("div.article-hero03__inner") + + val manga = SManga.create() + manga.title = infoElement.select("h1").text() + manga.author = infoElement.select("p.article-hero03__author").text() + manga.description = infoElement.select("div.article-hero03__description p").text() + manga.thumbnail_url = infoElement.select("img").attr("src") + + return manga + } + + override fun chapterListParse(response: Response): List { + val chapters = mutableListOf() + + gson.fromJson(response.body()!!.string())["result"]["list"].asJsonArray + .forEach{ chapters.add(chapterFromJson(it)) } + + return chapters.reversed() + } + + override fun pageListParse(document: Document): List { + val pages = mutableListOf() + + document.select("img.comic-image__image").forEachIndexed{ i, img -> + pages.add(Page(i, "", img.attr("src"))) + } + + return pages + } +} + + +