Add MangaShow.Me (Korean source) (#775)

Add MangaShow.Me (Korean source)
This commit is contained in:
DitFranXX 2019-02-07 00:55:27 +09:00 committed by Carlos
parent 4599a37c4a
commit 8983e8e688
4 changed files with 613 additions and 0 deletions

View File

@ -0,0 +1,16 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
ext {
appName = 'Tachiyomi: MangaShow.Me'
pkgNameSuffix = 'ko.mangashowme'
extClass = '.MangaShowMe'
extVersionCode = 1
libVersion = '1.2'
}
dependencies {
compileOnly project(':duktape-stub')
}
apply from: "$rootDir/common.gradle"

View File

@ -0,0 +1,193 @@
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(val id: Int, name: String) : Filter.CheckBox(name)
private class SearchFieldMatch : Filter.Select<String>("Search Match", arrayOf("Not Set", "AND", "OR"))
private class SearchTagMatch : Filter.Select<String>("Tag Match", arrayOf("AND", "OR"))
private class SearchGenresList(genres: List<SearchCheckBox>) : Filter.Group<SearchCheckBox>("Genres", genres)
private class SearchNamingList(naming: List<SearchCheckBox>) : Filter.Group<SearchCheckBox>("Naming", naming)
private class SearchStatusList(status: List<SearchCheckBox>) : Filter.Group<SearchCheckBox>("Status", status)
private fun searchNaming() = listOf(
SearchCheckBox(0, ""),
SearchCheckBox(1, ""),
SearchCheckBox(2, ""),
SearchCheckBox(3, ""),
SearchCheckBox(4, ""),
SearchCheckBox(5, ""),
SearchCheckBox(6, ""),
SearchCheckBox(7, ""),
SearchCheckBox(8, ""),
SearchCheckBox(9, ""),
SearchCheckBox(10, ""),
SearchCheckBox(11, ""),
SearchCheckBox(12, ""),
SearchCheckBox(13, ""),
SearchCheckBox(14, ""),
SearchCheckBox(15, ""),
SearchCheckBox(16, ""),
SearchCheckBox(17, ""),
SearchCheckBox(18, ""),
SearchCheckBox(19, "A-Z"),
SearchCheckBox(20, "0-9")
)
private fun searchStatus() = listOf(
SearchCheckBox(0, "미분류"),
SearchCheckBox(1, "주간"),
SearchCheckBox(2, "격주"),
SearchCheckBox(3, "월간"),
SearchCheckBox(4, "격월/비정기"),
SearchCheckBox(5, "단편"),
SearchCheckBox(6, "단행본"),
SearchCheckBox(7, "완결")
)
private fun searchGenres() = listOf(
SearchCheckBox(0, "17"),
SearchCheckBox(0, "BL"),
SearchCheckBox(0, "SF"),
SearchCheckBox(0, "TS"),
SearchCheckBox(0, "개그"),
SearchCheckBox(0, "게임"),
SearchCheckBox(0, "공포"),
SearchCheckBox(0, "도박"),
SearchCheckBox(0, "드라마"),
SearchCheckBox(0, "라노벨"),
SearchCheckBox(0, "러브코미디"),
SearchCheckBox(0, "로맨스"),
SearchCheckBox(0, "먹방"),
SearchCheckBox(0, "백합"),
SearchCheckBox(0, "붕탁"),
SearchCheckBox(0, "순정"),
SearchCheckBox(0, "스릴러"),
SearchCheckBox(0, "스포츠"),
SearchCheckBox(0, "시대"),
SearchCheckBox(0, "애니화"),
SearchCheckBox(0, "액션"),
SearchCheckBox(0, "역사"),
SearchCheckBox(0, "요리"),
SearchCheckBox(0, "음악"),
SearchCheckBox(0, "이세계"),
SearchCheckBox(0, "일상"),
SearchCheckBox(0, "전생"),
SearchCheckBox(0, "추리"),
SearchCheckBox(0, "판타지"),
SearchCheckBox(0, "학원"),
SearchCheckBox(0, "호러")
)
fun getFilters() = FilterList(
SearchNamingList(searchNaming()),
SearchStatusList(searchStatus()),
SearchGenresList(searchGenres()),
Filter.Separator(),
SearchFieldMatch(),
SearchTagMatch()
//Filter.Separator(),
//TextField("Author/Artist (Accurate full name)", "author")
)
fun searchComplexFilterMangaRequestBuilder(baseUrl: String, page: Int, query: String, filters: FilterList): Request {
// normal search function.
fun normalSearch(state: Int = 0): Request {
val url = HttpUrl.parse("$baseUrl/bbs/search.php?url=$baseUrl/bbs/search.php")!!.newBuilder()
if (state > 0) {
url.addQueryParameter("sop", arrayOf("and", "or")[state - 1])
}
url.addQueryParameter("stx", query)
if (page > 1) {
url.addQueryParameter("page", "${page - 1}")
}
return GET(url.toString())
}
val nameFilter = mutableListOf<Int>()
val statusFilter = mutableListOf<Int>()
val genresFilter = mutableListOf<String>()
var matchFieldFilter = 0
var matchTagFilter = 1
filters.forEach { filter ->
when (filter) {
is SearchFieldMatch -> {
matchFieldFilter = filter.state
}
}
}
filters.forEach { filter ->
when (filter) {
is SearchTagMatch -> {
if (filter.state > 0) {
matchTagFilter = filter.state + 1
}
}
is SearchNamingList -> {
filter.state.forEach {
if (it.state) {
nameFilter.add(it.id)
}
}
}
is SearchStatusList -> {
filter.state.forEach {
if (it.state) {
statusFilter.add(it.id)
}
}
}
is SearchGenresList -> {
filter.state.forEach {
if (it.state) {
genresFilter.add(it.name)
}
}
}
// is TextField -> {
// if (type == 4 && filter.key == "author") {
// if (filter.key.length > 1) {
// return GET("$baseUrl/bbs/page.php?hid=manga_list&sfl=4&stx=${filter.state}")
// }
// }
// }
}
}
// If Query is over 2 length, just go to normal search
if (query.length > 1) {
return normalSearch(matchFieldFilter)
}
if (nameFilter.isEmpty() && statusFilter.isEmpty() && genresFilter.isEmpty()) {
return GET("$baseUrl/bbs/page.php?hid=manga_list")
}
val url = HttpUrl.parse("$baseUrl/bbs/page.php?hid=manga_list")!!.newBuilder()
url.addQueryParameter("search_type", matchTagFilter.toString())
url.addQueryParameter("_1", nameFilter.joinToString(","))
url.addQueryParameter("_2", statusFilter.joinToString(","))
url.addQueryParameter("_3", genresFilter.joinToString(","))
if (page > 1) {
url.addQueryParameter("page", "${page - 1}")
}
return GET(url.toString())
}

View File

@ -0,0 +1,166 @@
package eu.kanade.tachiyomi.extension.ko.mangashowme
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import eu.kanade.tachiyomi.network.GET
import okhttp3.*
import java.io.ByteArrayOutputStream
import java.io.InputStream
/*
* `v1` means url padding of image host.
* It's not need now, but it remains in this code for sometime.
*/
internal class ImageDecoder(private val version: String, scripts: String) {
private val cnt = substringBetween(scripts, "var view_cnt = ", ";")
.toIntOrNull() ?: 0
private val chapter = substringBetween(scripts, "var chapter = ", ";")
.toIntOrNull() ?: 0
fun request(url: String): String {
return when (version) {
"v1" -> decodeVersion1ImageUrl(cnt, chapter, url)
else -> url
}
}
private fun decodeVersion1ImageUrl(cnt: Int, chapter: Int, url: String): String {
return HttpUrl.parse(url)!!.newBuilder()
.addQueryParameter("cnt", cnt.toString())
.addQueryParameter("ch", chapter.toString())
.addQueryParameter("ver", "v1")
.addQueryParameter("type", "ImageDecodeRequest")
.build()!!.toString()
}
}
internal class ImageDecoderInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request()
val url = req.url().toString()
return if (url.contains("ImageDecodeRequest")) {
try {
val reqUrl = HttpUrl.parse(url)!!
val viewCnt = reqUrl.queryParameter("cnt")!!
val version = reqUrl.queryParameter("ver")!!
val chapter = reqUrl.queryParameter("ch")!!
val imageUrl = url.split("?").first()
val response = chain.proceed(GET(imageUrl))
val res = response.body()!!.byteStream().use {
decodeImageRequest(version, chapter, viewCnt, it)
}
val rb = ResponseBody.create(MediaType.parse("image/png"), res)
response.newBuilder().body(rb).build()
} catch (e: Exception) {
e.printStackTrace()
chain.proceed(req)
}
} else {
chain.proceed(req)
}
}
/*
* `decodeV1ImageNative` is modified version of
* https://github.com/junheah/MangaViewAndroid/blob/master/app/src/main/java/ml/melun/mangaview/Downloader.java#L213-L245
*
* MIT License
*
* Copyright (c) 2019 junheah
*/
private fun decodeV1ImageNative(input: Bitmap, chapter: Int, view_cnt: Int, half: Int = 0, CX: Int = MangaShowMe.V1_CX, CY: Int = MangaShowMe.V1_CY): Bitmap {
if (view_cnt == 0) return input
val viewCnt = view_cnt / 10
//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 -> java.lang.Double.compare(a[1].toDouble(), 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/master/app/src/main/java/ml/melun/mangaview/Downloader.java#L213-L245
*
* MIT License
*
* Copyright (c) 2019 junheah
*/
private fun decoderRandom(chapter: Int, view_cnt: Int, index: Int): Int {
if (chapter < 554714) {
val x = 10000 * Math.sin((view_cnt + index).toDouble())
return Math.floor(100000 * (x - Math.floor(x))).toInt()
}
val seed = view_cnt + index + 1
val t = 100 * Math.sin((10 * seed).toDouble())
val n = 1000 * Math.cos((13 * seed).toDouble())
val a = 10000 * Math.tan((14 * seed).toDouble())
return (Math.floor(100 * (t - Math.floor(t))) +
Math.floor(1000 * (n - Math.floor(n))) +
Math.floor(10000 * (a - Math.floor(a)))).toInt()
}
private fun decodeImageRequest(version: String, chapter: String, view_cnt: String, img: InputStream): ByteArray {
return when (version) {
"v1" -> decodeV1Image(chapter, view_cnt, img)
else -> img.readBytes()
}
}
private fun decodeV1Image(chapter: String, view_cnt: String, img: InputStream): ByteArray {
val decoded = BitmapFactory.decodeStream(img)
val result = decodeV1ImageNative(decoded, chapter.toInt(), view_cnt.toInt())
val output = ByteArrayOutputStream()
result.compress(Bitmap.CompressFormat.PNG, 100, output)
return output.toByteArray()
}
}
private fun substringBetween(target: String, prefix: String, suffix: String): String = {
target.substringAfter(prefix).substringBefore(suffix)
}()

View File

@ -0,0 +1,238 @@
package eu.kanade.tachiyomi.extension.ko.mangashowme
import android.annotation.SuppressLint
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.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 java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
/**
* MangaShow.Me Source
*
* 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 MangaShowMe : ParsedHttpSource() {
override val name = "MangaShow.Me"
override val baseUrl = "https://mangashow.me"
override val lang: String = "ko"
// Latest updates currently returns duplicate manga as it separates manga into chapters
override val supportsLatest = false
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(ImageDecoderInterceptor())
.addInterceptor { chain ->
val response = chain.proceed(chain.request())
if (response.code() == 503) {
val body = response.body().toString()
if (body.contains("console.log(\"503\")") || body.contains("console.log('503')"))
throw Exception("Try again.\nServer returns 503 Service Unavailable.")
}
response
}
.build()!!
//override fun popularMangaSelector() = "div.basic-post-gallery > div > div.post-row"
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 = urlTitleEscape(linkElement.attr("href"))
manga.title = titleElement.text()
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 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").text() ?: ""
val authorText = thumbnailElement.select("a.author").text() ?: ""
val mangaLike = info.select("div.recommend > i.fa").first().text() ?: "0"
val mangaChaptersLike = mangaElementsSum(document.select("div.addedAt i.fa.fa-thumbs-up > span"))
val mangaComments = mangaElementsSum(document.select("div.addedAt i.fa.fa-comment > span"))
val genres = mutableListOf<String>()
document.select("div.left-info > .manga-tags > a.tag").forEach {
genres.add(it.text())
}
val manga = SManga.create()
manga.title = info.select("div.red").text()
// They using background-image style tag for cover. extract url from style attribute.
manga.thumbnail_url = urlFinder(thumbnailElement.attr("style"))
// Only title and thumbnail are provided now.
// TODO: Implement description when site supports it.
manga.description = "\nMangaShow.Me doesn't provide manga description currently.\n" +
"\n\uD83D\uDCDD: ${if (publishTypeText.trim().isBlank()) "Unknown" else publishTypeText}" +
"\n\uD83D\uDCAC: $mangaComments" +
"\n👍: $mangaLike ($mangaChaptersLike)"
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
// "미분류", "" -> SManga.UNKNOWN
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.url = linkElement.attr("href")
chapter.chapter_number = parseChapterNumber(rawName.text())
chapter.name = rawName.ownText().trim()
chapter.date_upload = parseChapterDate(element.select("div.addedAt").text().split(" ").first())
return chapter
}
private fun parseChapterNumber(name: String): Float {
try {
if (name.contains("[단편]")) return 1f
// `특별` means `Special`, so It can be buggy. so pad `편`(Chapter) to prevent false return
if (name.contains("번외") || name.contains("특별편")) return -2f
val regex = Regex("([0-9]+)(?:[-.]([0-9]+))?(?:화)")
val (ch_primal, ch_second) = regex.find(name)!!.destructured
return (ch_primal + if (ch_second.isBlank()) "" else ".$ch_second").toFloatOrNull() ?: -1f
} catch (e: Exception) {
e.printStackTrace()
return -1f
}
}
@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
} 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.select("div.col-md-9.at-col.at-main script")
val imageUrl = element.html().substringAfter("var img_list = [").substringBefore("];")
val imageUrls = JSONArray("[$imageUrl]")
val decoder = ImageDecoder("v1", element.html())
(0 until imageUrls.length())
.map { imageUrls.getString(it) }
.forEach { pages.add(Page(pages.size, "", decoder.request(it))) }
} catch (e: Exception) {
e.printStackTrace()
}
return pages
}
// Latest not supported
override fun latestUpdatesSelector() = throw UnsupportedOperationException("This method should not be called!")
override fun latestUpdatesFromElement(element: Element) = throw UnsupportedOperationException("This method should not be called!")
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException("This method should not be called!")
override fun latestUpdatesNextPageSelector() = throw UnsupportedOperationException("This method should not be called!")
//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(")")
}
// Some title contains `&` and `#` which can cause a error.
private fun urlTitleEscape(title: String): String {
val url = title.split("&manga_name=")
return "${url[0]}&manga_name=" +
url[1].replace("&", "%26").replace("#", "%23")
}
override fun getFilterList() = getFilters()
companion object {
internal const val V1_CX = 5
internal const val V1_CY = 5
}
}