NewToki/ManaToki: update to v1.3.24 (#13546)

* NewToki/ManaToki: update to v1.3.24

* add Korean translation by moonji

* simplify chapter regex

* update Korean translation by manatails

* remove manga title from chapter name

* decrement fallback domain number by 1

* use parsed title in chapter list parse

Co-authored-by: moon <jamiejakie@gmail.com>
Co-authored-by: Pierre Kim <admin@manateeshome.com>
This commit is contained in:
stevenyomi 2022-09-24 05:44:08 +08:00 committed by GitHub
parent 229dd1c9be
commit 55db729814
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 408 additions and 336 deletions

View File

@ -4,8 +4,9 @@ apply plugin: 'kotlin-android'
ext {
extName = 'NewToki / ManaToki'
pkgNameSuffix = 'ko.newtoki'
extClass = '.NewTokiFactory'
extVersionCode = 23
extClass = '.TokiFactory'
extVersionCode = 24
isNsfw = true
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,56 @@
1. Visit https://t.me/s/newtoki3 and load all messages.
2. Run
```js
$$(".tgme_widget_message_service_date").map(e => e.innerText)
```
3. Paste the dates into a spreadsheet.
4. Remove duplicates.
The number is changed to 154 on 2022-09-21.
| Date | Days | Average |
| ---------: | ---: | ------: |
| 2022-09-21 | 12 | 12.0 |
| 2022-09-09 | 7 | 9.5 |
| 2022-09-02 | 8 | 9.0 |
| 2022-08-25 | 6 | 8.3 |
| 2022-08-19 | 11 | 8.8 |
| 2022-08-08 | 10 | 9.0 |
| 2022-07-29 | 8 | 8.9 |
| 2022-07-21 | 9 | 8.9 |
| 2022-07-12 | 7 | 8.7 |
| 2022-07-05 | 11 | 8.9 |
| 2022-06-24 | 6 | 8.6 |
| 2022-06-18 | 8 | 8.6 |
| 2022-06-10 | 3 | 8.2 |
| 2022-06-07 | 11 | 8.4 |
| 2022-05-27 | 6 | 8.2 |
| 2022-05-21 | 11 | 8.4 |
| 2022-05-10 | 11 | 8.5 |
| 2022-04-29 | 6 | 8.4 |
| 2022-04-23 | 8 | 8.4 |
| 2022-04-15 | 6 | 8.3 |
| 2022-04-09 | 10 | 8.3 |
| 2022-03-30 | 11 | 8.5 |
| 2022-03-19 | 9 | 8.5 |
| 2022-03-10 | 14 | 8.7 |
| 2022-02-24 | 12 | 8.8 |
| 2022-02-12 | 15 | 9.1 |
| 2022-01-28 | 9 | 9.1 |
| 2022-01-19 | 9 | 9.1 |
| 2022-01-10 | 17 | 9.3 |
| 2021-12-24 | 7 | 9.3 |
| 2021-12-17 | 6 | 9.2 |
| 2021-12-11 | 8 | 9.1 |
| 2021-12-03 | 9 | 9.1 |
| 2021-11-24 | 8 | 9.1 |
| 2021-11-16 | 11 | 9.1 |
| 2021-11-05 | 7 | 9.1 |
| 2021-10-29 | 9 | 9.1 |
| 2021-10-20 | 12 | 9.2 |
| 2021-10-08 | 8 | 9.1 |
| 2021-09-30 | 15 | 9.3 |
| 2021-09-15 | | |

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
/**
* Source changes domain names every few days (e.g. newtoki31.net to newtoki32.net)
* The domain name was newtoki32 on 2019-11-14, this attempts to match the rate at which the domain changes
*
* Since 2020-09-20, They changed manga side to Manatoki.
* It was merged after shutdown of ManaMoa.
* This is by the head of Manamoa, as they decided to move to Newtoki.
*
* Updated on 2022-09-21, see `domain_log.md`.
* To avoid going too fast and to utilize redirections,
* the number is decremented by 1 initially,
* and increments every 9.2 days which is a bit slower than the average.
*/
val fallbackDomainNumber get() = (154 - 1) + ((System.currentTimeMillis() - 1663723150_000) / 794880_000).toInt()
var domainNumber = ""
get() {
val currentValue = field
if (currentValue.isNotEmpty()) return currentValue
val prefValue = newTokiPreferences.domainNumber
if (prefValue.isNotEmpty()) {
field = prefValue
return prefValue
}
val fallback = fallbackDomainNumber.toString()
domainNumber = fallback
return fallback
}
set(value) {
for (preference in arrayOf(manaTokiPreferences, newTokiPreferences)) {
preference.domainNumber = value
}
field = value
}
object DomainInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = try {
chain.proceed(request)
} catch (e: IOException) {
if (chain.call().isCanceled()) throw e
val newDomainNumber = try {
val document = chain.proceed(GET("https://t.me/s/newtoki3")).asJsoup()
val description = document.selectFirst("meta[property=og:description]").attr("content")
numberRegex.find(description)!!.value
} catch (_: Throwable) {
fallbackDomainNumber.toString()
}
domainNumber = newDomainNumber
val url = request.url
val newHost = numberRegex.replaceFirst(url.host, newDomainNumber)
val newUrl = url.newBuilder().host(newHost).build()
try {
chain.proceed(request.newBuilder().url(newUrl).build())
} catch (e: IOException) {
Log.e("NewToki", "failed to fetch $newUrl", e)
throw IOException(editDomainNumber(), e)
}
}
if (response.priorResponse == null) return response
val newUrl = response.request.url
if ("captcha" in newUrl.toString()) throw IOException(solveCaptcha())
val newHost = newUrl.host
if (newHost.startsWith(MANATOKI_PREFIX) || newHost.startsWith(NEWTOKI_PREFIX)) {
numberRegex.find(newHost)?.run { domainNumber = value }
}
return response
}
private val numberRegex by lazy { Regex("""\d+""") }
}

View File

@ -1,97 +1,32 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.CacheControl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
import java.util.concurrent.TimeUnit
/*
* ManaToki is too big to support in a Factory File., So split into separate file.
*/
class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domainNumber.net", "comic") {
object ManaToki : NewToki("ManaToki", "comic") {
// / ! DO NOT CHANGE THIS ! Only the site name changed from newtoki.
override val id by lazy { generateSourceId("NewToki", lang, versionId) }
override val supportsLatest by lazy { getExperimentLatest() }
override val id = MANATOKI_ID
override fun latestUpdatesSelector() = ".media.post-list"
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/update?hid=update&page=$page")
override fun latestUpdatesNextPageSelector() = "nav.pg_wrap > .pg > strong"
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
// if this is true, Handle Only 10 mangas with accurate Details per page. (Real Latest Page has 70 mangas.)
// Else, Parse from Latest page. which is incomplete.
val isParseWithDetail = getLatestWithDetail()
val reqPage = if (isParseWithDetail) ((page - 1) / 7 + 1) else page
return rateLimitedClient.newCall(latestUpdatesRequest(reqPage))
.asObservableSuccess()
.map { response ->
if (isParseWithDetail) latestUpdatesParseWithDetailPage(response, page)
else latestUpdatesParseWithLatestPage(response)
}
}
override val baseUrl get() = "https://$MANATOKI_PREFIX$domainNumber.net"
private fun latestUpdatesParseWithDetailPage(response: Response, page: Int): MangasPage {
val document = response.asJsoup()
override val preferences = manaTokiPreferences
// given cache time to prevent repeated lots of request in latest.
val cacheControl = CacheControl.Builder().maxAge(28, TimeUnit.DAYS).maxStale(28, TimeUnit.DAYS).build()
private val chapterRegex by lazy { Regex(""" [ \d,~.-]+화$""") }
val rm = 70 * ((page - 1) / 7)
val min = (page - 1) * 10 - rm
val max = page * 10 - rm
val elements = document.select("${latestUpdatesSelector()} p > a").slice(min until max)
val mangas = elements.map { element ->
val url = element.attr("abs:href")
val manga = mangaDetailsParse(rateLimitedClient.newCall(GET(url, cache = cacheControl)).execute())
manga.url = getUrlPath(url)
manga
}
val hasNextPage = try {
!document.select(popularMangaNextPageSelector()).text().contains("10")
} catch (_: Exception) {
false
}
return MangasPage(mangas, hasNextPage)
}
private fun latestUpdatesParseWithLatestPage(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(latestUpdatesSelector()).map { element ->
latestUpdatesElementParse(element)
}
val hasNextPage = try {
!document.select(popularMangaNextPageSelector()).text().contains("10")
} catch (_: Exception) {
false
}
return MangasPage(mangas, hasNextPage)
}
private fun latestUpdatesElementParse(element: Element): SManga {
fun latestUpdatesElementParse(element: Element): SManga {
val linkElement = element.select("a.btn-primary")
val rawTitle = element.select(".post-subject > a").first().ownText().trim()
// TODO: Make Clear Regex.
val chapterRegex = Regex("""((?:\s+)(?:(?:(?:[0-9]+권)?(?:[0-9]+부)?(?:[0-9]*?시즌[0-9]*?)?)?(?:\s*)(?:(?:[0-9]+)(?:[-.](?:[0-9]+))?)?(?:\s*[~,]\s*)?(?:[0-9]+)(?:[-.](?:[0-9]+))?)(?:화))""")
val title = rawTitle.trim().replace(chapterRegex, "")
// val regexSpecialChapter = Regex("(부록|단편|외전|.+편)")
// val lastTitleWord = excludeChapterTitle.split(" ").last()
// val title = excludeChapterTitle.replace(lastTitleWord, lastTitleWord.replace(regexSpecialChapter, ""))
val manga = SManga.create()
manga.url = getUrlPath(linkElement.attr("href"))
@ -104,8 +39,6 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = ("$baseUrl/comic" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder()
val genres = mutableListOf<String>()
filters.forEach { filter ->
when (filter) {
is SearchPublishTypeList -> {
@ -121,20 +54,17 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
}
is SearchGenreTypeList -> {
filter.state.forEach {
if (it.state) {
genres.add(it.id)
}
}
val genres = filter.state.filter { it.state }.joinToString(",") { it.id }
url.addQueryParameter("tag", genres)
}
is SearchSortTypeList -> {
url.addQueryParameter("sst", listOf("wr_datetime", "wr_hit", "wr_good", "as_update")[filter.state])
val state = filter.state ?: return@forEach
url.addQueryParameter("sst", arrayOf("wr_datetime", "wr_hit", "wr_good", "as_update")[state.index])
url.addQueryParameter("sod", if (state.ascending) "asc" else "desc")
}
is SearchOrderTypeList -> {
url.addQueryParameter("sod", listOf("desc", "asc")[filter.state])
}
else -> {}
}
}
@ -142,14 +72,12 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
url.addQueryParameter("stx", query)
// Remove some filter QueryParams that not working with query
url.setQueryParameter("publish", null)
url.setQueryParameter("jaum", null)
return GET(url.toString())
url.removeAllQueryParameters("publish")
url.removeAllQueryParameters("jaum")
url.removeAllQueryParameters("tag")
}
url.addQueryParameter("tag", genres.joinToString(","))
return GET(url.toString())
return GET(url.toString(), headers)
}
private class SearchCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
@ -227,7 +155,7 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
).map { SearchCheckBox(it) }
)
private class SearchSortTypeList : Filter.Select<String>(
private class SearchSortTypeList : Filter.Sort(
"Sort",
arrayOf(
"기본(날짜순)",
@ -237,20 +165,12 @@ class ManaToki(domainNumber: Long) : NewToki("ManaToki", "https://manatoki$domai
)
)
private class SearchOrderTypeList : Filter.Select<String>(
"Order",
arrayOf(
"Descending",
"Ascending"
)
)
override fun getFilterList() = FilterList(
Filter.Header("Some filters can't use with query"),
SearchSortTypeList(),
Filter.Separator(),
Filter.Header(ignoredForTextSearch()),
SearchPublishTypeList(),
SearchJaumTypeList(),
SearchSortTypeList(),
SearchOrderTypeList(),
SearchGenreTypeList()
)
}

View File

@ -1,10 +1,7 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import android.annotation.SuppressLint
import android.app.Application
import android.content.SharedPreferences
import android.widget.Toast
import eu.kanade.tachiyomi.AppInfo
import android.util.Log
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimit
@ -16,41 +13,50 @@ 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 okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URI
import java.net.URISyntaxException
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
/**
* NewToki Source
*
* Based on https://github.com/gnuboard/gnuboard5
**/
open class NewToki(override val name: String, private val defaultBaseUrl: String, private val boardName: String) : ConfigurableSource, ParsedHttpSource() {
override val baseUrl by lazy { getPrefBaseUrl() }
abstract class NewToki(override val name: String, private val boardName: String) : ConfigurableSource, ParsedHttpSource() {
override val lang: String = "ko"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient
protected val rateLimitedClient: OkHttpClient by lazy {
network.cloudflareClient.newBuilder()
.rateLimit(1, getRateLimitPeriod())
.build()
override val client: OkHttpClient by lazy {
buildClient(withRateLimit = false)
}
protected val rateLimitedClient: OkHttpClient by lazy {
buildClient(withRateLimit = true)
}
private fun buildClient(withRateLimit: Boolean) =
network.cloudflareClient.newBuilder()
.apply { if (withRateLimit) rateLimit(1, preferences.rateLimitPeriod.toLong()) }
.addInterceptor(DomainInterceptor) // not rate-limited
.connectTimeout(10, TimeUnit.SECONDS) // fail fast
.build()
override fun popularMangaSelector() = "div#webtoon-list > ul > li"
override fun popularMangaFromElement(element: Element): SManga {
val linkElement = element.getElementsByTag("a").first()
val manga = SManga.create()
manga.setUrlWithoutDomain(linkElement.attr("href").substringBefore("?"))
manga.url = getUrlPath(linkElement.attr("href"))
manga.title = element.select("span.title").first().ownText()
manga.thumbnail_url = linkElement.getElementsByTag("img").attr("src")
return manga
@ -59,17 +65,16 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
override fun popularMangaNextPageSelector() = "ul.pagination > li:last-child:not(.disabled)"
// Do not add page parameter if page is 1 to prevent tracking.
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$boardName" + if (page > 1) "/p$page" else "")
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$boardName" + if (page > 1) "/p$page" else "", headers)
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
override fun searchMangaNextPageSelector() = popularMangaNextPageSelector()
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = GET("$baseUrl/$boardName" + (if (page > 1) "/p$page" else "") + "?stx=$query")
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
val urlPath = "/$boardName/$realQuery"
rateLimitedClient.newCall(GET("$baseUrl$urlPath"))
rateLimitedClient.newCall(GET("$baseUrl$urlPath", headers))
.asObservableSuccess()
.map { response ->
// the id is matches any of 'post' from their CMS board.
@ -95,7 +100,7 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
}
fullListButton?.text()?.contains("전체목록") == true -> { // Check this page is chapter page
val url = fullListButton.attr("abs:href")
val details = mangaDetailsParse(rateLimitedClient.newCall(GET(url)).execute())
val details = mangaDetailsParse(rateLimitedClient.newCall(GET(url, headers)).execute())
details.url = getUrlPath(url)
listOf(details)
}
@ -113,10 +118,11 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
val description = descriptionElement.map {
it.text().trim()
}
val prefix = if (isCleanPath(document.location())) "" else needMigration()
val manga = SManga.create()
manga.title = title
manga.description = description.joinToString("\n")
manga.description = description.joinToString("\n", prefix = prefix)
manga.thumbnail_url = thumbnail
descriptionElement.forEach {
val text = it.text()
@ -148,7 +154,7 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
val rawName = linkElement.ownText().trim()
val chapter = SChapter.create()
chapter.url = getUrlWithoutDomainWithFallback(linkElement.attr("href"))
chapter.setUrlWithoutDomain(linkElement.attr("href"))
chapter.chapter_number = parseChapterNumber(rawName)
chapter.name = rawName
chapter.date_upload = parseChapterDate(element.select(".wr-date").last().text().trim())
@ -160,21 +166,30 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
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 regex = chapterNumberRegex
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()
Log.e("NewToki", "failed to parse chapter number '$name'", e)
return -1f
}
}
private fun mangaDetailsParseWithTitleCheck(manga: SManga, document: Document) =
mangaDetailsParse(document).apply {
// TODO: don't throw when there is download folder rename feature
if (manga.description.isNullOrEmpty() && manga.title != title) {
throw Exception(titleNotMatch(title))
}
}
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return rateLimitedClient.newCall(mangaDetailsRequest(manga))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
val document = response.asJsoup()
mangaDetailsParseWithTitleCheck(manga, document).apply { initialized = true }
}
}
@ -182,11 +197,19 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
return rateLimitedClient.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
val document = response.asJsoup()
val title = mangaDetailsParseWithTitleCheck(manga, document).title
document.select(chapterListSelector()).map {
chapterFromElement(it).apply {
name = name.removePrefix(title).trimStart()
}
}
}
}
@SuppressLint("SimpleDateFormat")
// not thread-safe
private val dateFormat by lazy { SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH) }
private fun parseChapterDate(date: String): Long {
return try {
if (date.contains(":")) {
@ -205,16 +228,14 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
calendar.timeInMillis
} else {
SimpleDateFormat("yyyy.MM.dd").parse(date)?.time ?: 0
dateFormat.parse(date)?.time ?: 0
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("NewToki", "failed to parse chapter date '$date'", e)
0
}
}
private val htmlDataRegex = Regex("""html_data\+='([^']+)'""")
override fun pageListParse(document: Document): List<Page> {
val script = document.select("script:containsData(html_data)").firstOrNull()?.data()
?: throw Exception("data script not found")
@ -231,177 +252,37 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
.mapIndexed { i, img -> Page(i, "", if (img.hasAttr(dataAttr)) img.attr(dataAttr) else img.attr("abs:content")) }
}
override fun latestUpdatesSelector() = popularMangaSelector()
override fun latestUpdatesFromElement(element: Element) = popularMangaFromElement(element)
override fun latestUpdatesRequest(page: Int) = popularMangaRequest(page)
override fun latestUpdatesNextPageSelector() = popularMangaNextPageSelector()
override fun latestUpdatesSelector() = ".media.post-list"
override fun latestUpdatesFromElement(element: Element) = ManaToki.latestUpdatesElementParse(element)
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/update?hid=update&page=$page", headers)
override fun latestUpdatesNextPageSelector() = ".pg_end"
// 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 getFilterList() = FilterList()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
abstract val preferences: SharedPreferences
override fun setupPreferenceScreen(screen: androidx.preference.PreferenceScreen) {
val baseUrlPref = androidx.preference.EditTextPreference(screen.context).apply {
key = BASE_URL_PREF_TITLE
title = BASE_URL_PREF_TITLE
summary = BASE_URL_PREF_SUMMARY
this.setDefaultValue(defaultBaseUrl)
dialogTitle = BASE_URL_PREF_TITLE
dialogMessage = "Default: $defaultBaseUrl"
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putString(BASE_URL_PREF, newValue as String).commit()
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val latestExperimentPref = androidx.preference.CheckBoxPreference(screen.context).apply {
key = EXPERIMENTAL_LATEST_PREF_TITLE
title = EXPERIMENTAL_LATEST_PREF_TITLE
summary = EXPERIMENTAL_LATEST_PREF_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putBoolean(EXPERIMENTAL_LATEST_PREF, newValue as Boolean).commit()
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val latestWithDetailPref = androidx.preference.CheckBoxPreference(screen.context).apply {
key = EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_TITLE
title = EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_TITLE
summary = EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_SUMMARY
setOnPreferenceChangeListener { _, newValue ->
try {
val res = preferences.edit().putBoolean(EXPERIMENTAL_LATEST_WITH_DETAIL_PREF, newValue as Boolean).commit()
// Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
val rateLimitPeriodPref = androidx.preference.EditTextPreference(screen.context).apply {
key = RATE_LIMIT_PERIOD_PREF_TITLE
title = RATE_LIMIT_PERIOD_PREF_TITLE
summary = RATE_LIMIT_PERIOD_PREF_SUMMARY
this.setDefaultValue(defaultRateLimitPeriod.toString())
dialogTitle = RATE_LIMIT_PERIOD_PREF_TITLE
dialogMessage = "Min 1 to Max 9, Invalid value will treat as $defaultRateLimitPeriod. Only Integer.\nDefault: $defaultRateLimitPeriod"
setOnPreferenceChangeListener { _, newValue ->
try {
// Make sure to validate the value.
val p = (newValue as String).toLongOrNull(10)
var value = p ?: defaultRateLimitPeriod
if (p == null || value !in 1..9) {
Toast.makeText(screen.context, RATE_LIMIT_PERIOD_PREF_WARNING_INVALID_VALUE, Toast.LENGTH_LONG).show()
value = defaultRateLimitPeriod
}
val res = preferences.edit().putLong(RATE_LIMIT_PERIOD_PREF, value).commit()
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
res
} catch (e: Exception) {
e.printStackTrace()
false
}
}
}
screen.addPreference(baseUrlPref)
if (name == "ManaToki") {
screen.addPreference(latestExperimentPref)
screen.addPreference(latestWithDetailPref)
}
screen.addPreference(rateLimitPeriodPref)
getPreferencesInternal(screen.context).map(screen::addPreference)
}
protected fun getUrlPath(orig: String): String {
return try {
URI(orig).path
} catch (e: URISyntaxException) {
orig
}
val url = baseUrl.toHttpUrl().resolve(orig) ?: return orig
val pathSegments = url.pathSegments
return "/${pathSegments[0]}/${pathSegments[1]}"
}
// This is just replicate of original method but with fallback.
protected fun getUrlWithoutDomainWithFallback(orig: String): String {
return try {
val uri = URI(orig)
var out = uri.path
if (uri.query != null) {
out += "?" + uri.query
}
if (uri.fragment != null) {
out += "#" + uri.fragment
}
out
} catch (e: URISyntaxException) {
// fallback method. may not work.
orig.substringAfter(baseUrl)
}
}
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
protected fun getExperimentLatest(): Boolean = preferences.getBoolean(EXPERIMENTAL_LATEST_PREF, false)
protected fun getLatestWithDetail(): Boolean = preferences.getBoolean(EXPERIMENTAL_LATEST_WITH_DETAIL_PREF, false)
private fun getRateLimitPeriod(): Long = try { // Check again as preference is bit weirdly buggy.
val v = preferences.getLong(RATE_LIMIT_PERIOD_PREF, defaultRateLimitPeriod)
if (v in 1..9) v else defaultRateLimitPeriod
} catch (e: Exception) {
defaultRateLimitPeriod
private fun isCleanPath(absUrl: String): Boolean {
val url = absUrl.toHttpUrl()
return url.pathSegments.size == 2 && url.querySize == 0 && url.fragment == null
}
companion object {
private const val RESTART_TACHIYOMI = "Restart Tachiyomi to apply new setting."
private const val BASE_URL_PREF_TITLE = "Override BaseUrl"
private val BASE_URL_PREF = "overrideBaseUrl_v${AppInfo.getVersionName()}"
private const val BASE_URL_PREF_SUMMARY = "For temporary uses. Update extension will erase this setting."
// Setting: Experimental Latest Fetcher
private const val EXPERIMENTAL_LATEST_PREF_TITLE = "Enable Latest (Experimental)"
private const val EXPERIMENTAL_LATEST_PREF = "fetchLatestExperiment"
private const val EXPERIMENTAL_LATEST_PREF_SUMMARY = "Fetch Latest Manga using Latest Chapters. May has duplicates and May DB corruption on certain Tachiyomi builds"
// Setting: Experimental Latest Fetcher With Full Details (Optional)
private const val EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_TITLE = "Fetch Latest with detail (Optional)"
private const val EXPERIMENTAL_LATEST_WITH_DETAIL_PREF = "fetchLatestWithDetail"
private const val EXPERIMENTAL_LATEST_WITH_DETAIL_PREF_SUMMARY =
"Parse latest manga details with detail pages. This will reduce DB corruption on certain Tachiyomi builds.\n" +
"But makes chance of IP Ban, Also makes bunch of requests, For prevent IP ban, rate limit is set. so may slow,\n" +
"Still, It's experiment. Required to enable `Enable Latest (Experimental).`"
// Settings: Rate Limit Period
private const val defaultRateLimitPeriod: Long = 2L
private const val RATE_LIMIT_PERIOD_PREF_TITLE = "Rate Limit Request Period Seconds"
private const val RATE_LIMIT_PERIOD_PREF = "rateLimitPeriod"
private const val RATE_LIMIT_PERIOD_PREF_SUMMARY =
"As Source is using Temporary IP ban system to who makes bunch of request, Some of requests are rate limited\n" +
"If you want to reduce limit, Use this option.\n" +
"Invalid value will treat as default $defaultRateLimitPeriod seconds.\n" +
"(Valid: Min 1 to Max 9)"
private const val RATE_LIMIT_PERIOD_PREF_WARNING_INVALID_VALUE = "Invalid value detected. Treating as $defaultRateLimitPeriod..."
const val PREFIX_ID_SEARCH = "id:"
private val chapterNumberRegex by lazy { Regex("([0-9]+)(?:[-.]([0-9]+))?화") }
private val htmlDataRegex by lazy { Regex("""html_data\+='([^']+)'""") }
}
}

View File

@ -1,37 +1,18 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Source changes domain names every few days (e.g. newtoki31.net to newtoki32.net)
* The domain name was newtoki32 on 2019-11-14, this attempts to match the rate at which the domain changes
*
* Since 2020-09-20, They changed manga side to Manatoki.
* It was merged after shutdown of ManaMoa.
* This is by the head of Manamoa, as they decided to move to Newtoki.
*/
private val domainNumber = 32 + ((Date().time - SimpleDateFormat("yyyy-MM-dd", Locale.US).parse("2019-11-14")!!.time) / 595000000)
class NewTokiFactory : SourceFactory {
override fun createSources(): List<Source> = listOf(
ManaToki(domainNumber),
NewTokiWebtoon()
)
}
class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "webtoon") {
object NewTokiWebtoon : NewToki("NewToki", "webtoon") {
// / ! DO NOT CHANGE THIS ! Prevent to treating as a new site
override val id by lazy { generateSourceId("NewToki (Webtoon)", lang, versionId) }
override val id = NEWTOKI_ID
override val baseUrl get() = "https://$NEWTOKI_PREFIX$domainNumber.com"
override val preferences = newTokiPreferences
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = ("$baseUrl/webtoon" + (if (page > 1) "/p$page" else "")).toHttpUrl().newBuilder()
@ -44,12 +25,12 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
}
is SearchSortTypeList -> {
url.addQueryParameter("sst", listOf("as_update", "wr_hit", "wr_good")[filter.state])
val state = filter.state ?: return@forEach
url.addQueryParameter("sst", arrayOf("as_update", "wr_hit", "wr_good")[state.index])
url.addQueryParameter("sod", if (state.ascending) "asc" else "desc")
}
is SearchOrderTypeList -> {
url.addQueryParameter("sod", listOf("desc", "asc")[filter.state])
}
else -> {}
}
}
@ -76,11 +57,13 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
url.addQueryParameter("tag", filter.values[filter.state])
}
}
else -> {}
}
}
}
return GET(url.toString())
return GET(url.toString(), headers)
}
private class SearchTargetTypeList : Filter.Select<String>("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰"))
@ -143,7 +126,7 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
)
)
private class SearchSortTypeList : Filter.Select<String>(
private class SearchSortTypeList : Filter.Sort(
"Sort",
arrayOf(
"기본(업데이트순)",
@ -152,28 +135,13 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
)
)
private class SearchOrderTypeList : Filter.Select<String>(
"Order",
arrayOf(
"Descending",
"Ascending"
)
)
override fun getFilterList() = FilterList(
SearchTargetTypeList(),
SearchSortTypeList(),
SearchOrderTypeList(),
Filter.Separator(),
Filter.Header("Under 3 Filters can't use with query"),
Filter.Header(ignoredForTextSearch()),
SearchYoilTypeList(),
SearchJaumTypeList(),
SearchGenreTypeList()
)
}
fun generateSourceId(name: String, lang: String, versionId: Int): Long {
val key = "${name.lowercase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
return (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}

View File

@ -0,0 +1,74 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
const val MANATOKI_ID = 2526381983439079467L // "NewToki/ko/1"
const val NEWTOKI_ID = 1977818283770282459L // "NewToki (Webtoon)/ko/1"
const val MANATOKI_PREFIX = "manatoki"
const val NEWTOKI_PREFIX = "newtoki"
val manaTokiPreferences = getSharedPreferences(MANATOKI_ID)
val newTokiPreferences = getSharedPreferences(NEWTOKI_ID)
fun getPreferencesInternal(context: Context) = arrayOf(
EditTextPreference(context).apply {
key = DOMAIN_NUMBER_PREF
title = domainNumberTitle()
summary = domainNumberSummary()
setOnPreferenceChangeListener { _, newValue ->
val value = newValue as String
if (value.isEmpty() || value != value.trim()) {
false
} else {
domainNumber = value
true
}
}
},
ListPreference(context).apply {
key = RATE_LIMIT_PERIOD_PREF
title = rateLimitTitle()
summary = "%s\n" + requiresAppRestart()
val values = Array(RATE_LIMIT_PERIOD_MAX) { (it + 1).toString() }
entries = Array(RATE_LIMIT_PERIOD_MAX) { rateLimitEntry(values[it]) }
entryValues = values
setDefaultValue(RATE_LIMIT_PERIOD_DEFAULT)
},
)
var SharedPreferences.domainNumber: String
get() = getString(DOMAIN_NUMBER_PREF, "")!!
set(value) = edit().putString(DOMAIN_NUMBER_PREF, value).apply()
val SharedPreferences.rateLimitPeriod: Int
get() = getString(RATE_LIMIT_PERIOD_PREF, RATE_LIMIT_PERIOD_DEFAULT)!!.toInt().coerceIn(1, RATE_LIMIT_PERIOD_MAX)
/**
* Don't use the following legacy keys:
* - "Override BaseUrl"
* - "overrideBaseUrl_v${AppInfo.getVersionName()}"
* - "Enable Latest (Experimental)"
* - "fetchLatestExperiment"
* - "Fetch Latest with detail (Optional)"
* - "fetchLatestWithDetail"
* - "Rate Limit Request Period Seconds"
*/
private const val DOMAIN_NUMBER_PREF = "domainNumber"
private const val RATE_LIMIT_PERIOD_PREF = "rateLimitPeriod"
private const val RATE_LIMIT_PERIOD_DEFAULT = 2.toString()
private const val RATE_LIMIT_PERIOD_MAX = 9
private fun getSharedPreferences(id: Long): SharedPreferences =
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)

View File

@ -0,0 +1,76 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import android.os.Build
import android.os.LocaleList
import java.util.Locale
private val useKorean by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
LocaleList.getDefault().getFirstMatch(arrayOf("ko", "en"))?.language == "ko"
} else {
Locale.getDefault().language == "ko"
}
}
// region Prompts
fun solveCaptcha() = when {
useKorean -> "WebView에서 캡챠 풀기"
else -> "Solve Captcha with WebView"
}
fun titleNotMatch(realTitle: String) = when {
useKorean -> "이 만화를 찾으시려면 '$realTitle'으로 검색하세요"
else -> "Find this manga by searching '$realTitle'"
}
fun needMigration() = when {
useKorean -> "이 항목은 URL 포맷이 틀립니다. 중복된 항목을 피하려면 동일한 소스로 이전하세요.\n\n"
else -> "This entry has wrong URL format. Please migrate to the same source to avoid duplicates.\n\n"
}
// endregion
// region Filters
fun ignoredForTextSearch() = when {
useKorean -> "검색에서 다음 필터 항목은 무시됩니다"
else -> "The following filters are ignored for text search"
}
// endregion
// region Preferences
fun domainNumberTitle() = when {
useKorean -> "도메인 번호"
else -> "Domain number"
}
fun domainNumberSummary() = when {
useKorean -> "도메인 번호는 자동으로 갱신됩니다"
else -> "This number is updated automatically"
}
fun editDomainNumber() = when {
useKorean -> "확장기능 설정에서 도메인 번호를 수정해 주세요"
else -> "Please edit domain number in extension settings"
}
fun rateLimitTitle() = when {
useKorean -> "요청 제한"
else -> "Rate limit"
}
fun rateLimitEntry(period: String) = when {
useKorean -> "${period}초마다 요청"
else -> "1 request every $period seconds"
}
// taken from app strings
fun requiresAppRestart() = when {
useKorean -> "설정을 적용하려면 앱을 재시작하세요"
else -> "Requires app restart to take effect"
}
// endregion

View File

@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.extension.ko.newtoki
import eu.kanade.tachiyomi.source.SourceFactory
class TokiFactory : SourceFactory {
override fun createSources() = listOf(ManaToki, NewTokiWebtoon)
}