Update NewToki extension v1.2.16 / Remove ManaMoa Extension (#4389)
Update NewToki extension v1.2.16 / Remove ManaMoa Extension
This commit is contained in:
parent
9bfbba7d9d
commit
8143b34bf0
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".ManaMoaUrlActivity"
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:excludeFromRecents="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<!-- Current domain -->
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="manamoa28.net"
|
||||
android:pathPattern="/bbs/page.php"/>
|
||||
<!-- Future domains -->
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="manamoa29.net"
|
||||
android:pathPattern="/bbs/page.php"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="manamoa30.net"
|
||||
android:pathPattern="/bbs/page.php"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="manamoa31.net"
|
||||
android:pathPattern="/bbs/page.php"/>
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="manamoa32.net"
|
||||
android:pathPattern="/bbs/page.php"/>
|
||||
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,12 +0,0 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'ManaMoa'
|
||||
pkgNameSuffix = 'ko.mangashowme'
|
||||
extClass = '.ManaMoa'
|
||||
extVersionCode = 20
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
|
@ -1,20 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
import org.json.JSONArray
|
||||
|
||||
internal class MMCDNUrlHandler(scripts: String) {
|
||||
private val domains = JSONArray("[${scripts.substringBetween("var cdn_domains = [", "];")}]")
|
||||
private val chapter = scripts.substringBetween("var chapter = ", ";")
|
||||
.toIntOrNull() ?: 0
|
||||
|
||||
fun replace(array: JSONArray): List<String> {
|
||||
return (0 until array.length())
|
||||
.map {
|
||||
val cdn: String = domains.get((chapter + 4 * it) % domains.length()) as String
|
||||
(array.get(it) as String)
|
||||
.replace("cdntigermask.xyz", cdn)
|
||||
.replace("cdnmadmax.xyz", cdn)
|
||||
.replace("filecdn.xyz", cdn)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
|
||||
// TODO: Completely Implement/Update Filters(Genre/Artist).
|
||||
private class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
|
||||
private class SearchCheckBox(name: String, val id: String = name) : Filter.CheckBox(name)
|
||||
|
||||
private class SearchMatch : Filter.Select<String>("Match", arrayOf("AND", "OR"))
|
||||
private class SearchType : Filter.Select<String>("Type", arrayOf("Title", "Artist"))
|
||||
private class SearchGenresList(genres: List<SearchCheckBox>) : Filter.Group<SearchCheckBox>("Genres", genres)
|
||||
private class SearchNamingList : Filter.Select<String>("Naming", searchNaming())
|
||||
private class SearchStatusList : Filter.Select<String>("Status", searchStatus())
|
||||
private class SearchOrderList : Filter.Select<String>("Order", order())
|
||||
|
||||
// [`"Not Set"`, ...[...document.querySelectorAll(".categories ul[data-type='1'] li")].map((el, i) => `"${el.innerText.trim()}"`)].join(',\n')
|
||||
private fun searchNaming() = arrayOf(
|
||||
"Not Set",
|
||||
"ㄱ",
|
||||
"ㄲ",
|
||||
"ㄴ",
|
||||
"ㄷ",
|
||||
"ㄸ",
|
||||
"ㄹ",
|
||||
"ㅁ",
|
||||
"ㅂ",
|
||||
"ㅃ",
|
||||
"ㅅ",
|
||||
"ㅆ",
|
||||
"ㅇ",
|
||||
"ㅈ",
|
||||
"ㅉ",
|
||||
"ㅊ",
|
||||
"ㅋ",
|
||||
"ㅌ",
|
||||
"ㅍ",
|
||||
"ㅎ",
|
||||
"A-Z",
|
||||
"0-9"
|
||||
)
|
||||
|
||||
// [`"Not Set"`, ...[...document.querySelectorAll(".categories ul[data-type='2'] li")].map((el, i) => `"${el.innerText.trim()}"`)].join(',\n')
|
||||
private fun searchStatus() = arrayOf(
|
||||
"Not Set",
|
||||
"주간",
|
||||
"격주",
|
||||
"월간",
|
||||
"격월/비정기",
|
||||
"단편",
|
||||
"단행본",
|
||||
"완결"
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll(".categories ul[data-type='2'] li")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n')
|
||||
private fun order() = arrayOf(
|
||||
"Recent",
|
||||
"Likes",
|
||||
"Popular",
|
||||
"Comments",
|
||||
"Bookmarks"
|
||||
)
|
||||
|
||||
// [...document.querySelectorAll(".categories ul[data-type='3'] li")].map((el, i) => `SearchCheckBox("${el.innerText.trim()}")`).join(',\n')
|
||||
private fun searchGenres() = listOf(
|
||||
SearchCheckBox("17"),
|
||||
SearchCheckBox("BL"),
|
||||
SearchCheckBox("SF"),
|
||||
SearchCheckBox("TS"),
|
||||
SearchCheckBox("개그"),
|
||||
SearchCheckBox("게임"),
|
||||
SearchCheckBox("공포"),
|
||||
SearchCheckBox("도박"),
|
||||
SearchCheckBox("드라마"),
|
||||
SearchCheckBox("라노벨"),
|
||||
SearchCheckBox("러브코미디"),
|
||||
SearchCheckBox("먹방"),
|
||||
SearchCheckBox("백합"),
|
||||
SearchCheckBox("붕탁"),
|
||||
SearchCheckBox("순정"),
|
||||
SearchCheckBox("스릴러"),
|
||||
SearchCheckBox("스포츠"),
|
||||
SearchCheckBox("시대"),
|
||||
SearchCheckBox("애니화"),
|
||||
SearchCheckBox("액션"),
|
||||
SearchCheckBox("음악"),
|
||||
SearchCheckBox("이세계"),
|
||||
SearchCheckBox("일상"),
|
||||
SearchCheckBox("전생"),
|
||||
SearchCheckBox("추리"),
|
||||
SearchCheckBox("판타지"),
|
||||
SearchCheckBox("학원"),
|
||||
SearchCheckBox("호러")
|
||||
)
|
||||
|
||||
fun getFilters() = FilterList(
|
||||
SearchNamingList(),
|
||||
SearchStatusList(),
|
||||
SearchGenresList(searchGenres()),
|
||||
Filter.Separator(),
|
||||
SearchType(),
|
||||
SearchMatch(),
|
||||
SearchOrderList()
|
||||
)
|
||||
|
||||
fun searchComplexFilterMangaRequestBuilder(baseUrl: String, page: Int, query: String, filters: FilterList): Request {
|
||||
var nameFilter: Int? = null
|
||||
var statusFilter: Int? = null
|
||||
val genresFilter = mutableListOf<String>()
|
||||
var matchFilter = 1
|
||||
var orderFilter = 0
|
||||
var typeFilter = 0
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SearchMatch -> {
|
||||
matchFilter = filter.state + 1
|
||||
}
|
||||
|
||||
is SearchOrderList -> {
|
||||
orderFilter = filter.state
|
||||
}
|
||||
|
||||
is SearchType -> {
|
||||
typeFilter = arrayOf(0, 5)[filter.state]
|
||||
}
|
||||
|
||||
is SearchNamingList -> {
|
||||
if (filter.state > 0) {
|
||||
nameFilter = filter.state - 1
|
||||
}
|
||||
}
|
||||
|
||||
is SearchStatusList -> {
|
||||
if (filter.state > 0) {
|
||||
statusFilter = filter.state
|
||||
}
|
||||
}
|
||||
|
||||
is SearchGenresList -> {
|
||||
filter.state.forEach {
|
||||
if (it.state) {
|
||||
genresFilter.add(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
if (!authorFilter.isNullOrEmpty()) {
|
||||
Log.println(Log.DEBUG, "TACHI REQUEST", "ARTIST REQU")
|
||||
return GET("$baseUrl/bbs/page.php?hid=manga_list&search_type=1&sfl=5&_0=$authorFilter&_1=&_2=&_3=&_4=$orderFilter")
|
||||
}
|
||||
*/
|
||||
|
||||
if (query.isEmpty() && nameFilter == null && statusFilter == null && orderFilter == 0 && matchFilter == 1 && genresFilter.isEmpty()) {
|
||||
return GET(
|
||||
"$baseUrl/bbs/page.php?hid=manga_list" +
|
||||
if (page > 1) "&page=${page - 1}" else ""
|
||||
)
|
||||
}
|
||||
|
||||
val url = HttpUrl.parse("$baseUrl/bbs/page.php?hid=manga_list")!!.newBuilder()
|
||||
url.addQueryParameter("search_type", matchFilter.toString())
|
||||
url.addQueryParameter("sfl", typeFilter.toString())
|
||||
url.addQueryParameter("_0", query)
|
||||
url.addQueryParameter("_1", nameFilter?.toString() ?: "")
|
||||
url.addQueryParameter("_2", statusFilter?.toString() ?: "")
|
||||
url.addQueryParameter("_3", genresFilter.joinToString(","))
|
||||
url.addQueryParameter("_4", orderFilter.toString())
|
||||
if (page > 1) {
|
||||
url.addQueryParameter("page", "${page - 1}")
|
||||
}
|
||||
|
||||
return GET(url.toString())
|
||||
}
|
|
@ -1,171 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.sin
|
||||
import kotlin.math.tan
|
||||
|
||||
/*
|
||||
* `v1` means url padding of image host.
|
||||
* It's not need now, but it remains in this code for sometime.
|
||||
*/
|
||||
|
||||
internal class ImageDecoder(scripts: String) {
|
||||
private val cnt = scripts.substringBetween("var view_cnt = ", ";")
|
||||
.toIntOrNull() ?: 0
|
||||
private val chapter = scripts.substringBetween("var chapter = ", ";")
|
||||
.toIntOrNull() ?: 0
|
||||
|
||||
fun request(url: String): String {
|
||||
if (cnt < 10) return url
|
||||
return "$url??$chapter;$cnt"
|
||||
}
|
||||
}
|
||||
|
||||
internal class ImageDecoderInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val req = chain.request()
|
||||
val newReq = req.newBuilder()!!
|
||||
.removeHeader("ImageRequest")
|
||||
.removeHeader("ImageDecodeRequest")
|
||||
.removeHeader("SecondUrlToRequest")
|
||||
.build()!!
|
||||
val response = chain.proceed(newReq)
|
||||
|
||||
val decodeHeader = req.header("ImageDecodeRequest")
|
||||
|
||||
return if (decodeHeader != null) {
|
||||
try {
|
||||
val s = decodeHeader.split(";").map { it.toInt() }
|
||||
|
||||
if (s[1] < 10) return response
|
||||
|
||||
val res = response.body()!!.byteStream().use {
|
||||
decodeImageRequest(it, s[0], s[1])
|
||||
}
|
||||
|
||||
val rb = ResponseBody.create(MediaType.parse("image/png"), res)
|
||||
response.newBuilder().body(rb).build()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw IOException("Image decoder failure.", e)
|
||||
}
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeImageRequest(img: InputStream, chapter: Int, view_cnt: Int): ByteArray {
|
||||
val decoded = BitmapFactory.decodeStream(img)
|
||||
val result = imageDecoder(decoded, chapter, view_cnt)
|
||||
|
||||
val output = ByteArrayOutputStream()
|
||||
result.compress(Bitmap.CompressFormat.PNG, 100, output)
|
||||
return output.toByteArray()
|
||||
}
|
||||
|
||||
/*
|
||||
* `imageDecoder` is modified version of
|
||||
* https://github.com/junheah/MangaViewAndroid/blob/b69a4427258fe7fc5fb5363108572bbee0d65e94/app/src/main/java/ml/melun/mangaview/mangaview/Decoder.java#L6-L60
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2019 junheah
|
||||
*/
|
||||
private fun imageDecoder(input: Bitmap, chapter: Int, view_cnt: Int, half: Int = 0): Bitmap {
|
||||
if (view_cnt == 0) return input
|
||||
val viewCnt = view_cnt / 10
|
||||
var cx = ManaMoa.V1_CX
|
||||
var cy = ManaMoa.V1_CY
|
||||
|
||||
// view_cnt / 10 > 30000 ? (this._CX = 1, this._CY = 6) : view_cnt / 10 > 20000 ? this._CX = 1 : view_cnt / 10 > 10000 && (this._CY = 1)
|
||||
// DO NOT (AUTOMATICALLY) REPLACE TO when USING IDEA. seems it doesn't detect correct condition
|
||||
if (viewCnt > 30000) {
|
||||
cx = 1
|
||||
cy = 6
|
||||
} else if (viewCnt > 20000) {
|
||||
cx = 1
|
||||
} else if (viewCnt > 10000) {
|
||||
cy = 1
|
||||
}
|
||||
|
||||
// decode image
|
||||
val order = Array(cx * cy) { IntArray(2) }
|
||||
val oSize = order.size - 1
|
||||
|
||||
for (i in 0..oSize) {
|
||||
order[i][0] = i
|
||||
order[i][1] = decoderRandom(chapter, viewCnt, i)
|
||||
}
|
||||
|
||||
java.util.Arrays.sort(order) { a, b -> a[1].toDouble().compareTo(b[1].toDouble()) }
|
||||
|
||||
// create new bitmap
|
||||
val outputWidth = if (half == 0) input.width else input.width / 2
|
||||
val output = Bitmap.createBitmap(outputWidth, input.height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(output)
|
||||
|
||||
val rowWidth = input.width / cx
|
||||
val rowHeight = input.height / cy
|
||||
|
||||
for (i in 0..oSize) {
|
||||
val o = order[i]
|
||||
val ox = i % cx
|
||||
val oy = i / cx
|
||||
val tx = o[0] % cx
|
||||
val ty = o[0] / cx
|
||||
val sx = if (half == 2) -input.width / 2 else 0
|
||||
|
||||
val srcX = ox * rowWidth
|
||||
val srcY = oy * rowHeight
|
||||
val destX = (tx * rowWidth) + sx
|
||||
val destY = ty * rowHeight
|
||||
|
||||
canvas.drawBitmap(
|
||||
input,
|
||||
Rect(srcX, srcY, srcX + rowWidth, srcY + rowHeight),
|
||||
Rect(destX, destY, destX + rowWidth, destY + rowHeight),
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/*
|
||||
* `decodeRandom` is modified version of
|
||||
* https://github.com/junheah/MangaViewAndroid/blob/b69a4427258fe7fc5fb5363108572bbee0d65e94/app/src/main/java/ml/melun/mangaview/mangaview/Decoder.java#L6-L60
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2019 junheah
|
||||
*/
|
||||
private fun decoderRandom(chapter: Int, view_cnt: Int, index: Int): Int {
|
||||
if (chapter < 554714) {
|
||||
val x = 10000 * sin((view_cnt + index).toDouble())
|
||||
return floor(100000 * (x - floor(x))).toInt()
|
||||
}
|
||||
|
||||
val seed = view_cnt + index + 1
|
||||
val t = 100 * sin((10 * seed).toDouble())
|
||||
val n = 1000 * cos((13 * seed).toDouble())
|
||||
val a = 10000 * tan((14 * seed).toDouble())
|
||||
|
||||
return (
|
||||
floor(100 * (t - floor(t))) +
|
||||
floor(1000 * (n - floor(n))) +
|
||||
floor(10000 * (a - floor(a)))
|
||||
).toInt()
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
internal class ImageUrlHandlerInterceptor : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response = RequestHandler(chain).run()
|
||||
}
|
||||
|
||||
private class RequestHandler(val chain: Interceptor.Chain) {
|
||||
val req = chain.request()!!
|
||||
val origUrl = req.url().toString()
|
||||
|
||||
fun run(): Response {
|
||||
// only for image Request
|
||||
if (req.header("ImageRequest") != "1") return chain.proceed(req)
|
||||
|
||||
val secondUrl = req.header("SecondUrlToRequest")
|
||||
|
||||
val res = getRequest(origUrl)
|
||||
return if (!isSuccess(res) && secondUrl != null) {
|
||||
getRequest(secondUrl)
|
||||
} else res
|
||||
}
|
||||
|
||||
private fun isSuccess(res: Response): Boolean {
|
||||
val length = res.header("content-length")?.toInt() ?: 0
|
||||
return !(!res.isSuccessful || length < ManaMoa.MINIMUM_IMAGE_SIZE)
|
||||
}
|
||||
|
||||
private fun getRequest(url: String): Response = when {
|
||||
".xyz/" in url -> ownCDNRequestHandler(url)
|
||||
else -> outsideRequestHandler(url)
|
||||
}
|
||||
|
||||
private fun ownCDNRequestHandler(url: String): Response {
|
||||
val res = proceedRequest(url)
|
||||
return if (!isSuccess(res)) {
|
||||
val s3url = if (url.contains("img.")) {
|
||||
url.replace("img.", "s3.")
|
||||
} else {
|
||||
url.replace("://", "://s3.")
|
||||
}
|
||||
proceedRequest(s3url) // s3
|
||||
} else res
|
||||
}
|
||||
|
||||
private fun outsideRequestHandler(url: String): Response {
|
||||
val outUrl = url.substringBefore("?quick")
|
||||
return proceedRequest(outUrl)
|
||||
}
|
||||
|
||||
private fun proceedRequest(url: String): Response = chain.proceed(
|
||||
req.newBuilder()!!
|
||||
.url(url)
|
||||
.removeHeader("ImageRequest")
|
||||
.removeHeader("ImageDecodeRequest")
|
||||
.removeHeader("SecondUrlToRequest")
|
||||
.build()!!
|
||||
)
|
||||
}
|
|
@ -1,531 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.support.v7.preference.CheckBoxPreference
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import android.widget.Toast
|
||||
import eu.kanade.tachiyomi.extension.BuildConfig
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||
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 okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.json.JSONArray
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.select.Elements
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* ManaMoa Source
|
||||
*
|
||||
* Originally it was mangashow.me extension but they changed site structure widely.
|
||||
* so I moved to new name for treating as new source.
|
||||
* Users who uses =<1.2.11 need to migrate source. starts 1.2.12
|
||||
*
|
||||
* PS. There's no Popular section. It's just a list of manga. Also not latest updates.
|
||||
* `manga_list` returns latest 'added' manga. not a chapter updates.
|
||||
**/
|
||||
class ManaMoa : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
override val name = "ManaMoa"
|
||||
|
||||
// This keeps updating: https://twitter.com/manamoa24
|
||||
private val defaultBaseUrl = "https://manamoa34.net"
|
||||
override val baseUrl by lazy { getCurrentBaseUrl() }
|
||||
|
||||
override val lang: String = "ko"
|
||||
|
||||
// Latest updates currently returns duplicate manga as it separates manga into chapters
|
||||
// But allowing to fetch from chapters with experimental setting.
|
||||
override val supportsLatest by lazy { getExperimentLatest() }
|
||||
|
||||
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.addInterceptor(ImageDecoderInterceptor())
|
||||
.addInterceptor(ImageUrlHandlerInterceptor())
|
||||
.build()!!
|
||||
|
||||
override fun popularMangaSelector() = "div.manga-list-gallery > div > div.post-row"
|
||||
|
||||
override fun popularMangaFromElement(element: Element): SManga {
|
||||
val linkElement = element.select("a")
|
||||
val titleElement = element.select(".manga-subject > a").first()
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.url = linkElement.attr("href")
|
||||
manga.title = titleElement.html().trim()
|
||||
manga.thumbnail_url = urlFinder(element.select(".img-wrap-back").attr("style"))
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun popularMangaNextPageSelector() = "ul.pagination > li:not(.disabled)"
|
||||
|
||||
// Do not add page parameter if page is 1 to prevent tracking.
|
||||
override fun popularMangaRequest(page: Int) = GET(
|
||||
"$baseUrl/bbs/page.php?hid=manga_list" +
|
||||
if (page > 1) "&page=${page - 1}" else ""
|
||||
)
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector()).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}
|
||||
|
||||
val hasNextPage = try {
|
||||
!document.select(popularMangaNextPageSelector()).last().hasClass("active")
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
override fun searchMangaSelector() = popularMangaSelector()
|
||||
override fun searchMangaFromElement(element: Element) = popularMangaFromElement(element)
|
||||
override fun searchMangaNextPageSelector() = popularMangaSelector()
|
||||
override fun searchMangaParse(response: Response) = popularMangaParse(response)
|
||||
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 = "/bbs/page.php?hid=manga_detail&manga_id=$realQuery"
|
||||
client.newCall(GET("$baseUrl$urlPath"))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val details = mangaDetailsParse(response)
|
||||
details.url = urlPath
|
||||
MangasPage(listOf(details), false)
|
||||
}
|
||||
} else super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
searchComplexFilterMangaRequestBuilder(baseUrl, page, query, filters)
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val info = document.select("div.left-info").first()
|
||||
val thumbnailElement = info.select("div.manga-thumbnail").first()
|
||||
val publishTypeText = thumbnailElement.select("a.publish_type").trimText("Unknown")
|
||||
val authorText = thumbnailElement.select("a.author").trimText()
|
||||
|
||||
val mangaStatus = info.select("div.recommend")
|
||||
val mangaLike = mangaStatus.select(".fa-thumbs-up").trimText("0")
|
||||
// val mangaViews = trimElementText(mangaStatus.select(".fa-smile-o"), "0")
|
||||
val mangaComments = mangaStatus.select(".fa-comment").trimText("0")
|
||||
val mangaBookmarks = info.select(".fa-bookmark").trimText("0")
|
||||
val mangaChaptersLike = mangaElementsSum(document.select(".title i.fa.fa-thumbs-up > span"))
|
||||
val mangaChaptersComments = mangaElementsSum(document.select(".title i.fa.fa-comment > span"))
|
||||
|
||||
val genres = mutableListOf<String>()
|
||||
document.select("div.left-info div.information > .manga-tags > a.tag").forEach {
|
||||
genres.add(it.text())
|
||||
}
|
||||
|
||||
val manga = SManga.create()
|
||||
manga.title = info.select("div.red.title").html().trim()
|
||||
// They using background-image style tag for cover. extract url from style attribute.
|
||||
manga.thumbnail_url = urlFinder(thumbnailElement.attr("style"))
|
||||
manga.description =
|
||||
"\uD83D\uDCDD: $publishTypeText\n" +
|
||||
"👍: $mangaLike ($mangaChaptersLike)\n" +
|
||||
// "\uD83D\uDD0D: $mangaViews\n" +
|
||||
"\uD83D\uDCAC: $mangaComments ($mangaChaptersComments)\n" +
|
||||
"\uD83D\uDD16: $mangaBookmarks"
|
||||
manga.author = authorText
|
||||
manga.genre = genres.joinToString(", ")
|
||||
manga.status = parseStatus(publishTypeText)
|
||||
return manga
|
||||
}
|
||||
|
||||
private fun parseStatus(status: String) = when (status.trim()) {
|
||||
"주간", "격주", "월간", "격월/비정기", "단행본" -> SManga.ONGOING
|
||||
"단편", "완결" -> SManga.COMPLETED
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
private fun mangaElementsSum(element: Elements?): String {
|
||||
if (element.isNullOrEmpty()) return "0"
|
||||
return try {
|
||||
String.format(
|
||||
"%,d",
|
||||
element.map {
|
||||
it.text().toInt()
|
||||
}.sum()
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
"0"
|
||||
}
|
||||
}
|
||||
|
||||
override fun chapterListSelector() = "div.manga-detail-list > div.chapter-list > .slot"
|
||||
|
||||
override fun chapterFromElement(element: Element): SChapter {
|
||||
val linkElement = element.select("a")
|
||||
val rawName = linkElement.select("div.title").last()
|
||||
|
||||
val chapter = SChapter.create()
|
||||
chapter.setUrlWithoutDomain(linkElement.attr("href"))
|
||||
chapter.chapter_number = parseChapterNumber(rawName.text())
|
||||
chapter.name = rawName.html().substringBefore("<span").trim()
|
||||
chapter.date_upload = parseChapterDate(element.select("div.addedAt").text().split(" ").first())
|
||||
return chapter
|
||||
}
|
||||
|
||||
private fun parseChapterNumber(name: String): Float {
|
||||
try {
|
||||
if (name.endsWith("단편")) 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
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun parseChapterDate(date: String): Long {
|
||||
val calendar = Calendar.getInstance()
|
||||
|
||||
// MangaShow.Me doesn't provide uploaded year now(18/12/15).
|
||||
// If received month is bigger then current month, set last year.
|
||||
// TODO: Fix years due to lack of info.
|
||||
return try {
|
||||
val month = date.trim().split('-').first().toInt()
|
||||
val currYear = calendar.get(Calendar.YEAR)
|
||||
val year = if (month > calendar.get(Calendar.MONTH) + 1) // Before December now, // and Retrieved month is December == 2018.
|
||||
currYear - 1 else currYear
|
||||
SimpleDateFormat("yyyy-MM-dd").parse("$year-$date")?.time ?: 0L
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// They are using full url in every links.
|
||||
// There's possibility to using another domain for serve manga(s). Like marumaru.
|
||||
// override fun pageListRequest(chapter: SChapter) = GET(chapter.url, headers)
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
try {
|
||||
val element = document.toString()
|
||||
val cdnHandler = MMCDNUrlHandler(element)
|
||||
|
||||
val imageUrl = element.substringBetween("var img_list = [", "];")
|
||||
val imageUrls = cdnHandler.replace(JSONArray("[$imageUrl]"))
|
||||
|
||||
val imageUrl1 = element.substringBetween("var img_list1 = [", "];")
|
||||
val imageUrls1 = cdnHandler.replace(JSONArray("[$imageUrl1]"))
|
||||
|
||||
val decoder = ImageDecoder(element)
|
||||
|
||||
(imageUrls.indices)
|
||||
.map {
|
||||
imageUrls[it] + try {
|
||||
"!!${imageUrls1[it]}"
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
.forEach { pages.add(Page(pages.size, decoder.request(it), it.substringBefore("!!"))) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
override fun imageRequest(page: Page): Request {
|
||||
val requestHeaders = try {
|
||||
val data = page.url.substringAfter("??", "")
|
||||
val secondUrl = page.url.substringAfter("!!", "").substringBefore("??")
|
||||
|
||||
val builder = headers.newBuilder()!!
|
||||
|
||||
if (data.isNotBlank()) {
|
||||
builder.add("ImageDecodeRequest", data)
|
||||
}
|
||||
|
||||
if (secondUrl.isNotBlank()) {
|
||||
builder.add("SecondUrlToRequest", secondUrl)
|
||||
}
|
||||
|
||||
builder.build()!!
|
||||
} catch (_: Exception) {
|
||||
headers
|
||||
}.newBuilder()!!.add("ImageRequest", "1").build()!!
|
||||
|
||||
return GET(page.imageUrl!!, requestHeaders)
|
||||
}
|
||||
|
||||
// Latest not supported
|
||||
override fun latestUpdatesSelector() = ".post-row > div.media.post-list"
|
||||
|
||||
override fun latestUpdatesFromElement(element: Element): SManga {
|
||||
val linkElement = element.select("a.btn-primary")
|
||||
val rawTitle = element.select(".post-subject > a").first().ownText()
|
||||
|
||||
// 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 = linkElement.attr("href")
|
||||
manga.title = title
|
||||
manga.thumbnail_url = element.select(".img-item > img").attr("src")
|
||||
manga.initialized = false
|
||||
return manga
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/bbs/board.php?bo_table=manga" + if (page > 1) "&page=$page" else "")
|
||||
override fun latestUpdatesNextPageSelector() = "ul.pagination > li:not(.disabled)"
|
||||
|
||||
// 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!")
|
||||
|
||||
private fun urlFinder(style: String): String {
|
||||
// val regex = Regex("(https?:)?//[-a-zA-Z0-9@:%._\\\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\\\+.~#?&/=]*)")
|
||||
// return regex.find(style)!!.value
|
||||
return style.substringAfter("background-image:url(").substringBefore(")")
|
||||
}
|
||||
|
||||
private fun Elements.trimText(fallback: String = ""): String {
|
||||
return this.text()?.trim()?.takeUnless { it.isBlank() } ?: fallback
|
||||
}
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||
}
|
||||
|
||||
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 autoFetchUrlPref = androidx.preference.CheckBoxPreference(screen.context).apply {
|
||||
key = AUTOFETCH_URL_PREF_TITLE
|
||||
title = AUTOFETCH_URL_PREF_TITLE
|
||||
summary = AUTOFETCH_URL_PREF_SUMMARY
|
||||
this.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val res = preferences.edit().putBoolean(AUTOFETCH_URL_PREF, newValue as Boolean).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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(baseUrlPref)
|
||||
screen.addPreference(autoFetchUrlPref)
|
||||
screen.addPreference(latestExperimentPref)
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
val baseUrlPref = 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 autoFetchUrlPref = CheckBoxPreference(screen.context).apply {
|
||||
key = AUTOFETCH_URL_PREF_TITLE
|
||||
title = AUTOFETCH_URL_PREF_TITLE
|
||||
summary = AUTOFETCH_URL_PREF_SUMMARY
|
||||
this.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
try {
|
||||
val res = preferences.edit().putBoolean(AUTOFETCH_URL_PREF, newValue as Boolean).commit()
|
||||
Toast.makeText(screen.context, RESTART_TACHIYOMI, Toast.LENGTH_LONG).show()
|
||||
res
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val latestExperimentPref = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(baseUrlPref)
|
||||
screen.addPreference(autoFetchUrlPref)
|
||||
screen.addPreference(latestExperimentPref)
|
||||
}
|
||||
|
||||
private fun getCurrentBaseUrl(): String {
|
||||
val prefBaseUrl = getPrefBaseUrl()
|
||||
if (!preferences.getBoolean(AUTOFETCH_URL_PREF, false)) {
|
||||
return prefBaseUrl
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
try {
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
class CallbackFuture : CompletableFuture<Response?>(), Callback {
|
||||
override fun onResponse(call: Call?, response: Response?) {
|
||||
super.complete(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call?, e: IOException?) {
|
||||
super.completeExceptionally(e)
|
||||
}
|
||||
}
|
||||
|
||||
val request: Request = Request.Builder().get()
|
||||
// .url("https://mnmnmnmnm.xyz/")
|
||||
.url("http://52.74.159.59")
|
||||
.build()
|
||||
|
||||
val future = CallbackFuture()
|
||||
network.client
|
||||
// .newBuilder()
|
||||
// .addInterceptor(DDOSGuardInterceptor())
|
||||
// .build()!!
|
||||
.newCall(request).enqueue(future)
|
||||
|
||||
val response = future.get()!!
|
||||
return "https://${response.request().url().host()}"
|
||||
// val code = response.body().toString().substringBetween("manamoa", ".net")
|
||||
// return "https://manamoa$code.net"
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return prefBaseUrl
|
||||
}
|
||||
} else {
|
||||
return prefBaseUrl
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
||||
private fun getExperimentLatest(): Boolean = preferences.getBoolean(EXPERIMENTAL_LATEST_PREF, false)
|
||||
|
||||
override fun getFilterList() = getFilters()
|
||||
|
||||
companion object {
|
||||
// Setting: Override BaseUrl
|
||||
private const val BASE_URL_PREF_TITLE = "Override BaseUrl"
|
||||
private const val BASE_URL_PREF = "overrideBaseUrl_v${BuildConfig.VERSION_NAME}"
|
||||
private const val BASE_URL_PREF_SUMMARY = "For temporary uses. Update extension will erase this setting."
|
||||
// Setting: Fetch Domain
|
||||
private const val AUTOFETCH_URL_PREF_TITLE = "Automatically fetch new domain"
|
||||
private const val AUTOFETCH_URL_PREF = "autoFetchNewUrl"
|
||||
private const val AUTOFETCH_URL_PREF_SUMMARY =
|
||||
"Experimental, May cause Tachiyomi *very* unstable.\n" +
|
||||
"Requires Android Oreo or newer."
|
||||
|
||||
// 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 or invalid name."
|
||||
|
||||
private const val RESTART_TACHIYOMI = "Restart Tachiyomi to apply new setting."
|
||||
|
||||
// Image Decoder
|
||||
internal const val V1_CX = 5
|
||||
internal const val V1_CY = 5
|
||||
|
||||
// Url Handler
|
||||
internal const val MINIMUM_IMAGE_SIZE = 10000
|
||||
|
||||
// Activity Url Handler
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class ManaMoaUrlActivity : Activity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val titleid = intent?.data?.getQueryParameter("manga_id")
|
||||
if (titleid != null && titleid.toInt() > 1) {
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${ManaMoa.PREFIX_ID_SEARCH}$titleid")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e("ManaMoaUrlActivity", e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e("ManaMoaUrlActivity", "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.ko.mangashowme
|
||||
|
||||
internal fun String.substringBetween(prefix: String, suffix: String): String = {
|
||||
this.substringAfter(prefix).substringBefore(suffix)
|
||||
}()
|
|
@ -2,10 +2,10 @@ apply plugin: 'com.android.application'
|
|||
apply plugin: 'kotlin-android'
|
||||
|
||||
ext {
|
||||
extName = 'NewToki'
|
||||
extName = 'NewToki / ManaToki(ManaMoa)'
|
||||
pkgNameSuffix = 'ko.newtoki'
|
||||
extClass = '.NewTokiFactory'
|
||||
extVersionCode = 15
|
||||
extVersionCode = 16
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.extension.ko.newtoki
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.support.v7.preference.CheckBoxPreference
|
||||
import android.support.v7.preference.EditTextPreference
|
||||
import android.support.v7.preference.PreferenceScreen
|
||||
import android.widget.Toast
|
||||
|
@ -20,11 +21,14 @@ import eu.kanade.tachiyomi.util.asJsoup
|
|||
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
|
||||
|
||||
|
@ -82,14 +86,35 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
|
|||
client.newCall(GET("$baseUrl$urlPath"))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
// TODO: Fix the error which caused by shares id field with detail and each chapters in the source
|
||||
val details = mangaDetailsParse(response.asJsoup())
|
||||
details.url = urlPath
|
||||
MangasPage(listOf(details), false)
|
||||
// the id is matches any of 'post' from their CMS board.
|
||||
// Includes Manga Details Page, Chapters, Comments, and etcs...
|
||||
actualMangaParseById(urlPath, response)
|
||||
}
|
||||
} else super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
private fun actualMangaParseById(urlPath: String, response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
// Only exists on detail page.
|
||||
val firstChapterButton = document.select("tr > th > button.btn-blue").first()
|
||||
// only exists on chapter with proper manga detail page.
|
||||
val fullListButton = document.select(".comic-navbar .toon-nav a").last()
|
||||
|
||||
val list: List<SManga> = if (firstChapterButton?.text()?.contains("첫회보기") ?: false) { // Check this page is detail page
|
||||
val details = mangaDetailsParse(document)
|
||||
details.url = urlPath
|
||||
listOf(details)
|
||||
} else if (fullListButton?.text()?.contains("전체목록") ?: false) { // Check this page is chapter page
|
||||
val url = fullListButton.attr("abs:href")
|
||||
val details = mangaDetailsParse(client.newCall(GET(url)).execute())
|
||||
details.url = getUrlPath(url)
|
||||
listOf(details)
|
||||
} else emptyList()
|
||||
|
||||
return MangasPage(list, false)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(document: Document): SManga {
|
||||
val info = document.select("div.view-title > .view-content").first()
|
||||
val title = document.select("div.view-content > span > b").text()
|
||||
|
@ -181,11 +206,18 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
|
|||
}
|
||||
}
|
||||
|
||||
private val htmlDataRegex = Regex("""html_data\+='([^']+)'""")
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
// <article> - <div> - optional <div> - <div> - optional <p> - <img>
|
||||
return document.select("article > div div img")
|
||||
.filterNot { !it.hasAttr("data-original") || it.attr("data-original").contains("blank.gif") }
|
||||
.mapIndexed { i, img -> Page(i, "", img.attr("abs:data-original")) }
|
||||
val script = document.select("script:containsData(html_data)").firstOrNull()?.data() ?: throw Exception("script not found")
|
||||
|
||||
return htmlDataRegex.findAll(script).map { it.groupValues[1] }
|
||||
.asIterable()
|
||||
.flatMap { it.split(".") }
|
||||
.joinToString("") { it.toIntOrNull(16)?.toChar()?.toString() ?: "" }
|
||||
.let { Jsoup.parse(it) }
|
||||
.select("img[alt]")
|
||||
.mapIndexed { i, img -> Page(i, "", if (img.hasAttr("abs:data-original")) img.attr("abs:data-original") else img.attr("abs:content")) }
|
||||
}
|
||||
|
||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||
|
@ -224,7 +256,27 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(baseUrlPref)
|
||||
if (name == "ManaToki") {
|
||||
screen.addPreference(latestExperimentPref)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||
|
@ -248,10 +300,39 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
|
|||
}
|
||||
}
|
||||
|
||||
val latestExperimentPref = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screen.addPreference(baseUrlPref)
|
||||
if (name == "ManaToki") {
|
||||
screen.addPreference(latestExperimentPref)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getUrlPath(orig: String): String {
|
||||
return try {
|
||||
URI(orig).path
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPrefBaseUrl(): String = preferences.getString(BASE_URL_PREF, defaultBaseUrl)!!
|
||||
protected fun getExperimentLatest(): Boolean = preferences.getBoolean(EXPERIMENTAL_LATEST_PREF, false)
|
||||
|
||||
companion object {
|
||||
private const val BASE_URL_PREF_TITLE = "Override BaseUrl"
|
||||
|
@ -259,6 +340,11 @@ open class NewToki(override val name: String, private val defaultBaseUrl: String
|
|||
private const val BASE_URL_PREF_SUMMARY = "For temporary uses. Update extension will erase this setting."
|
||||
private const val RESTART_TACHIYOMI = "Restart Tachiyomi to apply new 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, Also requires LOTS OF requests (70 per page)"
|
||||
|
||||
const val PREFIX_ID_SEARCH = "id:"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,15 +5,17 @@ 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 eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import okhttp3.Response
|
||||
import java.security.MessageDigest
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit.DAYS
|
||||
|
||||
/**
|
||||
* Source changes domain names every few days (e.g. newtoki31.net to newtoki32.net)
|
||||
|
@ -35,6 +37,32 @@ class NewTokiFactory : SourceFactory {
|
|||
class NewTokiManga : NewToki("ManaToki", "https://manatoki$domainNumber.net", "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() }
|
||||
|
||||
// this does 70 request per page....
|
||||
override fun latestUpdatesSelector() = ".media.post-list p > a"
|
||||
override fun latestUpdatesRequest(page: Int) = GET("$baseUrl/page/update?hid=update&page=$page")
|
||||
override fun latestUpdatesNextPageSelector() = "nav.pg_wrap > .pg > strong"
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
// given cache time to prevent repeated lots of request in latest.
|
||||
val cacheControl = CacheControl.Builder().maxAge(14, DAYS).maxStale(14, DAYS).build()
|
||||
val mangas = document.select(latestUpdatesSelector()).map { element ->
|
||||
val url = element.attr("abs:href")
|
||||
val manga = mangaDetailsParse(client.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)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val url = HttpUrl.parse("$baseUrl/comic" + (if (page > 1) "/p$page" else ""))!!.newBuilder()
|
||||
|
@ -151,6 +179,7 @@ class NewTokiManga : NewToki("ManaToki", "https://manatoki$domainNumber.net", "c
|
|||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
Filter.Header("Filter can't use with query"),
|
||||
SearchPublishTypeList(),
|
||||
SearchJaumTypeList(),
|
||||
SearchGenreTypeList()
|
||||
|
@ -165,7 +194,7 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
|
|||
val url = HttpUrl.parse("$baseUrl/webtoon" + (if (page > 1) "/p$page" else ""))!!.newBuilder()
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SearchTypeList -> {
|
||||
is SearchTargetTypeList -> {
|
||||
if (filter.state > 0) {
|
||||
url.addQueryParameter("toon", filter.values[filter.state])
|
||||
}
|
||||
|
@ -173,31 +202,103 @@ class NewTokiWebtoon : NewToki("NewToki", "https://newtoki$domainNumber.com", "w
|
|||
}
|
||||
}
|
||||
|
||||
// Imcompatible with Other Search Parametor
|
||||
if (!query.isBlank()) {
|
||||
url.addQueryParameter("stx", query)
|
||||
} else {
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SearchYoilTypeList -> {
|
||||
if (filter.state > 0) {
|
||||
url.addQueryParameter("yoil", filter.values[filter.state])
|
||||
}
|
||||
}
|
||||
|
||||
is SearchJaumTypeList -> {
|
||||
if (filter.state > 0) {
|
||||
url.addQueryParameter("jaum", filter.values[filter.state])
|
||||
}
|
||||
}
|
||||
|
||||
is SearchGenreTypeList -> {
|
||||
if (filter.state > 0) {
|
||||
url.addQueryParameter("tag", filter.values[filter.state])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return GET(url.toString())
|
||||
}
|
||||
|
||||
private val htmlDataRegex = Regex("""html_data\+='([^']+)'""")
|
||||
private class SearchTargetTypeList : Filter.Select<String>("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰"))
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
val script = document.select("script:containsData(html_data)").firstOrNull()?.data() ?: throw Exception("script not found")
|
||||
// [...document.querySelectorAll("form.form td")[1].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n')
|
||||
private class SearchYoilTypeList : Filter.Select<String>(
|
||||
"Day of the Week",
|
||||
arrayOf(
|
||||
"전체",
|
||||
"월",
|
||||
"화",
|
||||
"수",
|
||||
"목",
|
||||
"금",
|
||||
"토",
|
||||
"일",
|
||||
"열흘"
|
||||
)
|
||||
)
|
||||
|
||||
return htmlDataRegex.findAll(script).map { it.groupValues[1] }
|
||||
.asIterable()
|
||||
.flatMap { it.split(".") }
|
||||
.joinToString("") { it.toIntOrNull(16)?.toChar()?.toString() ?: "" }
|
||||
.let { Jsoup.parse(it) }
|
||||
.select("img[alt]")
|
||||
.mapIndexed { i, img -> Page(i, "", img.attr("abs:data-original")) }
|
||||
}
|
||||
|
||||
private class SearchTypeList : Filter.Select<String>("Type", arrayOf("전체", "일반웹툰", "성인웹툰", "BL/GL", "완결웹툰"))
|
||||
// [...document.querySelectorAll("form.form td")[2].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n')
|
||||
private class SearchJaumTypeList : Filter.Select<String>(
|
||||
"Jaum",
|
||||
arrayOf(
|
||||
"전체",
|
||||
"ㄱ",
|
||||
"ㄴ",
|
||||
"ㄷ",
|
||||
"ㄹ",
|
||||
"ㅁ",
|
||||
"ㅂ",
|
||||
"ㅅ",
|
||||
"ㅇ",
|
||||
"ㅈ",
|
||||
"ㅊ",
|
||||
"ㅋ",
|
||||
"ㅌ",
|
||||
"ㅍ",
|
||||
"ㅎ",
|
||||
"a-z",
|
||||
"0-9"
|
||||
)
|
||||
)
|
||||
// [...document.querySelectorAll("form.form td")[3].querySelectorAll("a")].map((el, i) => `"${el.innerText.trim()}"`).join(',\n')
|
||||
private class SearchGenreTypeList : Filter.Select<String>(
|
||||
"Genre",
|
||||
arrayOf(
|
||||
"전체",
|
||||
"판타지",
|
||||
"액션",
|
||||
"개그",
|
||||
"미스터리",
|
||||
"로맨스",
|
||||
"드라마",
|
||||
"무협",
|
||||
"스포츠",
|
||||
"일상",
|
||||
"학원",
|
||||
"성인"
|
||||
)
|
||||
)
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
SearchTypeList()
|
||||
SearchTargetTypeList(),
|
||||
Filter.Separator(),
|
||||
Filter.Header("Under 3 Filters can't use with query"),
|
||||
SearchYoilTypeList(),
|
||||
SearchJaumTypeList(),
|
||||
SearchGenreTypeList()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue