Unites the MangaLivre source with MangasProject (#1216)
* Unite the MangaLivre source with the mangásPROJECT source. * Remove blank line.
This commit is contained in:
parent
30cb878aa0
commit
1d89d9f3ee
|
@ -5,13 +5,8 @@ ext {
|
|||
appName = 'Tachiyomi: Mangá Livre'
|
||||
pkgNameSuffix = 'pt.mangalivre'
|
||||
extClass = '.MangaLivre'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.google.code.gson:gson:2.8.2'
|
||||
compileOnly 'com.github.salomonbrys.kotson:kotson:2.5.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -1,25 +1,9 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.mangalivre
|
||||
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import java.lang.Exception
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MangaLivre : HttpSource() {
|
||||
override val name = "MangaLivre"
|
||||
|
@ -30,273 +14,18 @@ class MangaLivre : HttpSource() {
|
|||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Sometimes the site is slow.
|
||||
override val client =
|
||||
network.client.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
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("Host", "mangalivre.com")
|
||||
// The API doesn't return the result if this header isn't sent.
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
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
|
||||
|
||||
if (popularMangas != null)
|
||||
return MangasPage(popularMangas, hasNextPage)
|
||||
|
||||
return MangasPage(emptyList(), false)
|
||||
}
|
||||
|
||||
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["serie_name"].nullString ?: ""
|
||||
thumbnail_url = obj["cover"].nullString
|
||||
url = obj["link"].nullString ?: ""
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/home/releases?page=$page", catalogHeaders)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
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
|
||||
|
||||
if (latestMangas != null)
|
||||
return MangasPage(latestMangas, hasNextPage)
|
||||
|
||||
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 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)
|
||||
return MangasPage(searchMangas, false)
|
||||
|
||||
return MangasPage(emptyList(), false)
|
||||
}
|
||||
|
||||
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].nullString ?: ""
|
||||
thumbnail_url = obj["cover"].nullString
|
||||
url = obj["link"].nullString ?: ""
|
||||
author = obj["author"].nullString
|
||||
artist = obj["artist"].nullString
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
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 = document.select("div#series-data span.series-author").text()
|
||||
.substringAfter("Completo").substringBefore("+")
|
||||
|
||||
val authors = seriesAuthor.split("&")
|
||||
.map { it.trim() }
|
||||
|
||||
val cAuthor = authors.filter { !it.contains("(Arte)") }
|
||||
.map { it.split(", ").reversed().joinToString(" ") }
|
||||
|
||||
val cArtist = authors.filter { it.contains("(Arte)") }
|
||||
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
|
||||
.map { it.split(", ").reversed().joinToString(" ") }
|
||||
|
||||
// Check if the manga was removed by the publisher.
|
||||
var seriesBlocked = document.select("div.series-blocked-img").first()
|
||||
val cStatus = when {
|
||||
seriesBlocked == null && isCompleted -> SManga.COMPLETED
|
||||
seriesBlocked == null && !isCompleted -> 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("; ")
|
||||
}
|
||||
}
|
||||
|
||||
// Need to override because the chapter API is paginated.
|
||||
// Adapted from:
|
||||
// https://stackoverflow.com/questions/35254323/rxjs-observable-pagination
|
||||
// https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
fetchChapterList(manga, 1)
|
||||
} else {
|
||||
Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchChapterList(manga: SManga, page: Int,
|
||||
pastChapters: List<SChapter> = emptyList()): Observable<List<SChapter>> {
|
||||
val chapters = pastChapters.toMutableList()
|
||||
return fetchChapterListPage(manga, page)
|
||||
.flatMap {
|
||||
chapters += it
|
||||
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
|
||||
|
||||
val scanlators = scan["scanlators"]!!.asJsonArray
|
||||
.joinToString { it.asJsonObject["name"].asString }
|
||||
|
||||
return SChapter.create().apply {
|
||||
name = "Cap. ${obj["number"]!!.asString}" + (if (cName == "") "" else " - $cName")
|
||||
date_upload = parseChapterDate(obj["date_created"].asString.substring(0, 10))
|
||||
scanlator = scanlators
|
||||
url = scan["link"]!!.nullString ?: ""
|
||||
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChapterDate(date: String?) : Long {
|
||||
return try {
|
||||
SimpleDateFormat("yyyy-MM-dd", 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) ?: ""
|
||||
}
|
||||
override fun popularMangaRequest(page: Int) = throw Exception(NEED_MIGRATION)
|
||||
override fun popularMangaParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
override fun latestUpdatesRequest(page: Int) = throw Exception(NEED_MIGRATION)
|
||||
override fun latestUpdatesParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw Exception(NEED_MIGRATION)
|
||||
override fun searchMangaParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
override fun mangaDetailsParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
override fun chapterListParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
override fun pageListParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
override fun imageUrlParse(response: Response) = throw Exception(NEED_MIGRATION)
|
||||
|
||||
companion object {
|
||||
val jsonParser by lazy {
|
||||
JsonParser()
|
||||
}
|
||||
private const val NEED_MIGRATION = "Catálogo incorporado na nova versão da extensão mangásPROJECT."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ apply plugin: 'kotlin-android'
|
|||
ext {
|
||||
appName = 'Tachiyomi: mangásPROJECT'
|
||||
pkgNameSuffix = 'pt.mangasproject'
|
||||
extClass = '.MangasProject'
|
||||
extVersionCode = 3
|
||||
extClass = '.MangasProjectFactory'
|
||||
extVersionCode = 4
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.mangasproject
|
||||
|
||||
import com.github.salomonbrys.kotson.nullString
|
||||
import com.github.salomonbrys.kotson.array
|
||||
import com.github.salomonbrys.kotson.obj
|
||||
import com.github.salomonbrys.kotson.string
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.*
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.*
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import java.lang.Exception
|
||||
import java.text.ParseException
|
||||
|
@ -21,222 +19,217 @@ import java.text.SimpleDateFormat
|
|||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MangasProject : HttpSource() {
|
||||
override val name = "mangásPROJECT"
|
||||
|
||||
override val baseUrl = "https://leitor.net"
|
||||
|
||||
abstract class MangasProject(override val name: String,
|
||||
override val baseUrl: String) : HttpSource() {
|
||||
override val lang = "pt"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
// Sometimes the site is slow.
|
||||
override val client =
|
||||
network.client.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.build()
|
||||
override val client = network.client.newBuilder()
|
||||
.connectTimeout(1, TimeUnit.MINUTES)
|
||||
.readTimeout(1, TimeUnit.MINUTES)
|
||||
.writeTimeout(1, TimeUnit.MINUTES)
|
||||
.addInterceptor { pageListIntercept(it) }
|
||||
.build()
|
||||
|
||||
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("Host", "leitor.net")
|
||||
// The API doesn't return the result if this header isn't sent.
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
}.build()
|
||||
override fun headersBuilder(): Headers.Builder = Headers.Builder()
|
||||
.add("User-Agent", USER_AGENT)
|
||||
.add("Referer", baseUrl)
|
||||
.add("X-Requested-With", "XMLHttpRequest")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
return GET("$baseUrl/home/most_read?page=$page", catalogHeaders)
|
||||
return GET("$baseUrl/home/most_read?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val result = jsonParser.parse(response.body()!!.string()).obj
|
||||
val result = response.asJsonObject()
|
||||
|
||||
// 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 popularMangas = result["most_read"].array
|
||||
.map { popularMangaItemParse(it.obj) }
|
||||
|
||||
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 10
|
||||
val page = response.request().url().queryParameter("page")!!.toInt()
|
||||
|
||||
if (popularMangas != null)
|
||||
return MangasPage(popularMangas, hasNextPage)
|
||||
|
||||
return MangasPage(emptyList(), false)
|
||||
return MangasPage(popularMangas, page < 10)
|
||||
}
|
||||
|
||||
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["serie_name"].nullString ?: ""
|
||||
thumbnail_url = obj["cover"].nullString
|
||||
url = obj["link"].nullString ?: ""
|
||||
title = obj["serie_name"].string
|
||||
thumbnail_url = obj["cover"].string
|
||||
url = obj["link"].string
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request {
|
||||
return GET("$baseUrl/home/releases?page=$page", catalogHeaders)
|
||||
return GET("$baseUrl/home/releases?page=$page", headers)
|
||||
}
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
if (response.code() == 500)
|
||||
return MangasPage(emptyList(), false)
|
||||
|
||||
val result = jsonParser.parse(response.body()!!.string()).obj
|
||||
val result = response.asJsonObject()
|
||||
|
||||
val latestMangas = result.getAsJsonArray("releases")?.map {
|
||||
latestMangaItemParse(it.obj)
|
||||
}
|
||||
val latestMangas = result["releases"].array
|
||||
.map { latestMangaItemParse(it.obj) }
|
||||
|
||||
val hasNextPage = response.request().url().queryParameter("page")!!.toInt() < 5
|
||||
val page = response.request().url().queryParameter("page")!!.toInt()
|
||||
|
||||
if (latestMangas != null)
|
||||
return MangasPage(latestMangas, hasNextPage)
|
||||
|
||||
return MangasPage(emptyList(), false)
|
||||
return MangasPage(latestMangas, page < 5)
|
||||
}
|
||||
|
||||
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].nullString ?: ""
|
||||
thumbnail_url = obj["image"].nullString
|
||||
url = obj["link"].nullString ?: ""
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["image"].string
|
||||
url = obj["link"].string
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val form = FormBody.Builder().apply {
|
||||
add("search", query)
|
||||
}
|
||||
val form = FormBody.Builder()
|
||||
.add("search", query)
|
||||
.build()
|
||||
|
||||
return POST("$baseUrl/lib/search/series.json", catalogHeaders, form.build())
|
||||
return POST("$baseUrl/lib/search/series.json", headers, form)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val result = jsonParser.parse(response.body()!!.string()).obj
|
||||
val result = response.asJsonObject()
|
||||
|
||||
// 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)
|
||||
}
|
||||
val searchMangas = result["series"].array
|
||||
.map { searchMangaItemParse(it.obj) }
|
||||
|
||||
if (searchMangas != null)
|
||||
return MangasPage(searchMangas, false)
|
||||
|
||||
return MangasPage(emptyList(), false)
|
||||
return MangasPage(searchMangas, false)
|
||||
}
|
||||
|
||||
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
|
||||
title = obj["name"].nullString ?: ""
|
||||
thumbnail_url = obj["cover"].nullString
|
||||
url = obj["link"].nullString ?: ""
|
||||
author = obj["author"].nullString
|
||||
artist = obj["artist"].nullString
|
||||
title = obj["name"].string
|
||||
thumbnail_url = obj["cover"].string
|
||||
url = obj["link"].string
|
||||
author = obj["author"].string
|
||||
artist = obj["artist"].string
|
||||
}
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val newHeaders = Headers.Builder()
|
||||
.add("User-Agent", USER_AGENT)
|
||||
.add("Referer", baseUrl)
|
||||
.build()
|
||||
|
||||
return GET(baseUrl + manga.url, newHeaders)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response): SManga {
|
||||
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 = document.select("div#series-data span.series-author").text()
|
||||
.substringAfter("Completo").substringBefore("+")
|
||||
val seriesData = document.select("#series-data")
|
||||
|
||||
val authors = seriesAuthor.split("&")
|
||||
.map { it.trim() }
|
||||
|
||||
val cAuthor = authors.filter { !it.contains("(Arte)") }
|
||||
.map { it.split(", ").reversed().joinToString(" ") }
|
||||
|
||||
val cArtist = authors.filter { it.contains("(Arte)") }
|
||||
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
|
||||
.map { it.split(", ").reversed().joinToString(" ") }
|
||||
val isCompleted = seriesData.select("span.series-author i.complete-series").first() != null
|
||||
|
||||
// Check if the manga was removed by the publisher.
|
||||
var seriesBlocked = document.select("div.series-blocked-img").first()
|
||||
val cStatus = when {
|
||||
seriesBlocked == null && isCompleted -> SManga.COMPLETED
|
||||
seriesBlocked == null && !isCompleted -> SManga.ONGOING
|
||||
else -> SManga.LICENSED
|
||||
}
|
||||
val seriesBlocked = document.select("div.series-blocked-img").first()
|
||||
|
||||
val seriesAuthors = document.select("div#series-data span.series-author").text()
|
||||
.substringAfter("Completo")
|
||||
.substringBefore("+")
|
||||
.split("&")
|
||||
.map { it.trim() }
|
||||
|
||||
val seriesAuthor = seriesAuthors
|
||||
.filter { !it.contains("(Arte)") }
|
||||
.joinToString("; ") {
|
||||
it.split(", ")
|
||||
.reversed()
|
||||
.joinToString(" ")
|
||||
}
|
||||
|
||||
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("; ")
|
||||
thumbnail_url = seriesData.select("div.series-img > div.cover > img").attr("src")
|
||||
description = seriesData.select("span.series-desc").text()
|
||||
|
||||
status = parseStatus(seriesBlocked, isCompleted)
|
||||
author = seriesAuthor
|
||||
artist = seriesAuthors.filter { it.contains("(Arte)") }
|
||||
.map { it.replace("\\(Arte\\)".toRegex(), "").trim() }
|
||||
.joinToString("; ") {
|
||||
it.split(", ")
|
||||
.reversed()
|
||||
.joinToString(" ")
|
||||
}
|
||||
.ifEmpty { seriesAuthor }
|
||||
genre = seriesData.select("div#series-data ul.tags li")
|
||||
.joinToString { it.text() }
|
||||
}
|
||||
}
|
||||
|
||||
// Need to override because the chapter API is paginated.
|
||||
// Adapted from:
|
||||
// https://stackoverflow.com/questions/35254323/rxjs-observable-pagination
|
||||
// https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests
|
||||
private fun parseStatus(seriesBlocked: Element?, isCompleted: Boolean) = when {
|
||||
seriesBlocked != null -> SManga.LICENSED
|
||||
isCompleted -> SManga.COMPLETED
|
||||
else -> SManga.ONGOING
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
fetchChapterList(manga, 1)
|
||||
} else {
|
||||
Observable.error(Exception("Licensed - No chapters to show"))
|
||||
}
|
||||
}
|
||||
if (manga.status != SManga.LICENSED)
|
||||
return super.fetchChapterList(manga)
|
||||
|
||||
private fun fetchChapterList(manga: SManga, page: Int,
|
||||
pastChapters: List<SChapter> = emptyList()): Observable<List<SChapter>> {
|
||||
val chapters = pastChapters.toMutableList()
|
||||
return fetchChapterListPage(manga, page)
|
||||
.flatMap {
|
||||
chapters += it
|
||||
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)
|
||||
}
|
||||
return Observable.error(Exception("Mangá licenciado e removido pela editora."))
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
return chapterListRequest(manga, 1)
|
||||
val id = manga.url.substringAfterLast("/")
|
||||
|
||||
return chapterListRequestPaginated(manga.url, id, 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)
|
||||
private fun chapterListRequestPaginated(mangaUrl: String, id: String, page: Int): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", baseUrl + mangaUrl)
|
||||
.build()
|
||||
|
||||
return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", newHeaders)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
val result = jsonParser.parse(response.body()!!.string()).obj
|
||||
var result = response.asJsonObject()
|
||||
|
||||
if (!result["chapters"]!!.isJsonArray)
|
||||
return emptyList()
|
||||
|
||||
return result.getAsJsonArray("chapters")?.map {
|
||||
chapterListItemParse(it.obj)
|
||||
} ?: emptyList()
|
||||
val mangaUrl = response.request().header("Referer")!!
|
||||
val mangaId = mangaUrl.substringAfterLast("/")
|
||||
var page = 1
|
||||
|
||||
val chapters = mutableListOf<SChapter>()
|
||||
|
||||
while (result["chapters"]!!.isJsonArray) {
|
||||
chapters += result["chapters"].array
|
||||
.map { chapterListItemParse(it.obj) }
|
||||
.toMutableList()
|
||||
|
||||
val newRequest = chapterListRequestPaginated(mangaUrl, mangaId, ++page)
|
||||
result = client.newCall(newRequest).execute().asJsonObject()
|
||||
}
|
||||
|
||||
return chapters
|
||||
}
|
||||
|
||||
private fun chapterListItemParse(obj: JsonObject): SChapter {
|
||||
val scan = obj["releases"]!!.asJsonObject!!.entrySet().first().value.obj
|
||||
val cName = obj["chapter_name"]!!.asString
|
||||
|
||||
val scanlators = scan["scanlators"]!!.asJsonArray
|
||||
.joinToString { it.asJsonObject["name"].asString }
|
||||
val scan = obj["releases"].obj.entrySet().first().value.obj
|
||||
val chapterName = obj["chapter_name"]!!.string
|
||||
|
||||
return SChapter.create().apply {
|
||||
name = "Cap. ${obj["number"]!!.asString}" + (if (cName == "") "" else " - $cName")
|
||||
date_upload = parseChapterDate(obj["date_created"].asString.substring(0, 10))
|
||||
scanlator = scanlators
|
||||
url = scan["link"]!!.nullString ?: ""
|
||||
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat()
|
||||
name = "Cap. ${obj["number"].string}" + (if (chapterName == "") "" else " - $chapterName")
|
||||
date_upload = parseChapterDate(obj["date_created"].string.substringBefore("T"))
|
||||
scanlator = scan["scanlators"]!!.array
|
||||
.joinToString { it.obj["name"].string }
|
||||
url = scan["link"].string
|
||||
chapter_number = obj["number"].string.toFloatOrNull() ?: 0f
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -248,37 +241,52 @@ class MangasProject : HttpSource() {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val newHeaders = Headers.Builder()
|
||||
.add("User-Agent", USER_AGENT)
|
||||
.add("Referer", baseUrl + chapter.url)
|
||||
.build()
|
||||
|
||||
return GET(baseUrl + chapter.url, newHeaders)
|
||||
}
|
||||
|
||||
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(chapterUrl: String, token: String): Request {
|
||||
val newHeaders = headersBuilder()
|
||||
.set("Referer", chapterUrl)
|
||||
.build()
|
||||
|
||||
val id = chapterUrl
|
||||
.substringBeforeLast("/")
|
||||
.substringAfterLast("/")
|
||||
|
||||
return GET("$baseUrl/leitor/pages/$id.json?key=$token", newHeaders)
|
||||
}
|
||||
|
||||
private fun pageListApiRequest(id: String, token: String): Request {
|
||||
return GET("$baseUrl/leitor/pages/$id.json?key=$token", catalogHeaders)
|
||||
private fun pageListIntercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val result = chain.proceed(request)
|
||||
|
||||
if (!request.url().toString().contains("capitulo-"))
|
||||
return result
|
||||
|
||||
val document = result.asJsoup()
|
||||
val readerSrc = document.select("script[src*=\"reader.min.js\"]")
|
||||
?.attr("src") ?: ""
|
||||
|
||||
val token = TOKEN_REGEX.find(readerSrc)?.groupValues?.get(1) ?: ""
|
||||
|
||||
if (token.isEmpty())
|
||||
throw Exception("Mangá licenciado e removido pela editora.")
|
||||
|
||||
return chain.proceed(pageListApiRequest(request.url().toString(), token))
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val result = jsonParser.parse(response.body()!!.string()).obj
|
||||
val result = response.asJsonObject()
|
||||
|
||||
return result["images"]!!.asJsonArray
|
||||
.mapIndexed { i, obj ->
|
||||
Page(i, obj.asString, obj.asString)
|
||||
}
|
||||
return result["images"].array
|
||||
.filter { it.string.startsWith("http") }
|
||||
.mapIndexed { i, obj -> Page(i, "", obj.string)}
|
||||
}
|
||||
|
||||
override fun fetchImageUrl(page: Page): Observable<String> {
|
||||
|
@ -287,16 +295,13 @@ class MangasProject : HttpSource() {
|
|||
|
||||
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) ?: ""
|
||||
}
|
||||
private fun Response.asJsonObject(): JsonObject = JSON_PARSER.parse(body()!!.string()).obj
|
||||
|
||||
companion object {
|
||||
val jsonParser by lazy {
|
||||
JsonParser()
|
||||
}
|
||||
private const val 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"
|
||||
|
||||
private val TOKEN_REGEX = "token=(.*)&id".toRegex()
|
||||
|
||||
private val JSON_PARSER by lazy { JsonParser() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package eu.kanade.tachiyomi.extension.pt.mangasproject
|
||||
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceFactory
|
||||
|
||||
class MangasProjectFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> = getAllSources()
|
||||
}
|
||||
|
||||
class MangasProjectOriginal : MangasProject("mangásPROJECT", "https://leitor.net")
|
||||
class MangaLivre : MangasProject("MangaLivre", "https://mangalivre.com")
|
||||
|
||||
fun getAllSources(): List<Source> = listOf(
|
||||
MangasProjectOriginal(),
|
||||
MangaLivre()
|
||||
)
|
Loading…
Reference in New Issue