parent
fc03fb5abb
commit
0bb01b70b8
|
@ -1,11 +1,12 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
|
||||
ext {
|
||||
extName = 'Naver Comic'
|
||||
pkgNameSuffix = 'ko.navercomic'
|
||||
extClass = '.NaverComicFactory'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.extension.ko.navercomic
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
|
@ -14,70 +12,47 @@ import java.util.Locale
|
|||
class NaverWebtoon : NaverComicBase("webtoon") {
|
||||
override val name = "Naver Webtoon"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$mType/weekday.nhn")
|
||||
override fun popularMangaSelector() = ".list_area.daily_all .col ul > li"
|
||||
override fun popularMangaRequest(page: Int) = GET("$mobileUrl/$mType/weekday?sort=ALL_READER")
|
||||
override fun popularMangaSelector() = ".list_toon > [class='item ']"
|
||||
override fun popularMangaNextPageSelector() = null
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val thumb = element.select("div.thumb img").first()!!.attr("src")
|
||||
val title = element.select("a.title").first()!!
|
||||
val thumb = element.select("img").attr("src")
|
||||
val title = element.select("strong").text()
|
||||
val author = element.select("span.author").text().trim().split(" / ").joinToString()
|
||||
val url = element.select("a").attr("href")
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.url = title.attr("href").substringBefore("&week")
|
||||
manga.title = title.text().trim()
|
||||
manga.url = url
|
||||
manga.title = title
|
||||
manga.author = author
|
||||
manga.thumbnail_url = thumb
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$mType/weekday.nhn?order=Update")
|
||||
override fun latestUpdatesSelector() = ".list_area.daily_all .col.col_selected ul > li"
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$mobileUrl/$mType/weekday?sort=UPDATE")
|
||||
override fun latestUpdatesSelector() = ".list_toon > [class='item ']"
|
||||
override fun latestUpdatesNextPageSelector() = null
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
|
||||
override fun mangaDetailsParse(document: Document) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
}
|
||||
|
||||
class NaverBestChallenge : NaverComicChallengeBase("bestChallenge") {
|
||||
override val name = "Naver Webtoon Best Challenge"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn?m=main&order=StarScore")
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn?m=main&order=Update")
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=VIEW&page=$page")
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=UPDATE&page=$page")
|
||||
|
||||
override fun mangaDetailsParse(document: Document) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
}
|
||||
|
||||
class NaverChallenge : NaverComicChallengeBase("challenge") {
|
||||
override val name = "Naver Webtoon Challenge"
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn")
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn?m=list&order=Update")
|
||||
|
||||
// Chapter list is paginated, but there are no mobile pages to work with
|
||||
override fun chapterListRequest(manga: SManga) = GET("$baseUrl${manga.url}", headers)
|
||||
|
||||
override fun chapterListSelector() = "tbody tr:not([class])"
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
var document = response.asJsoup()
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||
while (document.select(paginationNextPageSelector).hasText()) {
|
||||
document.select(paginationNextPageSelector).let {
|
||||
document = client.newCall(GET(it.attr("abs:href"))).execute().asJsoup()
|
||||
document.select(chapterListSelector()).map { element -> chapters.add(chapterFromElement(element)) }
|
||||
}
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
override val paginationNextPageSelector = "div.paginate a.next"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
element.select("td + td a").let {
|
||||
val rawName = it.text()
|
||||
chapter.url = it.attr("href")
|
||||
chapter.chapter_number = parseChapterNumber(rawName)
|
||||
chapter.name = rawName
|
||||
chapter.date_upload = parseChapterDate(element.select("td.num").text().trim())
|
||||
}
|
||||
return chapter
|
||||
}
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=VIEW&page=$page")
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=UPDATE&page=$page")
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
|
@ -92,4 +67,7 @@ class NaverChallenge : NaverComicChallengeBase("challenge") {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document) =
|
||||
throw UnsupportedOperationException("Not used")
|
||||
}
|
||||
|
|
|
@ -1,18 +1,25 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.navercomic
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
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.ParsedHttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
@ -20,75 +27,66 @@ import java.util.Locale
|
|||
abstract class NaverComicBase(protected val mType: String) : ParsedHttpSource() {
|
||||
override val lang: String = "ko"
|
||||
override val baseUrl: String = "https://comic.naver.com"
|
||||
private val mobileUrl = "https://m.comic.naver.com"
|
||||
internal val mobileUrl = "https://m.comic.naver.com"
|
||||
override val supportsLatest = true
|
||||
override val client: OkHttpClient = network.client
|
||||
internal val json: Json by injectLazy()
|
||||
|
||||
private val mobileHeaders = super.headersBuilder()
|
||||
.add("Referer", mobileUrl)
|
||||
.build()
|
||||
|
||||
override fun searchMangaSelector() = ".resultList > li h5 > a"
|
||||
override fun searchMangaNextPageSelector() = ".paginate a.next"
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/search.nhn?m=$mType&keyword=$query&type=title&page=$page")
|
||||
override fun searchMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
manga.url = element.attr("href").substringBefore("&week").substringBefore("&listPage=")
|
||||
manga.title = element.text().trim()
|
||||
return manga
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/api/search/$mType?keyword=$query&page=$page")
|
||||
|
||||
override fun searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
|
||||
override fun searchMangaSelector() = throw UnsupportedOperationException("Not used")
|
||||
override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val result = json.decodeFromString<ApiMangaSearchResponse>(response.body.string())
|
||||
val mangas = result.searchList.map {
|
||||
SManga.create().apply {
|
||||
title = it.titleName
|
||||
description = it.synopsis
|
||||
thumbnail_url = it.thumbnailUrl
|
||||
url = "/$mType/list?titleId=${it.titleId}"
|
||||
}
|
||||
}
|
||||
|
||||
return MangasPage(mangas, result.pageInfo.nextPage != 0)
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div#ct > ul.section_episode_list li.item"
|
||||
|
||||
// Chapter list is paginated, use mobile version of site for speed and data savings
|
||||
override fun chapterListSelector() = throw UnsupportedOperationException("Not used")
|
||||
override fun chapterListRequest(manga: SManga) = chapterListRequest(manga.url, 1)
|
||||
|
||||
private fun chapterListRequest(mangaUrl: String, page: Int): Request {
|
||||
return GET("$mobileUrl$mangaUrl&page=$page", mobileHeaders)
|
||||
val titleId = Uri.parse("$baseUrl$mangaUrl").getQueryParameter("titleId")
|
||||
return GET("$baseUrl/api/article/list?titleId=$titleId&page=$page")
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
var document = response.asJsoup()
|
||||
var result = json.decodeFromString<ApiMangaChapterListResponse>(response.body.string())
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) }
|
||||
var nextPage = 2
|
||||
while (document.select(paginationNextPageSelector).isNotEmpty()) {
|
||||
document.select(paginationNextPageSelector).let {
|
||||
document = client.newCall(chapterListRequest(it.attr("href"), nextPage)).execute().asJsoup()
|
||||
document.select(chapterListSelector()).map { element -> chapters.add(chapterFromElement(element)) }
|
||||
nextPage++
|
||||
}
|
||||
chapters.addAll(result.articleList.map { createChapter(it, result.titleId) })
|
||||
|
||||
while (result.pageInfo.nextPage != 0) {
|
||||
result = json.decodeFromString(client.newCall(chapterListRequest("/$mType/list?titleId=${result.titleId}", result.pageInfo.nextPage)).execute().body.string())
|
||||
chapters.addAll(result.articleList.map { createChapter(it, result.titleId) })
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
open val paginationNextPageSelector = "a.btn_next:not(.disabled)"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val chapter = SChapter.create()
|
||||
val rawName = element.select("span.name").text()
|
||||
chapter.url = element.select("a").attr("href")
|
||||
chapter.chapter_number = parseChapterNumber(rawName)
|
||||
chapter.name = rawName
|
||||
chapter.date_upload = parseChapterDate(element.select("span.date").text().trim())
|
||||
|
||||
return chapter
|
||||
}
|
||||
|
||||
protected fun parseChapterNumber(name: String): Float {
|
||||
try {
|
||||
if (name.contains("[단편]")) return 1f
|
||||
// `특별` means `Special`, so It can be buggy. so pad `편`(Chapter) to prevent false return
|
||||
if (name.contains("번외") || name.contains("특별편")) return -2f
|
||||
val regex = Regex("([0-9]+)(?:[-.]([0-9]+))?(?:화)")
|
||||
val (ch_primal, ch_second) = regex.find(name)!!.destructured
|
||||
return (ch_primal + if (ch_second.isBlank()) "" else ".$ch_second").toFloatOrNull() ?: -1f
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return -1f
|
||||
private fun createChapter(chapter: MangaChapter, id: Int): SChapter {
|
||||
return SChapter.create().apply {
|
||||
url = "/$mType/detail?titleId=$id&no=${chapter.no}"
|
||||
name = chapter.subtitle
|
||||
chapter_number = chapter.no.toFloat()
|
||||
date_upload = parseChapterDate(chapter.serviceDateDescription)
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used")
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
return if (date.contains(":")) {
|
||||
|
@ -103,16 +101,24 @@ abstract class NaverComicBase(protected val mType: String) : ParsedHttpSource()
|
|||
}
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val element = document.select(".comicinfo")
|
||||
val titleElement = element.select(".detail > h2")
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
val titleId = Uri.parse(manga.url).getQueryParameter("titleId")
|
||||
return client.newCall(GET("$baseUrl/api/article/list/info?titleId=$titleId")).asObservableSuccess().map { mangaDetailsParse(it) }
|
||||
}
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.title = titleElement.first()!!.ownText().trim()
|
||||
manga.author = titleElement.select("span").text().trim()
|
||||
manga.description = document.select("div.detail p").text().trim()
|
||||
manga.thumbnail_url = element.select(".thumb > a > img").last()!!.attr("src")
|
||||
return manga
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
val manga = json.decodeFromString<Manga>(response.body.string())
|
||||
val authors = manga.author.run {
|
||||
setOf(writers, painters, originAuthors).flatten().map { it.name }
|
||||
}.joinToString()
|
||||
|
||||
return SManga.create().apply {
|
||||
title = manga.titleName
|
||||
author = authors
|
||||
description = manga.synopsis
|
||||
thumbnail_url = manga.thumbnailUrl
|
||||
status = if (manga.finished) SManga.COMPLETED else SManga.ONGOING
|
||||
}
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
|
@ -133,26 +139,104 @@ abstract class NaverComicBase(protected val mType: String) : ParsedHttpSource()
|
|||
}
|
||||
|
||||
// We are able to get the image URL directly from the page list
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("This method should not be called!")
|
||||
override fun imageUrlParse(document: Document) = throw UnsupportedOperationException("Not used")
|
||||
|
||||
override fun getFilterList() = FilterList()
|
||||
}
|
||||
|
||||
abstract class NaverComicChallengeBase(mType: String) : NaverComicBase(mType) {
|
||||
override fun popularMangaSelector() = ".weekchallengeBox tbody td:not([class])"
|
||||
override fun popularMangaNextPageSelector(): String? = ".paginate .page_wrap a.next"
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val thumb = element.select("a img").first()!!.attr("src")
|
||||
val title = element.select(".challengeTitle a").first()!!
|
||||
override fun popularMangaSelector() = throw UnsupportedOperationException("Not used")
|
||||
override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
|
||||
override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val apiMangaResponse = json.decodeFromString<ApiMangaChallengeResponse>(response.body.string())
|
||||
val mangas = apiMangaResponse.list.map {
|
||||
SManga.create().apply {
|
||||
title = it.titleName
|
||||
thumbnail_url = it.thumbnailUrl
|
||||
url = "/$mType/list?titleId=${it.titleId}"
|
||||
}
|
||||
}
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.url = title.attr("href").substringBefore("&week")
|
||||
manga.title = title.text().trim()
|
||||
manga.thumbnail_url = thumb
|
||||
return manga
|
||||
var pageInfo = apiMangaResponse.pageInfo
|
||||
|
||||
if (pageInfo == null) {
|
||||
val page = response.request.url.queryParameter("page")
|
||||
pageInfo = client.newCall(GET("$baseUrl/api/$mType/pageInfo?order=VIEW&page=$page")).execute().let { parsePageInfo(it) }
|
||||
}
|
||||
|
||||
return MangasPage(mangas, pageInfo?.nextPage != 0)
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
|
||||
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
|
||||
override fun latestUpdatesParse(response: Response) = popularMangaParse(response)
|
||||
|
||||
private fun parsePageInfo(response: Response): PageInfo? {
|
||||
return json.decodeFromString<ApiMangaChallengeResponse>(response.body.string()).pageInfo
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class ApiMangaSearchResponse(
|
||||
val pageInfo: PageInfo,
|
||||
val searchList: List<Manga>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiMangaChallengeResponse(
|
||||
val pageInfo: PageInfo?,
|
||||
val list: List<MangaChallenge>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiMangaChapterListResponse(
|
||||
val pageInfo: PageInfo,
|
||||
val titleId: Int,
|
||||
val articleList: List<MangaChapter>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PageInfo(
|
||||
val nextPage: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaChapter(
|
||||
val serviceDateDescription: String,
|
||||
val subtitle: String,
|
||||
val no: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Manga(
|
||||
val thumbnailUrl: String,
|
||||
val titleName: String,
|
||||
val titleId: Int,
|
||||
val finished: Boolean,
|
||||
val author: AuthorList,
|
||||
val synopsis: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaChallenge(
|
||||
val thumbnailUrl: String,
|
||||
val titleName: String,
|
||||
val titleId: Int,
|
||||
val finish: Boolean,
|
||||
val author: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorList(
|
||||
val writers: List<Author>,
|
||||
val painters: List<Author>,
|
||||
val originAuthors: List<Author>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Author(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue