Fix Naver Comic (#16049)

* Fix Naver Comic

* remove useless types
This commit is contained in:
Slowlife 2023-04-15 21:41:47 +07:00 committed by GitHub
parent fc03fb5abb
commit 0bb01b70b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 179 additions and 116 deletions

View File

@ -1,11 +1,12 @@
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext { ext {
extName = 'Naver Comic' extName = 'Naver Comic'
pkgNameSuffix = 'ko.navercomic' pkgNameSuffix = 'ko.navercomic'
extClass = '.NaverComicFactory' extClass = '.NaverComicFactory'
extVersionCode = 2 extVersionCode = 3
} }
apply from: "$rootDir/common.gradle" apply from: "$rootDir/common.gradle"

View File

@ -2,10 +2,8 @@ package eu.kanade.tachiyomi.extension.ko.navercomic
import android.annotation.SuppressLint import android.annotation.SuppressLint
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup import org.jsoup.nodes.Document
import okhttp3.Response
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
@ -14,70 +12,47 @@ import java.util.Locale
class NaverWebtoon : NaverComicBase("webtoon") { class NaverWebtoon : NaverComicBase("webtoon") {
override val name = "Naver Webtoon" override val name = "Naver Webtoon"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$mType/weekday.nhn") override fun popularMangaRequest(page: Int) = GET("$mobileUrl/$mType/weekday?sort=ALL_READER")
override fun popularMangaSelector() = ".list_area.daily_all .col ul > li" override fun popularMangaSelector() = ".list_toon > [class='item ']"
override fun popularMangaNextPageSelector() = null override fun popularMangaNextPageSelector() = null
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element): SManga {
val thumb = element.select("div.thumb img").first()!!.attr("src") val thumb = element.select("img").attr("src")
val title = element.select("a.title").first()!! 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() val manga = SManga.create()
manga.url = title.attr("href").substringBefore("&week") manga.url = url
manga.title = title.text().trim() manga.title = title
manga.author = author
manga.thumbnail_url = thumb manga.thumbnail_url = thumb
return manga return manga
} }
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/$mType/weekday.nhn?order=Update") override fun latestUpdatesRequest(page: Int) = GET("$mobileUrl/$mType/weekday?sort=UPDATE")
override fun latestUpdatesSelector() = ".list_area.daily_all .col.col_selected ul > li" override fun latestUpdatesSelector() = ".list_toon > [class='item ']"
override fun latestUpdatesNextPageSelector() = null override fun latestUpdatesNextPageSelector() = null
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun mangaDetailsParse(document: Document) =
throw UnsupportedOperationException("Not used")
} }
class NaverBestChallenge : NaverComicChallengeBase("bestChallenge") { class NaverBestChallenge : NaverComicChallengeBase("bestChallenge") {
override val name = "Naver Webtoon Best Challenge" override val name = "Naver Webtoon Best Challenge"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn?m=main&order=StarScore") override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=VIEW&page=$page")
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn?m=main&order=Update") 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") { class NaverChallenge : NaverComicChallengeBase("challenge") {
override val name = "Naver Webtoon Challenge" override val name = "Naver Webtoon Challenge"
override fun popularMangaRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn") override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=VIEW&page=$page")
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/genre/$mType.nhn?m=list&order=Update") override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/api/$mType/list?order=UPDATE&page=$page")
// 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
}
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
private fun parseChapterDate(date: String): Long { private fun parseChapterDate(date: String): Long {
@ -92,4 +67,7 @@ class NaverChallenge : NaverComicChallengeBase("challenge") {
} }
} }
} }
override fun mangaDetailsParse(document: Document) =
throw UnsupportedOperationException("Not used")
} }

View File

@ -1,18 +1,25 @@
package eu.kanade.tachiyomi.extension.ko.navercomic package eu.kanade.tachiyomi.extension.ko.navercomic
import android.annotation.SuppressLint import android.annotation.SuppressLint
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.source.model.FilterList 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.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource 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.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Document import org.jsoup.nodes.Document
import org.jsoup.nodes.Element import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -20,75 +27,66 @@ import java.util.Locale
abstract class NaverComicBase(protected val mType: String) : ParsedHttpSource() { abstract class NaverComicBase(protected val mType: String) : ParsedHttpSource() {
override val lang: String = "ko" override val lang: String = "ko"
override val baseUrl: String = "https://comic.naver.com" 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 supportsLatest = true
override val client: OkHttpClient = network.client override val client: OkHttpClient = network.client
internal val json: Json by injectLazy()
private val mobileHeaders = super.headersBuilder() private val mobileHeaders = super.headersBuilder()
.add("Referer", mobileUrl) .add("Referer", mobileUrl)
.build() .build()
override fun searchMangaSelector() = ".resultList > li h5 > a" override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/api/search/$mType?keyword=$query&page=$page")
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 searchMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaSelector() = throw UnsupportedOperationException("Not used")
val manga = SManga.create() override fun searchMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
manga.url = element.attr("href").substringBefore("&week").substringBefore("&listPage=")
manga.title = element.text().trim() override fun searchMangaParse(response: Response): MangasPage {
return manga 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" override fun chapterListSelector() = throw UnsupportedOperationException("Not used")
// Chapter list is paginated, use mobile version of site for speed and data savings
override fun chapterListRequest(manga: SManga) = chapterListRequest(manga.url, 1) override fun chapterListRequest(manga: SManga) = chapterListRequest(manga.url, 1)
private fun chapterListRequest(mangaUrl: String, page: Int): Request { 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> { override fun chapterListParse(response: Response): List<SChapter> {
var document = response.asJsoup() var result = json.decodeFromString<ApiMangaChapterListResponse>(response.body.string())
val chapters = mutableListOf<SChapter>() val chapters = mutableListOf<SChapter>()
document.select(chapterListSelector()).map { chapters.add(chapterFromElement(it)) } chapters.addAll(result.articleList.map { createChapter(it, result.titleId) })
var nextPage = 2
while (document.select(paginationNextPageSelector).isNotEmpty()) { while (result.pageInfo.nextPage != 0) {
document.select(paginationNextPageSelector).let { result = json.decodeFromString(client.newCall(chapterListRequest("/$mType/list?titleId=${result.titleId}", result.pageInfo.nextPage)).execute().body.string())
document = client.newCall(chapterListRequest(it.attr("href"), nextPage)).execute().asJsoup() chapters.addAll(result.articleList.map { createChapter(it, result.titleId) })
document.select(chapterListSelector()).map { element -> chapters.add(chapterFromElement(element)) }
nextPage++
}
} }
return chapters return chapters
} }
open val paginationNextPageSelector = "a.btn_next:not(.disabled)" private fun createChapter(chapter: MangaChapter, id: Int): SChapter {
return SChapter.create().apply {
override fun chapterFromElement(element: Element): SChapter { url = "/$mType/detail?titleId=$id&no=${chapter.no}"
val chapter = SChapter.create() name = chapter.subtitle
val rawName = element.select("span.name").text() chapter_number = chapter.no.toFloat()
chapter.url = element.select("a").attr("href") date_upload = parseChapterDate(chapter.serviceDateDescription)
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
} }
} }
override fun chapterFromElement(element: Element) = throw UnsupportedOperationException("Not used")
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
private fun parseChapterDate(date: String): Long { private fun parseChapterDate(date: String): Long {
return if (date.contains(":")) { return if (date.contains(":")) {
@ -103,16 +101,24 @@ abstract class NaverComicBase(protected val mType: String) : ParsedHttpSource()
} }
} }
override fun mangaDetailsParse(document: Document): SManga { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
val element = document.select(".comicinfo") val titleId = Uri.parse(manga.url).getQueryParameter("titleId")
val titleElement = element.select(".detail > h2") return client.newCall(GET("$baseUrl/api/article/list/info?titleId=$titleId")).asObservableSuccess().map { mangaDetailsParse(it) }
}
val manga = SManga.create() override fun mangaDetailsParse(response: Response): SManga {
manga.title = titleElement.first()!!.ownText().trim() val manga = json.decodeFromString<Manga>(response.body.string())
manga.author = titleElement.select("span").text().trim() val authors = manga.author.run {
manga.description = document.select("div.detail p").text().trim() setOf(writers, painters, originAuthors).flatten().map { it.name }
manga.thumbnail_url = element.select(".thumb > a > img").last()!!.attr("src") }.joinToString()
return manga
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> { 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 // 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() override fun getFilterList() = FilterList()
} }
abstract class NaverComicChallengeBase(mType: String) : NaverComicBase(mType) { abstract class NaverComicChallengeBase(mType: String) : NaverComicBase(mType) {
override fun popularMangaSelector() = ".weekchallengeBox tbody td:not([class])" override fun popularMangaSelector() = throw UnsupportedOperationException("Not used")
override fun popularMangaNextPageSelector(): String? = ".paginate .page_wrap a.next" override fun popularMangaNextPageSelector() = throw UnsupportedOperationException("Not used")
override fun popularMangaFromElement(element: Element): SManga { override fun popularMangaFromElement(element: Element) = throw UnsupportedOperationException("Not used")
val thumb = element.select("a img").first()!!.attr("src") override fun popularMangaParse(response: Response): MangasPage {
val title = element.select(".challengeTitle a").first()!! 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() var pageInfo = apiMangaResponse.pageInfo
manga.url = title.attr("href").substringBefore("&week")
manga.title = title.text().trim() if (pageInfo == null) {
manga.thumbnail_url = thumb val page = response.request.url.queryParameter("page")
return manga 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 latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector() override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element) 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,
)