diff --git a/src/all/webtoons/build.gradle b/src/all/webtoons/build.gradle index 7e7d47bbc..3ed5015d5 100644 --- a/src/all/webtoons/build.gradle +++ b/src/all/webtoons/build.gradle @@ -5,7 +5,7 @@ ext { appName = 'Tachiyomi: Webtoons' pkgNameSuffix = 'all.webtoons' extClass = '.WebtoonsFactory' - extVersionCode = 8 + extVersionCode = 9 libVersion = '1.2' } diff --git a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt index f2a98155d..54016b6e7 100644 --- a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt +++ b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/Webtoons.kt @@ -4,9 +4,7 @@ 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 okhttp3.* import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.util.* @@ -71,7 +69,7 @@ abstract class Webtoons(override val lang: String, open val langCode: String = l return manga } - override fun popularMangaNextPageSelector() = null + override fun popularMangaNextPageSelector() : String? = null override fun latestUpdatesNextPageSelector() = null diff --git a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsFactory.kt b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsFactory.kt index 09a0c48e9..a1369ee1a 100644 --- a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsFactory.kt +++ b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsFactory.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.extension.all.webtoons import eu.kanade.tachiyomi.extension.en.webtoons.WebtoonsEnglish -import eu.kanade.tachiyomi.extension.fr.webtoons.WebtoonsFrench import eu.kanade.tachiyomi.extension.id.webtoons.WebtoonsIndonesian import eu.kanade.tachiyomi.extension.th.webtoons.WebtoonsThai import eu.kanade.tachiyomi.extension.zh.webtoons.WebtoonsChineseTraditional @@ -14,10 +13,42 @@ class WebtoonsFactory : SourceFactory { fun getAllWebtoons(): List { return listOf( - WebtoonsEnglish(), - WebtoonsChineseTraditional(), - WebtoonsFrench(), - WebtoonsIndonesian(), - WebtoonsThai() + WebtoonsEnglish(), + WebtoonsChineseTraditional(), + WebtoonsIndonesian(), + WebtoonsThai(), + + // fan translations + WebtoonsTranslate("en", "ENG"), + WebtoonsTranslate("zh", "CMN", " (Simplified)"), + WebtoonsTranslate("zh", "CHT", " (Traditional)"), + WebtoonsTranslate("th", "THA"), + WebtoonsTranslate("in", "IND"), + WebtoonsTranslate("fr", "FRA"), + WebtoonsTranslate("vi", "VIE"), + WebtoonsTranslate("ru", "RUS"), + WebtoonsTranslate("ar", "ARA"), + WebtoonsTranslate("fil", "FIL"), + WebtoonsTranslate("de", "DEU"), + WebtoonsTranslate("hi", "HIN"), + WebtoonsTranslate("it", "ITA"), + WebtoonsTranslate("ja", "JPN"), + WebtoonsTranslate("pt", "POR", " (Brazilian)"), + WebtoonsTranslate("tr", "TUR"), + WebtoonsTranslate("ms", "MAY"), + WebtoonsTranslate("pl", "POL"), + WebtoonsTranslate("pt", "POT", " (European)"), + WebtoonsTranslate("bg", "BUL"), + WebtoonsTranslate("da", "DAN"), + WebtoonsTranslate("nl", "NLD"), + WebtoonsTranslate("ro", "RON"), + WebtoonsTranslate("mn", "MON"), + WebtoonsTranslate("el", "GRE"), + WebtoonsTranslate("lt", "LIT"), + WebtoonsTranslate("cs", "CES"), + WebtoonsTranslate("sv", "SWE"), + WebtoonsTranslate("bn", "BEN"), + WebtoonsTranslate("fa", "PER"), + WebtoonsTranslate("uk", "UKR") ) } diff --git a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsTranslate.kt b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsTranslate.kt index 59bc3bd2f..c2d294591 100644 --- a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsTranslate.kt +++ b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/all/webtoons/WebtoonsTranslate.kt @@ -1,8 +1,11 @@ package eu.kanade.tachiyomi.extension.all.webtoons -import android.util.Log.e import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.source.model.* +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.HttpUrl import okhttp3.Request import okhttp3.Response import org.json.JSONObject @@ -10,14 +13,100 @@ import org.jsoup.nodes.Document import org.jsoup.nodes.Element import java.util.* -open class WebtoonsTranslate(override val lang: String, private val translationLangCode: String) : Webtoons(lang) { +open class WebtoonsTranslate(override val lang: String, private val translateLangCode: String, private val languageNameExtra: String = "") : Webtoons(lang) { - private val apiBaseUrl = "https://global.apis.naver.com" - - private val chapterListUrlPattern = "/lineWebtoon/ctrans/translatedEpisodes_jsonp.json?titleNo=%d&languageCode=%s&offset=0&limit=10000" + private val apiBaseUrl = HttpUrl.parse("https://global.apis.naver.com")!! + private val mobileBaseUrl = HttpUrl.parse("https://m.webtoons.com")!! + private val thumbnailBaseUrl = "https://mwebtoon-phinf.pstatic.net" private val pageListUrlPattern = "/lineWebtoon/ctrans/translatedEpisodeDetail_jsonp.json?titleNo=%s&episodeNo=%d&languageCode=%s&teamVersion=%d" + private val pageSize = 24 + + override val name = "Webtoons.com Translations${languageNameExtra}" + + override fun headersBuilder() = super.headersBuilder() + .removeAll("Referer") + .add("Referer", mobileBaseUrl.toString()) + + override fun popularMangaRequest(page: Int): Request { + // Webtoons translations doesn't really have a "popular" sort; just "UPDATE", "TITLE_ASC", + // and "TITLE_DESC". Pick UPDATE as the most useful sort. + var url = apiBaseUrl + .resolve("/lineWebtoon/ctrans/translatedWebtoons_jsonp.json")!! + .newBuilder() + .addQueryParameter("orderType", "UPDATE") + .addQueryParameter("offset", "${page * pageSize}") + .addQueryParameter("size", "${pageSize}") + .addQueryParameter("languageCode", translateLangCode) + .build() + return GET(url.toString(), headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val responseText = response.body()!!.string() + val requestURL = response.request().url() + val offset = requestURL.queryParameter("offset")!!.toInt() + val responseJSON = JSONObject(responseText) + val responseCode = responseJSON.getString("code") + if (responseCode != "000") { + throw Exception("Error getting popular manga: error code ${responseCode}") + } + var results = responseJSON.getJSONObject("result") + val totalCount = results.getInt("totalCount") + val titleList = results.getJSONArray("titleList") + val mangas = (0 until titleList.length()).map { i -> + val titleJSON = titleList.get(i) as JSONObject + val titleNo = titleJSON.getInt("titleNo") + val team = titleJSON.optInt("teamVersion", 0) + val relativeThumnailURL = titleJSON.getString("thumbnailIPadUrl") + ?: titleJSON.getString("thumbnailMobileUrl") + SManga.create() + .apply { + title = titleJSON.getString("representTitle") + author = titleJSON.getString("writeAuthorName") + artist = titleJSON.getString("pictureAuthorName") ?: author + thumbnail_url = if (relativeThumnailURL != null) "${thumbnailBaseUrl}${relativeThumnailURL}" else null + status = SManga.UNKNOWN + url = mobileBaseUrl + .resolve("/translate/episodeList")!! + .newBuilder() + .addQueryParameter("titleNo", titleNo.toString()) + .addQueryParameter("languageCode", translateLangCode) + .addQueryParameter("teamVersion", team.toString()) + .build() + .toString() + initialized = true + } + } + return MangasPage(mangas, totalCount < pageSize * offset) + } + + override fun mangaDetailsRequest(manga: SManga): Request { + return GET(manga.url, headers) + } + + override fun mangaDetailsParse(document: Document): SManga { + val getMetaProp = fun(property: String): String = + document.head().select("meta[property=\"${property}\"]").attr("content") + var parsedAuthor = getMetaProp("com-linewebtoon:webtoon:author") + var parsedArtist = parsedAuthor + val authorSplit = parsedAuthor.split(" / ", limit = 2) + if (authorSplit.count() > 1) { + parsedAuthor = authorSplit[0] + parsedArtist = authorSplit[1] + } + + return SManga.create().apply { + title = getMetaProp("og:title") + artist = parsedArtist + author = parsedAuthor + description = getMetaProp("og:description") + status = SManga.UNKNOWN + thumbnail_url = getMetaProp("og:image") + } + } + override fun chapterListSelector(): String = throw Exception("Not used") override fun chapterFromElement(element: Element): SChapter = throw Exception("Not used") @@ -25,16 +114,27 @@ open class WebtoonsTranslate(override val lang: String, private val translationL override fun pageListParse(document: Document): List = throw Exception("Not used") override fun chapterListRequest(manga: SManga): Request { - val original = manga.url; - val titleRegex = Regex("title_?[nN]o=([0-9]*)") - val titleNo = titleRegex.find(original)!!.groupValues[1].toInt() - - val chapterUrl = String.format("$apiBaseUrl$chapterListUrlPattern", titleNo, translationLangCode) - return GET(chapterUrl, headers) + val titleNo = HttpUrl.parse(manga.url)!! + .queryParameter("titleNo") + val chapterUrl = apiBaseUrl + .resolve("/lineWebtoon/ctrans/translatedEpisodes_jsonp.json")!! + .newBuilder() + .addQueryParameter("titleNo", titleNo) + .addQueryParameter("languageCode", translateLangCode) + .addQueryParameter("offset", "0") + .addQueryParameter("limit", "10000") + .toString() + return GET(chapterUrl, mobileHeaders) } override fun chapterListParse(response: Response): List { - val chapterJson = JSONObject(response.body()!!.string()) + val chapterData = response.body()!!.string() + val chapterJson = JSONObject(chapterData) + val responseCode = chapterJson.getString("code") + if (responseCode != "000") { + val message = chapterJson.optString("message", "error code ${responseCode}") + throw Exception("Error getting chapter list: ${message}") + } var results = chapterJson.getJSONObject("result").getJSONArray("episodes") val ret = ArrayList() for (i in 0 until results.length()) { @@ -52,11 +152,14 @@ open class WebtoonsTranslate(override val lang: String, private val translationL chapter_number = obj.getInt("episodeSeq").toFloat() date_upload = obj.getLong("updateYmdt") scanlator = obj.getString("teamVersion") + if (scanlator == "0") { + scanlator = "(wiki)" + } url = String.format(pageListUrlPattern, obj.getInt("titleNo"), obj.getInt("episodeNo"), obj.getString("languageCode"), obj.getInt("teamVersion")) } override fun pageListRequest(chapter: SChapter): Request { - return GET(apiBaseUrl + chapter.url, mobileHeaders) + return GET(apiBaseUrl.resolve(chapter.url).toString(), headers) } override fun pageListParse(response: Response): List { diff --git a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/fr/webtoons/WebtoonsFrench.kt b/src/all/webtoons/src/eu/kanade/tachiyomi/extension/fr/webtoons/WebtoonsFrench.kt deleted file mode 100644 index fc9d846f9..000000000 --- a/src/all/webtoons/src/eu/kanade/tachiyomi/extension/fr/webtoons/WebtoonsFrench.kt +++ /dev/null @@ -1,5 +0,0 @@ -package eu.kanade.tachiyomi.extension.fr.webtoons - -import eu.kanade.tachiyomi.extension.all.webtoons.WebtoonsTranslate - -class WebtoonsFrench : WebtoonsTranslate("fr", "FRA") \ No newline at end of file