Mangasee - update to use Manga Life code (#3662)

This commit is contained in:
Mike 2020-07-01 04:49:50 -04:00 committed by GitHub
parent e64df144a5
commit f14439bcad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 263 additions and 166 deletions

View File

@ -5,7 +5,7 @@ ext {
extName = 'Mangasee' extName = 'Mangasee'
pkgNameSuffix = 'en.mangasee' pkgNameSuffix = 'en.mangasee'
extClass = '.Mangasee' extClass = '.Mangasee'
extVersionCode = 7 extVersionCode = 8
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,72 +1,174 @@
package eu.kanade.tachiyomi.extension.en.mangasee package eu.kanade.tachiyomi.extension.en.mangasee
import eu.kanade.tachiyomi.network.POST import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.string
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonElement
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.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.ParsedHttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.concurrent.TimeUnit
import okhttp3.FormBody import okhttp3.Headers
import okhttp3.HttpUrl import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONObject import okhttp3.Response
import org.jsoup.nodes.Document import rx.Observable
import org.jsoup.nodes.Element
class Mangasee : ParsedHttpSource() { /**
* Exact same code as Manga Life except for better chapter names thanks to Regex
* Probably should make this a multi-source extension, but decided that that's a problem for a different day
*/
class Mangasee : HttpSource() {
override val id: Long = 9 override val id: Long = 9
override val name = "Mangasee" override val name = "Mangasee"
override val baseUrl = "https://mangaseeonline.us" override val baseUrl = "https://mangasee123.com"
override val lang = "en" override val lang = "en"
override val supportsLatest = true override val supportsLatest = true
private val recentUpdatesPattern = Pattern.compile("(.*?)\\s(\\d+\\.?\\d*)\\s?(Completed)?") override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES)
.build()
override fun popularMangaSelector() = "div.requested > div.row" override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:71.0) Gecko/20100101 Firefox/77.0")
private val gson = GsonBuilder().setLenient().create()
private lateinit var directory: List<JsonElement>
// Popular
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return if (page == 1) {
client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.map { response ->
popularMangaParse(response)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
val (body, requestUrl) = convertQueryToPost(page, "$baseUrl/search/request.php?sortBy=popularity&sortOrder=descending") return GET("$baseUrl/search/", headers)
return POST(requestUrl, headers, body.build())
} }
override fun popularMangaFromElement(element: Element): SManga { // don't use ";" for substringBefore() !
val manga = SManga.create() private fun directoryFromResponse(response: Response): String {
element.select("a.resultLink").first().let { return response.asJsoup().select("script:containsData(MainFunction)").first().data()
manga.setUrlWithoutDomain(it.attr("href")) .substringAfter("vm.Directory = ").substringBefore("vm.GetIntValue").trim()
manga.title = it.text() .replace(";", " ")
}
override fun popularMangaParse(response: Response): MangasPage {
directory = gson.fromJson<JsonArray>(directoryFromResponse(response))
.sortedByDescending { it["v"].string }
return parseDirectory(1)
}
private fun parseDirectory(page: Int): MangasPage {
val mangas = mutableListOf<SManga>()
val endRange = ((page * 24) - 1).let { if (it <= directory.lastIndex) it else directory.lastIndex }
for (i in (((page - 1) * 24)..endRange)) {
mangas.add(SManga.create().apply {
title = directory[i]["s"].string
url = "/manga/${directory[i]["i"].string}"
thumbnail_url = "https://static.mangaboss.net/cover/${directory[i]["i"].string}.jpg"
})
} }
manga.thumbnail_url = element.select("img").attr("abs:src") return MangasPage(mangas, endRange < directory.lastIndex)
return manga
} }
override fun popularMangaNextPageSelector() = "button.requestMore" // Latest
override fun searchMangaSelector() = "div.requested > div.row" override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return if (page == 1) {
client.newCall(latestUpdatesRequest(page))
.asObservableSuccess()
.map { response ->
latestUpdatesParse(response)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun latestUpdatesRequest(page: Int): Request = popularMangaRequest(1)
override fun latestUpdatesParse(response: Response): MangasPage {
directory = gson.fromJson<JsonArray>(directoryFromResponse(response))
.sortedByDescending { it["lt"].string }
return parseDirectory(1)
}
// Search
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return if (page == 1) {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response, query, filters)
}
} else {
Observable.just(parseDirectory(page))
}
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(1)
private fun searchMangaParse(response: Response, query: String, filters: FilterList): MangasPage {
directory = gson.fromJson<JsonArray>(directoryFromResponse(response))
.filter { it["s"].string.contains(query, ignoreCase = true) }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = HttpUrl.parse("$baseUrl/search/request.php")!!.newBuilder()
if (query.isNotEmpty()) url.addQueryParameter("keyword", query)
val genres = mutableListOf<String>() val genres = mutableListOf<String>()
val genresNo = mutableListOf<String>() val genresNo = mutableListOf<String>()
var sortBy: String
for (filter in if (filters.isEmpty()) getFilterList() else filters) { for (filter in if (filters.isEmpty()) getFilterList() else filters) {
when (filter) { when (filter) {
is Sort -> { is Sort -> {
if (filter.state?.index != 0) sortBy = when (filter.state?.index) {
url.addQueryParameter("sortBy", if (filter.state?.index == 1) "dateUpdated" else "popularity") 1 -> "ls"
if (filter.state?.ascending != true) 2 -> "v"
url.addQueryParameter("sortOrder", "descending") else -> "s"
}
directory = if (filter.state?.ascending != true) {
directory.sortedByDescending { it[sortBy].string }
} else {
directory.sortedByDescending { it[sortBy].string }.reversed()
}
} }
is SelectField -> if (filter.state != 0) url.addQueryParameter(filter.key, filter.values[filter.state]) is SelectField -> if (filter.state != 0) directory = when (filter.name) {
is TextField -> if (filter.state.isNotEmpty()) url.addQueryParameter(filter.key, filter.state) "Scan Status" -> directory.filter { it["ss"].string.contains(filter.values[filter.state], ignoreCase = true) }
"Publish Status" -> directory.filter { it["ps"].string.contains(filter.values[filter.state], ignoreCase = true) }
"Type" -> directory.filter { it["t"].string.contains(filter.values[filter.state], ignoreCase = true) }
"Translation" -> directory.filter { it["o"].string.contains("yes", ignoreCase = true) }
else -> directory
}
is YearField -> if (filter.state.isNotEmpty()) directory = directory.filter { it["y"].string.contains(filter.state) }
is AuthorField -> if (filter.state.isNotEmpty()) directory = directory.filter { e -> e["a"].asJsonArray.any { it.string.contains(filter.state, ignoreCase = true) } }
is GenreList -> filter.state.forEach { genre -> is GenreList -> filter.state.forEach { genre ->
when (genre.state) { when (genre.state) {
Filter.TriState.STATE_INCLUDE -> genres.add(genre.name) Filter.TriState.STATE_INCLUDE -> genres.add(genre.name)
@ -75,167 +177,162 @@ class Mangasee : ParsedHttpSource() {
} }
} }
} }
if (genres.isNotEmpty()) url.addQueryParameter("genre", genres.joinToString(",")) if (genres.isNotEmpty()) genres.map { genre -> directory = directory.filter { e -> e["g"].asJsonArray.any { it.string.contains(genre, ignoreCase = true) } } }
if (genresNo.isNotEmpty()) url.addQueryParameter("genreNo", genresNo.joinToString(",")) if (genresNo.isNotEmpty()) genresNo.map { genre -> directory = directory.filterNot { e -> e["g"].asJsonArray.any { it.string.contains(genre, ignoreCase = true) } } }
val (body, requestUrl) = convertQueryToPost(page, url.toString()) return parseDirectory(1)
return POST(requestUrl, headers, body.build())
} }
private fun convertQueryToPost(page: Int, urlString: String): Pair<FormBody.Builder, String> { override fun searchMangaParse(response: Response): MangasPage = throw UnsupportedOperationException("Not used")
val url = HttpUrl.parse(urlString)!!
val body = FormBody.Builder().add("page", page.toString()) // Details
for (i in 0 until url.querySize()) {
body.add(url.queryParameterName(i), url.queryParameterValue(i)) override fun mangaDetailsParse(response: Response): SManga {
return response.asJsoup().select("div.BoxBody > div.row").let { info ->
SManga.create().apply {
title = info.select("h1").text()
author = info.select("li.list-group-item:has(span:contains(Author)) a").first()?.text()
genre = info.select("li.list-group-item:has(span:contains(Genre)) a").joinToString { it.text() }
status = info.select("li.list-group-item:has(span:contains(Status)) a:contains(publish)").text().toStatus()
description = info.select("div.Content").text()
thumbnail_url = info.select("img").attr("abs:src")
}
} }
val requestUrl = url.scheme() + "://" + url.host() + url.encodedPath()
return Pair(body, requestUrl)
} }
override fun searchMangaFromElement(element: Element): SManga { private fun String.toStatus() = when {
val manga = SManga.create() this.contains("Ongoing", ignoreCase = true) -> SManga.ONGOING
element.select("a.resultLink").first().let { this.contains("Complete", ignoreCase = true) -> SManga.COMPLETED
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
}
manga.thumbnail_url = element.select("img").attr("abs:src")
return manga
}
override fun searchMangaNextPageSelector() = "button.requestMore"
override fun mangaDetailsParse(document: Document): SManga {
val detailElement = document.select("div.well > div.row").first()
val manga = SManga.create()
manga.author = detailElement.select("a[href^=/search/?author=]").first()?.text()
manga.genre = detailElement.select("span.details > div.row > div:has(b:contains(Genre(s))) > a").joinToString { it.text() }
manga.description = detailElement.select("strong:contains(Description:) + div").first()?.text()
manga.status = detailElement.select("a[href^=/search/?status=]").first()?.text().orEmpty().let { parseStatus(it) }
manga.thumbnail_url = detailElement.select("div > img").first()?.absUrl("src")
return manga
}
private fun parseStatus(status: String) = when {
status.contains("Ongoing (Scan)") -> SManga.ONGOING
status.contains("Complete (Scan)") -> SManga.COMPLETED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
override fun chapterListSelector() = "div.chapter-list > a" // Chapters - Mind special cases like decimal chapters (e.g. One Punch Man) and manga with seasons (e.g. The Gamer)
override fun chapterFromElement(element: Element): SChapter { private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val urlElement = element.select("a").first()
val chapter = SChapter.create() private fun chapterURLEncode(e: String): String {
chapter.setUrlWithoutDomain(urlElement.attr("href")) var index = ""
chapter.name = element.select("span.chapterLabel").firstOrNull()?.text() ?: "" val t = e.substring(0, 1).toInt()
chapter.date_upload = element.select("time").first()?.attr("datetime")?.let { parseChapterDate(it) } ?: 0 if (1 != t) { index = "-index-$t" }
return chapter val n = e.substring(1, e.length - 1)
var suffix = ""
val path = e.substring(e.length - 1).toInt()
if (0 != path) { suffix = ".$path" }
return "-chapter-$n$index$suffix.html"
} }
private fun parseChapterDate(dateAsString: String): Long { private val chapterImageRegex = Regex("""^0+""")
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US).parse(dateAsString).time
}
override fun pageListParse(document: Document): List<Page> { private fun chapterImage(e: String): String {
val pageArr = document.select("script:containsData(PageArr={)").first().data() val a = e.substring(1, e.length - 1).replace(chapterImageRegex, "")
.substringAfter("PageArr=").substringBefore(";") val b = e.substring(e.length - 1).toInt()
return JSONObject(pageArr).let { jsonObject -> return if (b == 0) {
jsonObject.keys() a
.asSequence() } else {
.toList() "$a.$b"
.filter { it.toIntOrNull() is Int }
.mapIndexed { i, key -> Page(i, "", jsonObject.getString(key)) }
} }
} }
override fun imageUrlParse(document: Document): String = throw UnsupportedOperationException("Not used") override fun chapterListParse(response: Response): List<SChapter> {
val vmChapters = response.asJsoup().select("script:containsData(MainFunction)").first().data()
.substringAfter("vm.Chapters = ").substringBefore(";")
override fun latestUpdatesNextPageSelector() = "button.requestMore" return gson.fromJson<JsonArray>(vmChapters).map { json ->
val indexChapter = json["Chapter"].string
override fun latestUpdatesSelector(): String = "a.latestSeries" SChapter.create().apply {
name = json["ChapterName"].nullString.let { if (it.isNullOrEmpty()) "${json["Type"].string} ${chapterImage(indexChapter)}" else it }
override fun latestUpdatesRequest(page: Int): Request { url = "/read-online/" + response.request().url().toString().substringAfter("/manga/") + chapterURLEncode(indexChapter)
val url = "$baseUrl/home/latest.request.php" date_upload = try {
val (body, requestUrl) = convertQueryToPost(page, url) dateFormat.parse(json["Date"].string.substringBefore(" "))?.time ?: 0
return POST(requestUrl, headers, body.build()) } catch (_: Exception) {
} 0L
}
override fun latestUpdatesFromElement(element: Element): SManga { }
val manga = SManga.create()
element.select("a.latestSeries").first().let {
val chapterUrl = it.attr("href")
val indexOfMangaUrl = chapterUrl.indexOf("-chapter-")
val indexOfLastPath = chapterUrl.lastIndexOf("/")
val mangaUrl = chapterUrl.substring(indexOfLastPath, indexOfMangaUrl)
val defaultText = it.select("p.clamp2").text()
val m = recentUpdatesPattern.matcher(defaultText)
val title = if (m.matches()) m.group(1) else defaultText
manga.setUrlWithoutDomain("/manga$mangaUrl")
manga.title = title
} }
manga.thumbnail_url = element.select("img").attr("abs:src")
return manga
} }
// Pages
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
val script = document.select("script:containsData(MainFunction)").first().data()
val curChapter = gson.fromJson<JsonElement>(script.substringAfter("vm.CurChapter = ").substringBefore(";"))
val pageTotal = curChapter["Page"].string.toInt()
val host = "https://" + script.substringAfter("vm.CurPathName = \"").substringBefore("\"")
val titleURI = script.substringAfter("vm.IndexName = \"").substringBefore("\"")
val seasonURI = curChapter["Directory"].string
.let { if (it.isEmpty()) "" else "$it/" }
val path = "$host/manga/$titleURI/$seasonURI"
val chNum = chapterImage(curChapter["Chapter"].string)
return IntRange(1, pageTotal).mapIndexed { i, _ ->
val imageNum = (i + 1).toString().let { "000$it" }.let { it.substring(it.length - 3) }
Page(i, "", "$path$chNum-$imageNum.png")
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
// Filters
private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Selection(2, false)) private class Sort : Filter.Sort("Sort", arrayOf("Alphabetically", "Date updated", "Popularity"), Selection(2, false))
private class Genre(name: String) : Filter.TriState(name) private class Genre(name: String) : Filter.TriState(name)
private class TextField(name: String, val key: String) : Filter.Text(name) private class YearField : Filter.Text("Years")
private class SelectField(name: String, val key: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state) private class AuthorField : Filter.Text("Author")
private class SelectField(name: String, values: Array<String>, state: Int = 0) : Filter.Select<String>(name, values, state)
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres) private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList( override fun getFilterList() = FilterList(
TextField("Years", "year"), YearField(),
TextField("Author", "author"), AuthorField(),
SelectField("Scan Status", "status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")), SelectField("Scan Status", arrayOf("Any", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing")),
SelectField("Publish Status", "pstatus", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")), SelectField("Publish Status", arrayOf("Any", "Cancelled", "Complete", "Discontinued", "Hiatus", "Incomplete", "Ongoing", "Unfinished")),
SelectField("Type", "type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")), SelectField("Type", arrayOf("Any", "Doujinshi", "Manga", "Manhua", "Manhwa", "OEL", "One-shot")),
Sort(), SelectField("Translation", arrayOf("Any", "Official Only")),
GenreList(getGenreList()) Sort(),
GenreList(getGenreList())
) )
// [...document.querySelectorAll(".genres .list-group-item")].map(el => `Genre("${el.getAttribute('value')}")`).join(',\n') // copied over from Manga Life
// https://mangaseeonline.us/search/
private fun getGenreList() = listOf( private fun getGenreList() = listOf(
Genre("Action"), Genre("Action"),
Genre("Adult"), Genre("Adult"),
Genre("Adventure"), Genre("Adventure"),
Genre("Comedy"), Genre("Comedy"),
Genre("Doujinshi"), Genre("Doujinshi"),
Genre("Drama"), Genre("Drama"),
Genre("Ecchi"), Genre("Ecchi"),
Genre("Fantasy"), Genre("Fantasy"),
Genre("Gender Bender"), Genre("Gender Bender"),
Genre("Harem"), Genre("Harem"),
Genre("Hentai"), Genre("Hentai"),
Genre("Historical"), Genre("Historical"),
Genre("Horror"), Genre("Horror"),
Genre("Isekai"), Genre("Josei"),
Genre("Josei"), Genre("Lolicon"),
Genre("Lolicon"), Genre("Martial Arts"),
Genre("Martial Arts"), Genre("Mature"),
Genre("Mature"), Genre("Mecha"),
Genre("Mecha"), Genre("Mystery"),
Genre("Mystery"), Genre("Psychological"),
Genre("Psychological"), Genre("Romance"),
Genre("Romance"), Genre("School Life"),
Genre("School Life"), Genre("Sci-fi"),
Genre("Sci-fi"), Genre("Seinen"),
Genre("Seinen"), Genre("Shotacon"),
Genre("Seinen Supernatural"), Genre("Shoujo"),
Genre("Shotacon"), Genre("Shoujo Ai"),
Genre("Shoujo"), Genre("Shounen"),
Genre("Shoujo Ai"), Genre("Shounen Ai"),
Genre("Shounen"), Genre("Slice of Life"),
Genre("Shounen Ai"), Genre("Smut"),
Genre("Slice of Life"), Genre("Sports"),
Genre("Smut"), Genre("Supernatural"),
Genre("Sport"), Genre("Tragedy"),
Genre("Sports"), Genre("Yaoi"),
Genre("Supernatural"), Genre("Yuri")
Genre("Tragedy"),
Genre("Yaoi"),
Genre("Yuri")
) )
} }