Fixes for brazilian sources (#716)

Fixes for brazilian sources
This commit is contained in:
Alessandro Jean 2019-01-09 22:56:12 -02:00 committed by Carlos
parent 65f0403ccb
commit b993afac8d
6 changed files with 541 additions and 528 deletions

View File

@ -5,7 +5,7 @@ ext {
appName = 'Tachiyomi: Mangá Livre' appName = 'Tachiyomi: Mangá Livre'
pkgNameSuffix = 'pt.mangalivre' pkgNameSuffix = 'pt.mangalivre'
extClass = '.MangaLivre' extClass = '.MangaLivre'
extVersionCode = 1 extVersionCode = 2
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -22,293 +22,294 @@ import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MangaLivre : HttpSource() { class MangaLivre : HttpSource() {
override val name = "MangaLivre" override val name = "MangaLivre"
override val baseUrl = "https://mangalivre.com/" override val baseUrl = "https://mangalivre.com"
override val lang = "pt" override val lang = "pt"
override val supportsLatest = true override val supportsLatest = true
// Sometimes the site is slow. // Sometimes the site is slow.
override val client = network.client.newBuilder() override val client =
.connectTimeout(1, TimeUnit.MINUTES) network.client.newBuilder()
.readTimeout(1, TimeUnit.MINUTES) .connectTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.MINUTES)
.build()!! .writeTimeout(1, TimeUnit.MINUTES)
.build()
private val catalogHeaders = Headers.Builder().apply { private val catalogHeaders = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36") add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36")
add("Host", "mangalivre.com") add("Host", "mangalivre.com")
// The API doesn't return the result if this header isn't sent. // The API doesn't return the result if this header isn't sent.
add("X-Requested-With", "XMLHttpRequest") add("X-Requested-With", "XMLHttpRequest")
}.build() }.build()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/home/most_read?page=$page", catalogHeaders) return GET("$baseUrl/home/most_read?page=$page", catalogHeaders)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = jsonParser.parse(response.body()!!.string()).obj
// If "most_read" have boolean false value, then it doesn't have next page.
if (!result["most_read"]!!.isJsonArray)
return MangasPage(emptyList(), false)
val popularMangas = result.getAsJsonArray("most_read")?.map {
popularMangaItemParse(it.obj)
} }
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 10 override fun popularMangaParse(response: Response): MangasPage {
val result = jsonParser.parse(response.body()!!.string()).obj
if (popularMangas != null) // If "most_read" have boolean false value, then it doesn't have next page.
return MangasPage(popularMangas, hasNextPage) if (!result["most_read"]!!.isJsonArray)
return MangasPage(emptyList(), false)
return MangasPage(emptyList(), false) val popularMangas = result.getAsJsonArray("most_read")?.map {
} popularMangaItemParse(it.obj)
}
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply { val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 10
title = obj["serie_name"].nullString ?: ""
thumbnail_url = obj["cover"].nullString
url = obj["link"].nullString ?: ""
}
override fun latestUpdatesRequest(page: Int): Request { if (popularMangas != null)
return GET("$baseUrl/home/releases?page=$page", catalogHeaders) return MangasPage(popularMangas, hasNextPage)
}
override fun latestUpdatesParse(response: Response): MangasPage { return MangasPage(emptyList(), false)
if (response.code() == 500)
return MangasPage(emptyList(), false)
val result = jsonParser.parse(response.body()!!.string()).obj
val latestMangas = result.getAsJsonArray("releases")?.map {
latestMangaItemParse(it.obj)
} }
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 5 private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["serie_name"].nullString ?: ""
if (latestMangas != null) thumbnail_url = obj["cover"].nullString
return MangasPage(latestMangas, hasNextPage) url = obj["link"].nullString ?: ""
return MangasPage(emptyList(), false)
}
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["name"].nullString ?: ""
thumbnail_url = obj["image"].nullString
url = obj["link"].nullString ?: ""
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val form = FormBody.Builder().apply {
add("search", query)
} }
return POST("$baseUrl/lib/search/series.json", catalogHeaders, form.build()) override fun latestUpdatesRequest(page: Int): Request {
} return GET("$baseUrl/home/releases?page=$page", catalogHeaders)
override fun searchMangaParse(response: Response): MangasPage {
val result = jsonParser.parse(response.body()!!.string()).obj
// If "series" have boolean false value, then it doesn't have results.
if (!result["series"]!!.isJsonArray)
return MangasPage(emptyList(), false)
val searchMangas = result.getAsJsonArray("series")?.map {
searchMangaItemParse(it.obj)
} }
if (searchMangas != null) override fun latestUpdatesParse(response: Response): MangasPage {
return MangasPage(searchMangas, false) if (response.code() == 500)
return MangasPage(emptyList(), false)
return MangasPage(emptyList(), false) val result = jsonParser.parse(response.body()!!.string()).obj
}
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply { val latestMangas = result.getAsJsonArray("releases")?.map {
title = obj["name"].nullString ?: "" latestMangaItemParse(it.obj)
thumbnail_url = obj["cover"].nullString }
url = obj["link"].nullString ?: ""
author = obj["author"].nullString
artist = obj["artist"].nullString
}
override fun mangaDetailsParse(response: Response): SManga { val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 5
val document = response.asJsoup()
val isCompleted = document.select("div#series-data span.series-author i.complete-series").first() != null
val cGenre = document.select("div#series-data ul.tags li").joinToString { it!!.text() }
val seriesAuthor = if (isCompleted) { if (latestMangas != null)
document.select("div#series-data span.series-author").first()!!.nextSibling().toString().substringBeforeLast("+") return MangasPage(latestMangas, hasNextPage)
} else {
document.select("div#series-data span.series-author").first()!!.text().substringBeforeLast("+") return MangasPage(emptyList(), false)
} }
val authors = seriesAuthor.split("&") private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
.map { it.trim() } title = obj["name"].nullString ?: ""
thumbnail_url = obj["image"].nullString
val cAuthor = authors.filter { !it.contains("(Arte)") } url = obj["link"].nullString ?: ""
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
val cArtist = authors.filter { it.contains("(Arte)") }
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
// Check if the manga was removed by the publisher.
val cStatus = if (document.select("div.series-blocked-img").first() == null) {
if (isCompleted) SManga.COMPLETED else SManga.ONGOING
} else {
SManga.LICENSED
} }
return SManga.create().apply { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
genre = cGenre val form = FormBody.Builder().apply {
status = cStatus add("search", query)
description = document.select("div#series-data span.series-desc").first()?.text() }
author = cAuthor.joinToString("; ")
artist = if (cArtist.isEmpty()) cAuthor.joinToString("; ") else cArtist.joinToString("; ") return POST("$baseUrl/lib/search/series.json", catalogHeaders, form.build())
} }
}
// Need to override because the chapter API is paginated. override fun searchMangaParse(response: Response): MangasPage {
// Adapted from: val result = jsonParser.parse(response.body()!!.string()).obj
// https://stackoverflow.com/questions/35254323/rxjs-observable-pagination
// https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests // If "series" have boolean false value, then it doesn't have results.
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { if (!result["series"]!!.isJsonArray)
return if (manga.status != SManga.LICENSED) { return MangasPage(emptyList(), false)
fetchChapterList(manga, 1)
} else { val searchMangas = result.getAsJsonArray("series")?.map {
Observable.error(Exception("Licensed - No chapters to show")) searchMangaItemParse(it.obj)
}
if (searchMangas != null)
return MangasPage(searchMangas, false)
return MangasPage(emptyList(), false)
} }
}
private fun fetchChapterList(manga: SManga, page: Int, private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
pastChapters: List<SChapter> = emptyList()): Observable<List<SChapter>> { title = obj["name"].nullString ?: ""
val chapters = pastChapters.toMutableList() thumbnail_url = obj["cover"].nullString
return fetchChapterListPage(manga, page) url = obj["link"].nullString ?: ""
.flatMap { author = obj["author"].nullString
chapters += it artist = obj["artist"].nullString
if (it.isEmpty()) {
Observable.just(chapters)
} else {
fetchChapterList(manga, page + 1, chapters)
}
}
}
private fun fetchChapterListPage(manga: SManga, page: Int): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga, page))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
}
}
override fun chapterListRequest(manga: SManga): Request {
return chapterListRequest(manga, 1)
}
private fun chapterListRequest(manga: SManga, page: Int): Request {
val id = manga.url.substringAfterLast("/")
return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", catalogHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = jsonParser.parse(response.body()!!.string()).obj
if (!result["chapters"]!!.isJsonArray)
return emptyList()
return result.getAsJsonArray("chapters")?.map {
chapterListItemParse(it.obj)
} ?: emptyList()
}
private fun chapterListItemParse(obj: JsonObject): SChapter {
val scan = obj["releases"]!!.asJsonObject!!.entrySet().first().value.obj
val cName = obj["chapter_name"]!!.asString
return SChapter.create().apply {
name = if (cName == "") "Capítulo " + obj["number"]!!.asString else cName
date_upload = parseChapterDate(obj["date"].nullString)
scanlator = scan["scanlators"]!!.asJsonArray.get(0)!!.asJsonObject["name"].nullString
url = scan["link"]!!.nullString ?: ""
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat()
} }
}
private fun parseChapterDate(date: String?) : Long { override fun mangaDetailsParse(response: Response): SManga {
return try { val document = response.asJsoup()
SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH).parse(date).time val isCompleted = document.select("div#series-data span.series-author i.complete-series").first() != null
} catch (e: ParseException) { val cGenre = document.select("div#series-data ul.tags li").joinToString { it!!.text() }
0L
val seriesAuthor = if (isCompleted) {
document.select("div#series-data span.series-author").first()!!.nextSibling().toString().substringBeforeLast("+")
} else {
document.select("div#series-data span.series-author").first()!!.text().substringBeforeLast("+")
}
val authors = seriesAuthor.split("&")
.map { it.trim() }
val cAuthor = authors.filter { !it.contains("(Arte)") }
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
val cArtist = authors.filter { it.contains("(Arte)") }
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
// Check if the manga was removed by the publisher.
val cStatus = if (document.select("div.series-blocked-img").first() == null) {
if (isCompleted) SManga.COMPLETED else SManga.ONGOING
} else {
SManga.LICENSED
}
return SManga.create().apply {
genre = cGenre
status = cStatus
description = document.select("div#series-data span.series-desc").first()?.text()
author = cAuthor.joinToString("; ")
artist = if (cArtist.isEmpty()) cAuthor.joinToString("; ") else cArtist.joinToString("; ")
}
} }
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { // Need to override because the chapter API is paginated.
return client.newCall(pageListRequest(chapter)) // Adapted from:
.asObservableSuccess() // https://stackoverflow.com/questions/35254323/rxjs-observable-pagination
.flatMap { response -> // https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests
val token = getReaderToken(response) override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return@flatMap if (token == "") return if (manga.status != SManga.LICENSED) {
Observable.error(Exception("Licensed - No chapter to show")) fetchChapterList(manga, 1)
else fetchPageListApi(chapter, token) } else {
} Observable.error(Exception("Licensed - No chapters to show"))
} }
}
private fun fetchPageListApi(chapter: SChapter, token: String): Observable<List<Page>> {
val id = "\\/(\\d+)\\/capitulo".toRegex().find(chapter.url)?.groupValues?.get(1) ?: "" private fun fetchChapterList(manga: SManga, page: Int,
return client.newCall(pageListApiRequest(id, token)) pastChapters: List<SChapter> = emptyList()): Observable<List<SChapter>> {
.asObservableSuccess() val chapters = pastChapters.toMutableList()
.map { response -> return fetchChapterListPage(manga, page)
pageListParse(response) .flatMap {
} chapters += it
} if (it.isEmpty()) {
Observable.just(chapters)
private fun pageListApiRequest(id: String, token: String): Request { } else {
return GET("$baseUrl/leitor/pages/$id.json?key=$token", catalogHeaders) fetchChapterList(manga, page + 1, chapters)
} }
}
override fun pageListParse(response: Response): List<Page> { }
val result = jsonParser.parse(response.body()!!.string()).obj
private fun fetchChapterListPage(manga: SManga, page: Int): Observable<List<SChapter>> {
return result["images"]!!.asJsonArray return client.newCall(chapterListRequest(manga, page))
.mapIndexed { i, obj -> .asObservableSuccess()
Page(i, obj.asString, obj.asString) .map { response ->
} chapterListParse(response)
} }
}
override fun fetchImageUrl(page: Page): Observable<String> {
return Observable.just(page.imageUrl!!) override fun chapterListRequest(manga: SManga): Request {
} return chapterListRequest(manga, 1)
}
override fun imageUrlParse(response: Response): String = ""
private fun chapterListRequest(manga: SManga, page: Int): Request {
private fun getReaderToken(response: Response): String { val id = manga.url.substringAfterLast("/")
val document = response.asJsoup() return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", catalogHeaders)
// The pages API needs the token provided in the reader script. }
val scriptSrc = document.select("script[src*=\"reader.min.js\"]")?.first()?.attr("src") ?: ""
return "token=(.*)&id".toRegex().find(scriptSrc)?.groupValues?.get(1) ?: "" override fun chapterListParse(response: Response): List<SChapter> {
} val result = jsonParser.parse(response.body()!!.string()).obj
companion object { if (!result["chapters"]!!.isJsonArray)
val jsonParser by lazy { return emptyList()
JsonParser()
return result.getAsJsonArray("chapters")?.map {
chapterListItemParse(it.obj)
} ?: emptyList()
}
private fun chapterListItemParse(obj: JsonObject): SChapter {
val scan = obj["releases"]!!.asJsonObject!!.entrySet().first().value.obj
val cName = obj["chapter_name"]!!.asString
return SChapter.create().apply {
name = "Cap. ${obj["number"]!!.asString}" + (if (cName == "") "" else " - $cName")
date_upload = parseChapterDate(obj["date"].nullString)
scanlator = scan["scanlators"]!!.asJsonArray.get(0)!!.asJsonObject["name"].nullString
url = scan["link"]!!.nullString ?: ""
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat()
}
}
private fun parseChapterDate(date: String?) : Long {
return try {
SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).time
} catch (e: ParseException) {
0L
}
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.flatMap { response ->
val token = getReaderToken(response)
return@flatMap if (token == "")
Observable.error(Exception("Licensed - No chapter to show"))
else fetchPageListApi(chapter, token)
}
}
private fun fetchPageListApi(chapter: SChapter, token: String): Observable<List<Page>> {
val id = "\\/(\\d+)\\/capitulo".toRegex().find(chapter.url)?.groupValues?.get(1) ?: ""
return client.newCall(pageListApiRequest(id, token))
.asObservableSuccess()
.map { response ->
pageListParse(response)
}
}
private fun pageListApiRequest(id: String, token: String): Request {
return GET("$baseUrl/leitor/pages/$id.json?key=$token", catalogHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val result = jsonParser.parse(response.body()!!.string()).obj
return result["images"]!!.asJsonArray
.mapIndexed { i, obj ->
Page(i, obj.asString, obj.asString)
}
}
override fun fetchImageUrl(page: Page): Observable<String> {
return Observable.just(page.imageUrl!!)
}
override fun imageUrlParse(response: Response): String = ""
private fun getReaderToken(response: Response): String {
val document = response.asJsoup()
// The pages API needs the token provided in the reader script.
val scriptSrc = document.select("script[src*=\"reader.min.js\"]")?.first()?.attr("src") ?: ""
return "token=(.*)&id".toRegex().find(scriptSrc)?.groupValues?.get(1) ?: ""
}
companion object {
val jsonParser by lazy {
JsonParser()
}
} }
}
} }

View File

@ -5,7 +5,7 @@ ext {
appName = 'Tachiyomi: mangásPROJECT' appName = 'Tachiyomi: mangásPROJECT'
pkgNameSuffix = 'pt.mangasproject' pkgNameSuffix = 'pt.mangasproject'
extClass = '.MangasProject' extClass = '.MangasProject'
extVersionCode = 1 extVersionCode = 2
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -22,293 +22,294 @@ import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MangasProject : HttpSource() { class MangasProject : HttpSource() {
override val name = "mangásPROJECT" override val name = "mangásPROJECT"
override val baseUrl = "https://leitor.net" override val baseUrl = "https://leitor.net"
override val lang = "pt" override val lang = "pt"
override val supportsLatest = true override val supportsLatest = true
// Sometimes the site is slow. // Sometimes the site is slow.
override val client = network.client.newBuilder() override val client =
.connectTimeout(1, TimeUnit.MINUTES) network.client.newBuilder()
.readTimeout(1, TimeUnit.MINUTES) .connectTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.MINUTES)
.build()!! .writeTimeout(1, TimeUnit.MINUTES)
.build()
private val catalogHeaders = Headers.Builder().apply { private val catalogHeaders = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36") add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36")
add("Host", "leitor.net") add("Host", "leitor.net")
// The API doesn't return the result if this header isn't sent. // The API doesn't return the result if this header isn't sent.
add("X-Requested-With", "XMLHttpRequest") add("X-Requested-With", "XMLHttpRequest")
}.build() }.build()
override fun popularMangaRequest(page: Int): Request { override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/home/most_read?page=$page", catalogHeaders) return GET("$baseUrl/home/most_read?page=$page", catalogHeaders)
}
override fun popularMangaParse(response: Response): MangasPage {
val result = jsonParser.parse(response.body()!!.string()).obj
// If "most_read" have boolean false value, then it doesn't have next page.
if (!result["most_read"]!!.isJsonArray)
return MangasPage(emptyList(), false)
val popularMangas = result.getAsJsonArray("most_read")?.map {
popularMangaItemParse(it.obj)
} }
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 10 override fun popularMangaParse(response: Response): MangasPage {
val result = jsonParser.parse(response.body()!!.string()).obj
if (popularMangas != null) // If "most_read" have boolean false value, then it doesn't have next page.
return MangasPage(popularMangas, hasNextPage) if (!result["most_read"]!!.isJsonArray)
return MangasPage(emptyList(), false)
return MangasPage(emptyList(), false) val popularMangas = result.getAsJsonArray("most_read")?.map {
} popularMangaItemParse(it.obj)
}
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply { val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 10
title = obj["serie_name"].nullString ?: ""
thumbnail_url = obj["cover"].nullString
url = obj["link"].nullString ?: ""
}
override fun latestUpdatesRequest(page: Int): Request { if (popularMangas != null)
return GET("$baseUrl/home/releases?page=$page", catalogHeaders) return MangasPage(popularMangas, hasNextPage)
}
override fun latestUpdatesParse(response: Response): MangasPage { return MangasPage(emptyList(), false)
if (response.code() == 500)
return MangasPage(emptyList(), false)
val result = jsonParser.parse(response.body()!!.string()).obj
val latestMangas = result.getAsJsonArray("releases")?.map {
latestMangaItemParse(it.obj)
} }
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 5 private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["serie_name"].nullString ?: ""
if (latestMangas != null) thumbnail_url = obj["cover"].nullString
return MangasPage(latestMangas, hasNextPage) url = obj["link"].nullString ?: ""
return MangasPage(emptyList(), false)
}
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["name"].nullString ?: ""
thumbnail_url = obj["image"].nullString
url = obj["link"].nullString ?: ""
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val form = FormBody.Builder().apply {
add("search", query)
} }
return POST("$baseUrl/lib/search/series.json", catalogHeaders, form.build()) override fun latestUpdatesRequest(page: Int): Request {
} return GET("$baseUrl/home/releases?page=$page", catalogHeaders)
override fun searchMangaParse(response: Response): MangasPage {
val result = jsonParser.parse(response.body()!!.string()).obj
// If "series" have boolean false value, then it doesn't have results.
if (!result["series"]!!.isJsonArray)
return MangasPage(emptyList(), false)
val searchMangas = result.getAsJsonArray("series")?.map {
searchMangaItemParse(it.obj)
} }
if (searchMangas != null) override fun latestUpdatesParse(response: Response): MangasPage {
return MangasPage(searchMangas, false) if (response.code() == 500)
return MangasPage(emptyList(), false)
return MangasPage(emptyList(), false) val result = jsonParser.parse(response.body()!!.string()).obj
}
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply { val latestMangas = result.getAsJsonArray("releases")?.map {
title = obj["name"].nullString ?: "" latestMangaItemParse(it.obj)
thumbnail_url = obj["cover"].nullString }
url = obj["link"].nullString ?: ""
author = obj["author"].nullString
artist = obj["artist"].nullString
}
override fun mangaDetailsParse(response: Response): SManga { val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 5
val document = response.asJsoup()
val isCompleted = document.select("div#series-data span.series-author i.complete-series").first() != null
val cGenre = document.select("div#series-data ul.tags li").joinToString { it!!.text() }
val seriesAuthor = if (isCompleted) { if (latestMangas != null)
document.select("div#series-data span.series-author").first()!!.nextSibling().toString().substringBeforeLast("+") return MangasPage(latestMangas, hasNextPage)
} else {
document.select("div#series-data span.series-author").first()!!.text().substringBeforeLast("+") return MangasPage(emptyList(), false)
} }
val authors = seriesAuthor.split("&") private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
.map { it.trim() } title = obj["name"].nullString ?: ""
thumbnail_url = obj["image"].nullString
val cAuthor = authors.filter { !it.contains("(Arte)") } url = obj["link"].nullString ?: ""
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
val cArtist = authors.filter { it.contains("(Arte)") }
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
// Check if the manga was removed by the publisher.
val cStatus = if (document.select("div.series-blocked-img").first() == null) {
if (isCompleted) SManga.COMPLETED else SManga.ONGOING
} else {
SManga.LICENSED
} }
return SManga.create().apply { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
genre = cGenre val form = FormBody.Builder().apply {
status = cStatus add("search", query)
description = document.select("div#series-data span.series-desc").first()?.text() }
author = cAuthor.joinToString("; ")
artist = if (cArtist.isEmpty()) cAuthor.joinToString("; ") else cArtist.joinToString("; ") return POST("$baseUrl/lib/search/series.json", catalogHeaders, form.build())
} }
}
// Need to override because the chapter API is paginated. override fun searchMangaParse(response: Response): MangasPage {
// Adapted from: val result = jsonParser.parse(response.body()!!.string()).obj
// https://stackoverflow.com/questions/35254323/rxjs-observable-pagination
// https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests // If "series" have boolean false value, then it doesn't have results.
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { if (!result["series"]!!.isJsonArray)
return if (manga.status != SManga.LICENSED) { return MangasPage(emptyList(), false)
fetchChapterList(manga, 1)
} else { val searchMangas = result.getAsJsonArray("series")?.map {
Observable.error(Exception("Licensed - No chapters to show")) searchMangaItemParse(it.obj)
}
if (searchMangas != null)
return MangasPage(searchMangas, false)
return MangasPage(emptyList(), false)
} }
}
private fun fetchChapterList(manga: SManga, page: Int, private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
pastChapters: List<SChapter> = emptyList()): Observable<List<SChapter>> { title = obj["name"].nullString ?: ""
val chapters = pastChapters.toMutableList() thumbnail_url = obj["cover"].nullString
return fetchChapterListPage(manga, page) url = obj["link"].nullString ?: ""
.flatMap { author = obj["author"].nullString
chapters += it artist = obj["artist"].nullString
if (it.isEmpty()) {
Observable.just(chapters)
} else {
fetchChapterList(manga, page + 1, chapters)
}
}
}
private fun fetchChapterListPage(manga: SManga, page: Int): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga, page))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
}
}
override fun chapterListRequest(manga: SManga): Request {
return chapterListRequest(manga, 1)
}
private fun chapterListRequest(manga: SManga, page: Int): Request {
val id = manga.url.substringAfterLast("/")
return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", catalogHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val result = jsonParser.parse(response.body()!!.string()).obj
if (!result["chapters"]!!.isJsonArray)
return emptyList()
return result.getAsJsonArray("chapters")?.map {
chapterListItemParse(it.obj)
} ?: emptyList()
}
private fun chapterListItemParse(obj: JsonObject): SChapter {
val scan = obj["releases"]!!.asJsonObject!!.entrySet().first().value.obj
val cName = obj["chapter_name"]!!.asString
return SChapter.create().apply {
name = if (cName == "") "Capítulo " + obj["number"]!!.asString else cName
date_upload = parseChapterDate(obj["date"].nullString)
scanlator = scan["scanlators"]!!.asJsonArray.get(0)!!.asJsonObject["name"].nullString
url = scan["link"]!!.nullString ?: ""
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat()
} }
}
private fun parseChapterDate(date: String?) : Long { override fun mangaDetailsParse(response: Response): SManga {
return try { val document = response.asJsoup()
SimpleDateFormat("dd/MM/yyyy", Locale.ENGLISH).parse(date).time val isCompleted = document.select("div#series-data span.series-author i.complete-series").first() != null
} catch (e: ParseException) { val cGenre = document.select("div#series-data ul.tags li").joinToString { it!!.text() }
0L
val seriesAuthor = if (isCompleted) {
document.select("div#series-data span.series-author").first()!!.nextSibling().toString().substringBeforeLast("+")
} else {
document.select("div#series-data span.series-author").first()!!.text().substringBeforeLast("+")
}
val authors = seriesAuthor.split("&")
.map { it.trim() }
val cAuthor = authors.filter { !it.contains("(Arte)") }
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
val cArtist = authors.filter { it.contains("(Arte)") }
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
.map { author ->
if (author.contains(", ")) {
val authorSplit = author.split(", ")
authorSplit[1] + " " + authorSplit[0]
} else {
author
}
}
// Check if the manga was removed by the publisher.
val cStatus = if (document.select("div.series-blocked-img").first() == null) {
if (isCompleted) SManga.COMPLETED else SManga.ONGOING
} else {
SManga.LICENSED
}
return SManga.create().apply {
genre = cGenre
status = cStatus
description = document.select("div#series-data span.series-desc").first()?.text()
author = cAuthor.joinToString("; ")
artist = if (cArtist.isEmpty()) cAuthor.joinToString("; ") else cArtist.joinToString("; ")
}
} }
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { // Need to override because the chapter API is paginated.
return client.newCall(pageListRequest(chapter)) // Adapted from:
.asObservableSuccess() // https://stackoverflow.com/questions/35254323/rxjs-observable-pagination
.flatMap { response -> // https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests
val token = getReaderToken(response) override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return@flatMap if (token == "") return if (manga.status != SManga.LICENSED) {
Observable.error(Exception("Licensed - No chapter to show")) fetchChapterList(manga, 1)
else fetchPageListApi(chapter, token) } else {
} Observable.error(Exception("Licensed - No chapters to show"))
} }
}
private fun fetchPageListApi(chapter: SChapter, token: String): Observable<List<Page>> {
val id = "\\/(\\d+)\\/capitulo".toRegex().find(chapter.url)?.groupValues?.get(1) ?: "" private fun fetchChapterList(manga: SManga, page: Int,
return client.newCall(pageListApiRequest(id, token)) pastChapters: List<SChapter> = emptyList()): Observable<List<SChapter>> {
.asObservableSuccess() val chapters = pastChapters.toMutableList()
.map { response -> return fetchChapterListPage(manga, page)
pageListParse(response) .flatMap {
} chapters += it
} if (it.isEmpty()) {
Observable.just(chapters)
private fun pageListApiRequest(id: String, token: String): Request { } else {
return GET("$baseUrl/leitor/pages/$id.json?key=$token", catalogHeaders) fetchChapterList(manga, page + 1, chapters)
} }
}
override fun pageListParse(response: Response): List<Page> { }
val result = jsonParser.parse(response.body()!!.string()).obj
private fun fetchChapterListPage(manga: SManga, page: Int): Observable<List<SChapter>> {
return result["images"]!!.asJsonArray return client.newCall(chapterListRequest(manga, page))
.mapIndexed { i, obj -> .asObservableSuccess()
Page(i, obj.asString, obj.asString) .map { response ->
} chapterListParse(response)
} }
}
override fun fetchImageUrl(page: Page): Observable<String> {
return Observable.just(page.imageUrl!!) override fun chapterListRequest(manga: SManga): Request {
} return chapterListRequest(manga, 1)
}
override fun imageUrlParse(response: Response): String = ""
private fun chapterListRequest(manga: SManga, page: Int): Request {
private fun getReaderToken(response: Response): String { val id = manga.url.substringAfterLast("/")
val document = response.asJsoup() return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", catalogHeaders)
// The pages API needs the token provided in the reader script. }
val scriptSrc = document.select("script[src*=\"reader.min.js\"]")?.first()?.attr("src") ?: ""
return "token=(.*)&id".toRegex().find(scriptSrc)?.groupValues?.get(1) ?: "" override fun chapterListParse(response: Response): List<SChapter> {
} val result = jsonParser.parse(response.body()!!.string()).obj
companion object { if (!result["chapters"]!!.isJsonArray)
val jsonParser by lazy { return emptyList()
JsonParser()
return result.getAsJsonArray("chapters")?.map {
chapterListItemParse(it.obj)
} ?: emptyList()
}
private fun chapterListItemParse(obj: JsonObject): SChapter {
val scan = obj["releases"]!!.asJsonObject!!.entrySet().first().value.obj
val cName = obj["chapter_name"]!!.asString
return SChapter.create().apply {
name = "Cap. ${obj["number"]!!.asString}" + (if (cName == "") "" else " - $cName")
date_upload = parseChapterDate(obj["date"].nullString)
scanlator = scan["scanlators"]!!.asJsonArray.get(0)!!.asJsonObject["name"].nullString
url = scan["link"]!!.nullString ?: ""
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat()
}
}
private fun parseChapterDate(date: String?) : Long {
return try {
SimpleDateFormat("dd/MM/yy", Locale.ENGLISH).parse(date).time
} catch (e: ParseException) {
0L
}
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.flatMap { response ->
val token = getReaderToken(response)
return@flatMap if (token == "")
Observable.error(Exception("Licensed - No chapter to show"))
else fetchPageListApi(chapter, token)
}
}
private fun fetchPageListApi(chapter: SChapter, token: String): Observable<List<Page>> {
val id = "\\/(\\d+)\\/capitulo".toRegex().find(chapter.url)?.groupValues?.get(1) ?: ""
return client.newCall(pageListApiRequest(id, token))
.asObservableSuccess()
.map { response ->
pageListParse(response)
}
}
private fun pageListApiRequest(id: String, token: String): Request {
return GET("$baseUrl/leitor/pages/$id.json?key=$token", catalogHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val result = jsonParser.parse(response.body()!!.string()).obj
return result["images"]!!.asJsonArray
.mapIndexed { i, obj ->
Page(i, obj.asString, obj.asString)
}
}
override fun fetchImageUrl(page: Page): Observable<String> {
return Observable.just(page.imageUrl!!)
}
override fun imageUrlParse(response: Response): String = ""
private fun getReaderToken(response: Response): String {
val document = response.asJsoup()
// The pages API needs the token provided in the reader script.
val scriptSrc = document.select("script[src*=\"reader.min.js\"]")?.first()?.attr("src") ?: ""
return "token=(.*)&id".toRegex().find(scriptSrc)?.groupValues?.get(1) ?: ""
}
companion object {
val jsonParser by lazy {
JsonParser()
}
} }
}
} }

View File

@ -5,7 +5,7 @@ ext {
appName = 'Tachiyomi: Union Mangás' appName = 'Tachiyomi: Union Mangás'
pkgNameSuffix = 'pt.unionmangas' pkgNameSuffix = 'pt.unionmangas'
extClass = '.UnionMangas' extClass = '.UnionMangas'
extVersionCode = 4 extVersionCode = 5
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -21,21 +21,19 @@ class UnionMangas : ParsedHttpSource() {
override val name = "Union Mangás" override val name = "Union Mangás"
override val baseUrl = "http://unionmangas.top" override val baseUrl = "https://unionmangas.top"
override val lang = "pt" override val lang = "pt"
override val supportsLatest = true override val supportsLatest = true
// Sometimes the site it's very slow... // Sometimes the site is very slow.
override val client = override val client =
network.client.newBuilder() network.client.newBuilder()
.connectTimeout(3, TimeUnit.MINUTES) .connectTimeout(3, TimeUnit.MINUTES)
.readTimeout(3, TimeUnit.MINUTES) .readTimeout(3, TimeUnit.MINUTES)
.writeTimeout(3, TimeUnit.MINUTES) .writeTimeout(3, TimeUnit.MINUTES)
.build() .build()
private val langRegex: String = "( )?\\(Pt-Br\\)"
private val catalogHeaders = Headers.Builder().apply { private val catalogHeaders = Headers.Builder().apply {
add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)") add("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64)")
@ -55,7 +53,7 @@ class UnionMangas : ParsedHttpSource() {
manga.thumbnail_url = element.select("a img").first()?.attr("src") manga.thumbnail_url = element.select("a img").first()?.attr("src")
element.select("a").last().let { element.select("a").last().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().replace(langRegex.toRegex(), "") manga.title = it.text().replace(LANG_REGEX.toRegex(), "")
} }
return manga return manga
@ -63,7 +61,7 @@ class UnionMangas : ParsedHttpSource() {
override fun popularMangaNextPageSelector() = ".pagination li:contains(Next)" override fun popularMangaNextPageSelector() = ".pagination li:contains(Next)"
override fun latestUpdatesSelector() = "div.row[style=margin-bottom: 10px;] > div.col-md-12" override fun latestUpdatesSelector() = "div.row[style] div.col-md-12[style]"
override fun latestUpdatesRequest(page: Int): Request { override fun latestUpdatesRequest(page: Int): Request {
val form = FormBody.Builder().apply { val form = FormBody.Builder().apply {
@ -74,24 +72,33 @@ class UnionMangas : ParsedHttpSource() {
} }
override fun latestUpdatesFromElement(element: Element): SManga { override fun latestUpdatesFromElement(element: Element): SManga {
return popularMangaFromElement(element) // return popularMangaFromElement(element)
val manga = SManga.create()
val infoElements = element.select("a.link-titulo")
manga.thumbnail_url = infoElements.first()?.select("img")?.attr("src")
infoElements.last().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().replace(LANG_REGEX.toRegex(), "")
}
return manga
} }
override fun latestUpdatesNextPageSelector() = "div#linha-botao-mais" override fun latestUpdatesNextPageSelector() = "div#linha-botao-mais"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
return GET("$baseUrl/busca/$query/$page", headers) return GET("$baseUrl/busca/$query/$page", headers)
} }
override fun searchMangaSelector() = ".bloco-manga" override fun searchMangaSelector() = ".bloco-manga"
override fun searchMangaFromElement(element: Element): SManga { override fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create() val manga = SManga.create()
val thumbnailElement = element.select("a img.img-thumbnail").first()
manga.thumbnail_url = element.select("a img.img-thumbnail").first().attr("src") manga.thumbnail_url = thumbnailElement.attr("src").replace("com.br", "top")
element.select("a").last().let { element.select("a").last().let {
manga.setUrlWithoutDomain(it.attr("href")) manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().replace(langRegex.toRegex(), "") manga.title = it.text().replace(LANG_REGEX.toRegex(), "")
} }
return manga return manga
@ -119,7 +126,7 @@ class UnionMangas : ParsedHttpSource() {
manga.thumbnail_url = infoElement.select(".img-thumbnail").first()?.attr("src") manga.thumbnail_url = infoElement.select(".img-thumbnail").first()?.attr("src")
// Need to grab title again because the ellipsize in search. // Need to grab title again because the ellipsize in search.
manga.title = infoElement.select("h2").first()!!.text().replace(langRegex.toRegex(), "") manga.title = infoElement.select("h2").first()!!.text().replace(LANG_REGEX.toRegex(), "")
return manga return manga
} }
@ -166,4 +173,8 @@ class UnionMangas : ParsedHttpSource() {
} }
override fun imageUrlParse(document: Document) = "" override fun imageUrlParse(document: Document) = ""
companion object {
private const val LANG_REGEX = "( )?\\(Pt-Br\\)"
}
} }