Unites the MangaLivre source with MangasProject (#1216)

* Unite the MangaLivre source with the mangásPROJECT source.

* Remove blank line.
This commit is contained in:
Alessandro Jean 2019-06-29 12:33:13 -03:00 committed by Eugene
parent 30cb878aa0
commit 1d89d9f3ee
5 changed files with 205 additions and 460 deletions

View File

@ -5,13 +5,8 @@ ext {
appName = 'Tachiyomi: Mangá Livre' appName = 'Tachiyomi: Mangá Livre'
pkgNameSuffix = 'pt.mangalivre' pkgNameSuffix = 'pt.mangalivre'
extClass = '.MangaLivre' extClass = '.MangaLivre'
extVersionCode = 3 extVersionCode = 4
libVersion = '1.2' 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" apply from: "$rootDir/common.gradle"

View File

@ -1,25 +1,9 @@
package eu.kanade.tachiyomi.extension.pt.mangalivre 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.model.*
import eu.kanade.tachiyomi.source.online.HttpSource 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.Response
import rx.Observable
import java.lang.Exception import java.lang.Exception
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
class MangaLivre : HttpSource() { class MangaLivre : HttpSource() {
override val name = "MangaLivre" override val name = "MangaLivre"
@ -30,273 +14,18 @@ class MangaLivre : HttpSource() {
override val supportsLatest = true override val supportsLatest = true
// Sometimes the site is slow. override fun popularMangaRequest(page: Int) = throw Exception(NEED_MIGRATION)
override val client = override fun popularMangaParse(response: Response) = throw Exception(NEED_MIGRATION)
network.client.newBuilder() override fun latestUpdatesRequest(page: Int) = throw Exception(NEED_MIGRATION)
.connectTimeout(1, TimeUnit.MINUTES) override fun latestUpdatesParse(response: Response) = throw Exception(NEED_MIGRATION)
.readTimeout(1, TimeUnit.MINUTES) override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = throw Exception(NEED_MIGRATION)
.writeTimeout(1, TimeUnit.MINUTES) override fun searchMangaParse(response: Response) = throw Exception(NEED_MIGRATION)
.build() override fun mangaDetailsParse(response: Response) = throw Exception(NEED_MIGRATION)
override fun chapterListParse(response: Response) = throw Exception(NEED_MIGRATION)
private val catalogHeaders = Headers.Builder().apply { override fun pageListParse(response: Response) = throw Exception(NEED_MIGRATION)
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") override fun imageUrlParse(response: Response) = throw Exception(NEED_MIGRATION)
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) ?: ""
}
companion object { companion object {
val jsonParser by lazy { private const val NEED_MIGRATION = "Catálogo incorporado na nova versão da extensão mangásPROJECT."
JsonParser()
}
} }
} }

View File

@ -4,8 +4,8 @@ apply plugin: 'kotlin-android'
ext { ext {
appName = 'Tachiyomi: mangásPROJECT' appName = 'Tachiyomi: mangásPROJECT'
pkgNameSuffix = 'pt.mangasproject' pkgNameSuffix = 'pt.mangasproject'
extClass = '.MangasProject' extClass = '.MangasProjectFactory'
extVersionCode = 3 extVersionCode = 4
libVersion = '1.2' libVersion = '1.2'
} }

View File

@ -1,19 +1,17 @@
package eu.kanade.tachiyomi.extension.pt.mangasproject 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.obj
import com.github.salomonbrys.kotson.string
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.* import eu.kanade.tachiyomi.source.model.*
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.asJsoup import eu.kanade.tachiyomi.util.asJsoup
import okhttp3.FormBody import okhttp3.*
import okhttp3.Headers import org.jsoup.nodes.Element
import okhttp3.Request
import okhttp3.Response
import rx.Observable import rx.Observable
import java.lang.Exception import java.lang.Exception
import java.text.ParseException import java.text.ParseException
@ -21,222 +19,217 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class MangasProject : HttpSource() { abstract class MangasProject(override val name: String,
override val name = "mangásPROJECT" override val baseUrl: String) : HttpSource() {
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 = override val client = network.client.newBuilder()
network.client.newBuilder()
.connectTimeout(1, TimeUnit.MINUTES) .connectTimeout(1, TimeUnit.MINUTES)
.readTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.MINUTES)
.writeTimeout(1, TimeUnit.MINUTES) .writeTimeout(1, TimeUnit.MINUTES)
.addInterceptor { pageListIntercept(it) }
.build() .build()
private val catalogHeaders = Headers.Builder().apply { override fun headersBuilder(): Headers.Builder = Headers.Builder()
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", USER_AGENT)
add("Host", "leitor.net") .add("Referer", baseUrl)
// The API doesn't return the result if this header isn't sent. .add("X-Requested-With", "XMLHttpRequest")
add("X-Requested-With", "XMLHttpRequest")
}.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", headers)
} }
override fun popularMangaParse(response: Response): MangasPage { 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 "most_read" have boolean false value, then it doesn't have next page.
if (!result["most_read"]!!.isJsonArray) if (!result["most_read"]!!.isJsonArray)
return MangasPage(emptyList(), false) return MangasPage(emptyList(), false)
val popularMangas = result.getAsJsonArray("most_read")?.map { val popularMangas = result["most_read"].array
popularMangaItemParse(it.obj) .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, page < 10)
return MangasPage(popularMangas, hasNextPage)
return MangasPage(emptyList(), false)
} }
private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun popularMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["serie_name"].nullString ?: "" title = obj["serie_name"].string
thumbnail_url = obj["cover"].nullString thumbnail_url = obj["cover"].string
url = obj["link"].nullString ?: "" url = obj["link"].string
} }
override fun latestUpdatesRequest(page: Int): Request { 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 { override fun latestUpdatesParse(response: Response): MangasPage {
if (response.code() == 500) if (response.code() == 500)
return MangasPage(emptyList(), false) return MangasPage(emptyList(), false)
val result = jsonParser.parse(response.body()!!.string()).obj val result = response.asJsonObject()
val latestMangas = result.getAsJsonArray("releases")?.map { val latestMangas = result["releases"].array
latestMangaItemParse(it.obj) .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, page < 5)
return MangasPage(latestMangas, hasNextPage)
return MangasPage(emptyList(), false)
} }
private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun latestMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["name"].nullString ?: "" title = obj["name"].string
thumbnail_url = obj["image"].nullString thumbnail_url = obj["image"].string
url = obj["link"].nullString ?: "" url = obj["link"].string
} }
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val form = FormBody.Builder().apply { val form = FormBody.Builder()
add("search", query) .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 { 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 "series" have boolean false value, then it doesn't have results.
if (!result["series"]!!.isJsonArray) if (!result["series"]!!.isJsonArray)
return MangasPage(emptyList(), false) return MangasPage(emptyList(), false)
val searchMangas = result.getAsJsonArray("series")?.map { val searchMangas = result["series"].array
searchMangaItemParse(it.obj) .map { searchMangaItemParse(it.obj) }
}
if (searchMangas != null)
return MangasPage(searchMangas, false) return MangasPage(searchMangas, false)
return MangasPage(emptyList(), false)
} }
private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply { private fun searchMangaItemParse(obj: JsonObject) = SManga.create().apply {
title = obj["name"].nullString ?: "" title = obj["name"].string
thumbnail_url = obj["cover"].nullString thumbnail_url = obj["cover"].string
url = obj["link"].nullString ?: "" url = obj["link"].string
author = obj["author"].nullString author = obj["author"].string
artist = obj["artist"].nullString 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 { override fun mangaDetailsParse(response: Response): SManga {
val document = response.asJsoup() 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() val seriesData = document.select("#series-data")
.substringAfter("Completo").substringBefore("+")
val authors = seriesAuthor.split("&") val isCompleted = seriesData.select("span.series-author i.complete-series").first() != null
.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. // Check if the manga was removed by the publisher.
var seriesBlocked = document.select("div.series-blocked-img").first() val seriesBlocked = document.select("div.series-blocked-img").first()
val cStatus = when {
seriesBlocked == null && isCompleted -> SManga.COMPLETED val seriesAuthors = document.select("div#series-data span.series-author").text()
seriesBlocked == null && !isCompleted -> SManga.ONGOING .substringAfter("Completo")
else -> SManga.LICENSED .substringBefore("+")
.split("&")
.map { it.trim() }
val seriesAuthor = seriesAuthors
.filter { !it.contains("(Arte)") }
.joinToString("; ") {
it.split(", ")
.reversed()
.joinToString(" ")
} }
return SManga.create().apply { return SManga.create().apply {
genre = cGenre thumbnail_url = seriesData.select("div.series-img > div.cover > img").attr("src")
status = cStatus description = seriesData.select("span.series-desc").text()
description = document.select("div#series-data span.series-desc").first()?.text()
author = cAuthor.joinToString("; ") status = parseStatus(seriesBlocked, isCompleted)
artist = if (cArtist.isEmpty()) cAuthor.joinToString("; ") else cArtist.joinToString("; ") 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. private fun parseStatus(seriesBlocked: Element?, isCompleted: Boolean) = when {
// Adapted from: seriesBlocked != null -> SManga.LICENSED
// https://stackoverflow.com/questions/35254323/rxjs-observable-pagination isCompleted -> SManga.COMPLETED
// https://stackoverflow.com/questions/40529232/angular-2-http-observables-and-recursive-requests else -> SManga.ONGOING
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> { override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return if (manga.status != SManga.LICENSED) { if (manga.status != SManga.LICENSED)
fetchChapterList(manga, 1) return super.fetchChapterList(manga)
} else {
Observable.error(Exception("Licensed - No chapters to show"))
}
}
private fun fetchChapterList(manga: SManga, page: Int, return Observable.error(Exception("Mangá licenciado e removido pela editora."))
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 { 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 { private fun chapterListRequestPaginated(mangaUrl: String, id: String, page: Int): Request {
val id = manga.url.substringAfterLast("/") val newHeaders = headersBuilder()
return GET("$baseUrl/series/chapters_list.json?page=$page&id_serie=$id", catalogHeaders) .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> { override fun chapterListParse(response: Response): List<SChapter> {
val result = jsonParser.parse(response.body()!!.string()).obj var result = response.asJsonObject()
if (!result["chapters"]!!.isJsonArray) if (!result["chapters"]!!.isJsonArray)
return emptyList() return emptyList()
return result.getAsJsonArray("chapters")?.map { val mangaUrl = response.request().header("Referer")!!
chapterListItemParse(it.obj) val mangaId = mangaUrl.substringAfterLast("/")
} ?: emptyList() 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 { private fun chapterListItemParse(obj: JsonObject): SChapter {
val scan = obj["releases"]!!.asJsonObject!!.entrySet().first().value.obj val scan = obj["releases"].obj.entrySet().first().value.obj
val cName = obj["chapter_name"]!!.asString val chapterName = obj["chapter_name"]!!.string
val scanlators = scan["scanlators"]!!.asJsonArray
.joinToString { it.asJsonObject["name"].asString }
return SChapter.create().apply { return SChapter.create().apply {
name = "Cap. ${obj["number"]!!.asString}" + (if (cName == "") "" else " - $cName") name = "Cap. ${obj["number"].string}" + (if (chapterName == "") "" else " - $chapterName")
date_upload = parseChapterDate(obj["date_created"].asString.substring(0, 10)) date_upload = parseChapterDate(obj["date_created"].string.substringBefore("T"))
scanlator = scanlators scanlator = scan["scanlators"]!!.array
url = scan["link"]!!.nullString ?: "" .joinToString { it.obj["name"].string }
chapter_number = obj["number"]!!.asString.toFloatOrNull() ?: "1".toFloat() 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>> { override fun pageListRequest(chapter: SChapter): Request {
return client.newCall(pageListRequest(chapter)) val newHeaders = Headers.Builder()
.asObservableSuccess() .add("User-Agent", USER_AGENT)
.flatMap { response -> .add("Referer", baseUrl + chapter.url)
val token = getReaderToken(response) .build()
return@flatMap if (token == "")
Observable.error(Exception("Licensed - No chapter to show")) return GET(baseUrl + chapter.url, newHeaders)
else fetchPageListApi(chapter, token)
}
} }
private fun fetchPageListApi(chapter: SChapter, token: String): Observable<List<Page>> { private fun pageListApiRequest(chapterUrl: String, token: String): Request {
val id = "\\/(\\d+)\\/capitulo".toRegex().find(chapter.url)?.groupValues?.get(1) ?: "" val newHeaders = headersBuilder()
return client.newCall(pageListApiRequest(id, token)) .set("Referer", chapterUrl)
.asObservableSuccess() .build()
.map { response ->
pageListParse(response) val id = chapterUrl
} .substringBeforeLast("/")
.substringAfterLast("/")
return GET("$baseUrl/leitor/pages/$id.json?key=$token", newHeaders)
} }
private fun pageListApiRequest(id: String, token: String): Request { private fun pageListIntercept(chain: Interceptor.Chain): Response {
return GET("$baseUrl/leitor/pages/$id.json?key=$token", catalogHeaders) 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> { override fun pageListParse(response: Response): List<Page> {
val result = jsonParser.parse(response.body()!!.string()).obj val result = response.asJsonObject()
return result["images"]!!.asJsonArray return result["images"].array
.mapIndexed { i, obj -> .filter { it.string.startsWith("http") }
Page(i, obj.asString, obj.asString) .mapIndexed { i, obj -> Page(i, "", obj.string)}
}
} }
override fun fetchImageUrl(page: Page): Observable<String> { override fun fetchImageUrl(page: Page): Observable<String> {
@ -287,16 +295,13 @@ class MangasProject : HttpSource() {
override fun imageUrlParse(response: Response): String = "" override fun imageUrlParse(response: Response): String = ""
private fun getReaderToken(response: Response): String { private fun Response.asJsonObject(): JsonObject = JSON_PARSER.parse(body()!!.string()).obj
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 { companion object {
val jsonParser by lazy { 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"
JsonParser()
} private val TOKEN_REGEX = "token=(.*)&id".toRegex()
private val JSON_PARSER by lazy { JsonParser() }
} }
} }

View File

@ -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()
)