diff --git a/src/pt/argosscan/build.gradle b/src/pt/argosscan/build.gradle index 2343dfcbc..8b5d1bf50 100644 --- a/src/pt/argosscan/build.gradle +++ b/src/pt/argosscan/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Argos Scan' pkgNameSuffix = 'pt.argosscan' extClass = '.ArgosScan' - extVersionCode = 18 + extVersionCode = 19 } dependencies { diff --git a/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt b/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt index 9dad4cb9b..64a62bded 100644 --- a/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt +++ b/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScan.kt @@ -16,7 +16,9 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.add import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.jsonArray @@ -83,14 +85,13 @@ class ArgosScan : HttpSource(), ConfigurableSource { } override fun popularMangaParse(response: Response): MangasPage { - val result = json.parseToJsonElement(response.body!!.string()).jsonObject + val result = response.parseAs>() - if (result["errors"] != null) { + if (result.data == null) { throw Exception(REQUEST_ERROR) } - val projectList = result["data"]!!.jsonObject["getProjects"]!! - .let { json.decodeFromJsonElement(it) } + val projectList = result.data["getProjects"]!! val mangaList = projectList.projects .map(::genericMangaFromObject) @@ -155,36 +156,32 @@ class ArgosScan : HttpSource(), ConfigurableSource { } override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { - val result = json.parseToJsonElement(response.body!!.string()).jsonObject + val result = response.parseAs>() - if (result["errors"] != null) { + if (result.data == null) { throw Exception(REQUEST_ERROR) } - val project = result["data"]!!.jsonObject["project"]!!.jsonObject - .let { json.decodeFromJsonElement(it) } + val project = result.data["project"]!! title = project.name!! thumbnail_url = "$baseUrl/images/${project.id}/${project.cover!!}" description = project.description.orEmpty() - author = project.authors.orEmpty().joinToString(", ") + author = project.authors.orEmpty().joinToString() status = SManga.ONGOING - genre = project.tags.orEmpty().joinToString(", ") { it.name } + genre = project.tags.orEmpty().sortedBy(ArgosTagDto::name).joinToString { it.name } } override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga) override fun chapterListParse(response: Response): List { - val result = json.parseToJsonElement(response.body!!.string()).jsonObject + val result = response.parseAs>() - if (result["errors"] != null) { + if (result.data == null) { throw Exception(REQUEST_ERROR) } - val project = result["data"]!!.jsonObject["project"]!!.jsonObject - .let { json.decodeFromJsonElement(it) } - - return project.chapters.map(::chapterFromObject) + return result.data["project"]!!.chapters.map(::chapterFromObject) } private fun chapterFromObject(chapter: ArgosChapterDto): SChapter = SChapter.create().apply { @@ -211,7 +208,7 @@ class ArgosScan : HttpSource(), ConfigurableSource { } override fun pageListParse(response: Response): List { - val result = json.parseToJsonElement(response.body!!.string()).jsonObject + val result = response.parseAs().jsonObject if (result["errors"] != null) { throw Exception(REQUEST_ERROR) @@ -345,6 +342,10 @@ class ArgosScan : HttpSource(), ConfigurableSource { return POST(GRAPHQL_URL, newHeaders, body) } + private inline fun Response.parseAs(): T = use { + json.decodeFromString(it.body?.string().orEmpty()) + } + private fun String.toDate(): Long { return runCatching { DATE_PARSER.parse(this)?.time } .getOrNull() ?: 0L diff --git a/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScanDto.kt b/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScanDto.kt index 65a3f5b1d..3475c62f4 100644 --- a/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScanDto.kt +++ b/src/pt/argosscan/src/eu/kanade/tachiyomi/extension/pt/argosscan/ArgosScanDto.kt @@ -3,6 +3,11 @@ package eu.kanade.tachiyomi.extension.pt.argosscan import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +@Serializable +data class ArgosResponseDto( + val data: Map? = null +) + @Serializable data class ArgosProjectListDto( val count: Int = 0, diff --git a/src/pt/hipercool/build.gradle b/src/pt/hipercool/build.gradle index cd33def3a..bec809c15 100644 --- a/src/pt/hipercool/build.gradle +++ b/src/pt/hipercool/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'HipercooL' pkgNameSuffix = 'pt.hipercool' extClass = '.Hipercool' - extVersionCode = 8 + extVersionCode = 9 isNsfw = true } diff --git a/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt b/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt index aaa225b8e..2acbc6eb0 100644 --- a/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt +++ b/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/Hipercool.kt @@ -1,6 +1,6 @@ package eu.kanade.tachiyomi.extension.pt.hipercool -import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor +import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess @@ -11,23 +11,21 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import rx.Observable import uy.kohesive.injekt.injectLazy -import java.text.ParseException import java.text.SimpleDateFormat import java.util.Locale -import java.util.concurrent.TimeUnit class Hipercool : HttpSource() { @@ -43,7 +41,8 @@ class Hipercool : HttpSource() { override val supportsLatest = true override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .addInterceptor(RateLimitInterceptor(1, 2, TimeUnit.SECONDS)) + .addInterceptor(SpecificHostRateLimitInterceptor(baseUrl.toHttpUrl(), 1, 2)) + .addInterceptor(SpecificHostRateLimitInterceptor(STATIC_URL.toHttpUrl(), 1, 1)) .build() override fun headersBuilder(): Headers.Builder = Headers.Builder() @@ -54,14 +53,14 @@ class Hipercool : HttpSource() { private val json: Json by injectLazy() private fun genericMangaListParse(response: Response): MangasPage { - val chapters = json.decodeFromString>(response.body!!.string()) + val chapters = response.parseAs>() if (chapters.isEmpty()) return MangasPage(emptyList(), false) val mangaList = chapters + .distinctBy { it.book!!.title } .map(::genericMangaFromObject) - .distinctBy { it.title } val hasNextPage = chapters.size == DEFAULT_COUNT @@ -87,17 +86,14 @@ class Hipercool : HttpSource() { override fun latestUpdatesParse(response: Response): MangasPage = genericMangaListParse(response) override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val mediaType = "application/json; charset=utf-8".toMediaTypeOrNull() + val searchPayload = HipercoolSearchDto( + start = (page - 1) * DEFAULT_COUNT, + count = DEFAULT_COUNT, + text = query, + type = "text" + ) - // Create json body. - val json = buildJsonObject { - put("start", (page - 1) * DEFAULT_COUNT) - put("content", DEFAULT_COUNT) - put("text", query) - put("type", "text") - } - - val body = json.toString().toRequestBody(mediaType) + val body = json.encodeToString(searchPayload).toRequestBody(JSON_MEDIA_TYPE) return POST("$baseUrl/api/books/chapters/search", headers, body) } @@ -119,39 +115,22 @@ class Hipercool : HttpSource() { return GET("$baseUrl/api/books/$slug", headers) } - override fun mangaDetailsParse(response: Response): SManga { - val book = json.decodeFromString(response.body!!.string()) + override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { + val book = response.parseAs() - val artists = book.tags - .filter { it.label == "Artista" } - .flatMap { it.values } - .joinToString("; ") { it.label } - - val authors = book.tags - .filter { it.label == "Autor" } - .flatMap { it.values } - .joinToString("; ") { it.label } - - val tags = book.tags - .filter { it.label == "Tags" } - .flatMap { it.values } - .joinToString { it.label } - - return SManga.create().apply { - title = book.title - thumbnail_url = book.slug.toThumbnailUrl(book.revision) - description = book.synopsis.orEmpty() - artist = artists - author = authors - genre = tags - } + title = book.title + thumbnail_url = book.slug.toThumbnailUrl(book.revision) + description = book.synopsis.orEmpty() + artist = book.fixedTags["artista"].orEmpty().joinToString("; ") + author = book.fixedTags["autor"].orEmpty().joinToString("; ") + genre = book.fixedTags["tags"].orEmpty().joinToString() } // Chapters are available in the same url of the manga details. override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga) override fun chapterListParse(response: Response): List { - val book = json.decodeFromString(response.body!!.string()) + val book = response.parseAs() if (book.chapters is JsonPrimitive) return emptyList() @@ -166,8 +145,9 @@ class Hipercool : HttpSource() { name = "Cap. " + chapter.title chapter_number = chapter.title.toFloatOrNull() ?: -1f date_upload = chapter.publishedAt.toDate() + scanlator = book.fixedTags["tradutor"]?.joinToString(" & ") - val fullUrl = "$baseUrl/books".toHttpUrlOrNull()!!.newBuilder() + val fullUrl = "$baseUrl/books".toHttpUrl().newBuilder() .addPathSegment(book.slug) .addPathSegment(chapter.slug) .addQueryParameter("images", chapter.images.toString()) @@ -183,30 +163,26 @@ class Hipercool : HttpSource() { val bookSlug = chapterUrl.pathSegments[1] val chapterSlug = chapterUrl.pathSegments[2] val images = chapterUrl.queryParameter("images")!!.toInt() - val revision = chapterUrl.queryParameter("revision")!!.toInt() + val revision = chapterUrl.queryParameter("revision")!! - val pages = arrayListOf() - - // Create the pages. - for (i in 1..images) { - val imageUrl = "$STATIC_URL/books".toHttpUrlOrNull()!!.newBuilder() + val pages = List(images) { i -> + val imageUrl = "$STATIC_URL/books".toHttpUrl().newBuilder() .addPathSegment(bookSlug) .addPathSegment(chapterSlug) - .addPathSegment("$bookSlug-chapter-$chapterSlug-page-$i.jpg") - .addQueryParameter("revision", revision.toString()) + .addPathSegment("$bookSlug-chapter-$chapterSlug-page-${i + 1}.jpg") + .addQueryParameter("revision", revision) .toString() - pages += Page(i - 1, chapter.url, imageUrl) + Page(i, chapter.url, imageUrl) } return Observable.just(pages) } - override fun pageListParse(response: Response): List = throw Exception("This method should not be called!") + override fun pageListParse(response: Response): List = + throw Exception("This method should not be called!") - override fun fetchImageUrl(page: Page): Observable { - return Observable.just(page.imageUrl!!) - } + override fun fetchImageUrl(page: Page): Observable = Observable.just(page.imageUrl!!) override fun imageUrlParse(response: Response): String = "" @@ -218,12 +194,13 @@ class Hipercool : HttpSource() { return GET(page.imageUrl!!, newHeaders) } + private inline fun Response.parseAs(): T = use { + json.decodeFromString(it.body?.string().orEmpty()) + } + private fun String.toDate(): Long { - return try { - DATE_FORMATTER.parse(substringBefore("T"))?.time ?: 0L - } catch (e: ParseException) { - 0L - } + return runCatching { DATE_FORMATTER.parse(this)?.time } + .getOrNull() ?: 0L } private fun String.toThumbnailUrl(revision: Int): String = @@ -236,11 +213,15 @@ class Hipercool : HttpSource() { companion object { private const val STATIC_URL = "https://static.hiper.cool" + private val JSON_MEDIA_TYPE = "application/json; charset=utf-8".toMediaType() + private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36" + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36" private const val DEFAULT_COUNT = 40 - private val DATE_FORMATTER by lazy { SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH) } + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ENGLISH) + } } } diff --git a/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/HipercoolDto.kt b/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/HipercoolDto.kt index 31ebd048e..69fd82e6d 100644 --- a/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/HipercoolDto.kt +++ b/src/pt/hipercool/src/eu/kanade/tachiyomi/extension/pt/hipercool/HipercoolDto.kt @@ -12,12 +12,18 @@ data class HipercoolBookDto( val synopsis: String? = null, val tags: List = emptyList(), val title: String -) +) { + val fixedTags: Map> + get() = tags + .groupBy(HipercoolTagDto::slug, HipercoolTagDto::values) + .mapValues { it.value.flatten().map(HipercoolTagDto::label) } +} @Serializable data class HipercoolTagDto( val label: String, - val values: List = emptyList() + val values: List = emptyList(), + val slug: String ) @Serializable @@ -28,3 +34,11 @@ data class HipercoolChapterDto( val slug: String, val title: String ) + +@Serializable +data class HipercoolSearchDto( + val start: Int, + val count: Int, + val text: String, + val type: String +) diff --git a/src/pt/saikaiscan/build.gradle b/src/pt/saikaiscan/build.gradle index 5d3c8866f..520b8c3d1 100644 --- a/src/pt/saikaiscan/build.gradle +++ b/src/pt/saikaiscan/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Saikai Scan' pkgNameSuffix = 'pt.saikaiscan' extClass = '.SaikaiScan' - extVersionCode = 7 + extVersionCode = 8 } dependencies { diff --git a/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScan.kt b/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScan.kt index 352af8ea2..ccf6e6277 100644 --- a/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScan.kt +++ b/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScan.kt @@ -1,9 +1,8 @@ package eu.kanade.tachiyomi.extension.pt.saikaiscan -import eu.kanade.tachiyomi.lib.ratelimit.RateLimitInterceptor +import eu.kanade.tachiyomi.lib.ratelimit.SpecificHostRateLimitInterceptor import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page @@ -20,10 +19,8 @@ import okhttp3.Response import org.jsoup.Jsoup import rx.Observable import uy.kohesive.injekt.injectLazy -import java.text.ParseException import java.text.SimpleDateFormat import java.util.Locale -import java.util.concurrent.TimeUnit class SaikaiScan : HttpSource() { @@ -36,7 +33,8 @@ class SaikaiScan : HttpSource() { override val supportsLatest = true override val client: OkHttpClient = network.cloudflareClient.newBuilder() - .addInterceptor(RateLimitInterceptor(1, 2, TimeUnit.SECONDS)) + .addInterceptor(SpecificHostRateLimitInterceptor(API_URL.toHttpUrl(), 1, 2)) + .addInterceptor(SpecificHostRateLimitInterceptor(IMAGE_SERVER_URL.toHttpUrl(), 1, 1)) .build() private val json: Json by injectLazy() @@ -63,7 +61,7 @@ class SaikaiScan : HttpSource() { } override fun popularMangaParse(response: Response): MangasPage { - val result = json.decodeFromString(response.body!!.string()) + val result = response.parseAs() val mangaList = result.data!!.map(::popularMangaFromObject) val hasNextPage = result.meta!!.currentPage < result.meta.lastPage @@ -170,7 +168,7 @@ class SaikaiScan : HttpSource() { } override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply { - val result = json.decodeFromString(response.body!!.string()) + val result = response.parseAs() val story = result.data!![0] title = story.title @@ -202,13 +200,13 @@ class SaikaiScan : HttpSource() { } override fun chapterListParse(response: Response): List { - val result = json.decodeFromString(response.body!!.string()) + val result = response.parseAs() val story = result.data!![0] return story.releases .filter { it.isActive == 1 } .map { chapterFromObject(it, story.slug) } - .sortedByDescending { it.chapter_number } + .sortedByDescending(SChapter::chapter_number) } private fun chapterFromObject(obj: SaikaiScanReleaseDto, storySlug: String): SChapter = @@ -216,7 +214,7 @@ class SaikaiScan : HttpSource() { name = "Capítulo ${obj.chapter}" + (if (obj.title.isNullOrEmpty().not()) " - ${obj.title}" else "") chapter_number = obj.chapter.toFloatOrNull() ?: -1f - date_upload = obj.publishedAt.substringBefore(" ").toDate() + date_upload = obj.publishedAt.toDate() scanlator = this@SaikaiScan.name url = "/ler/comics/$storySlug/${obj.id}/${obj.slug}" } @@ -238,7 +236,7 @@ class SaikaiScan : HttpSource() { } override fun pageListParse(response: Response): List { - val result = json.decodeFromString(response.body!!.string()) + val result = response.parseAs() return result.data!!.releaseImages.mapIndexed { i, obj -> Page(i, "", "$IMAGE_SERVER_URL/${obj.image}") @@ -257,43 +255,6 @@ class SaikaiScan : HttpSource() { return GET(page.imageUrl!!, imageHeaders) } - private class Genre(title: String, val id: Int) : Filter.CheckBox(title) - - private class GenreFilter(genres: List) : Filter.Group("Gêneros", genres) - - private data class Country(val name: String, val id: Int) { - override fun toString(): String = name - } - - private open class EnhancedSelect(name: String, values: Array) : Filter.Select(name, values) { - val selected: T - get() = values[state] - } - - private class CountryFilter(countries: List) : EnhancedSelect( - "Nacionalidade", - countries.toTypedArray() - ) - - private data class Status(val name: String, val id: Int) { - override fun toString(): String = name - } - - private class StatusFilter(statuses: List) : EnhancedSelect( - "Status", - statuses.toTypedArray() - ) - - private data class SortProperty(val name: String, val slug: String) { - override fun toString(): String = name - } - - private class SortByFilter(val sortProperties: List) : Filter.Sort( - "Ordenar por", - sortProperties.map { it.name }.toTypedArray(), - Selection(2, ascending = false) - ) - // fetch('https://api.saikai.com.br/api/genres') // .then(res => res.json()) // .then(res => console.log(res.data.map(g => `Genre("${g.name}", ${g.id})`).join(',\n'))) @@ -382,12 +343,13 @@ class SaikaiScan : HttpSource() { GenreFilter(getGenreList()) ) + private inline fun Response.parseAs(): T = use { + json.decodeFromString(it.body?.string().orEmpty()) + } + private fun String.toDate(): Long { - return try { - DATE_FORMATTER.parse(this)?.time ?: 0L - } catch (e: ParseException) { - 0L - } + return runCatching { DATE_FORMATTER.parse(this)?.time } + .getOrNull() ?: 0L } private fun String.toStatus(): Int = when (this) { @@ -403,7 +365,9 @@ class SaikaiScan : HttpSource() { private const val COMIC_FORMAT_ID = "2" private const val PER_PAGE = "12" - private val DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale("pt", "BR")) + private val DATE_FORMATTER by lazy { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale("pt", "BR")) + } private const val API_URL = "https://api.saikai.com.br" private const val IMAGE_SERVER_URL = "https://s3-alpha.saikai.com.br" diff --git a/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScanFilters.kt b/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScanFilters.kt new file mode 100644 index 000000000..580a0f113 --- /dev/null +++ b/src/pt/saikaiscan/src/eu/kanade/tachiyomi/extension/pt/saikaiscan/SaikaiScanFilters.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.extension.pt.saikaiscan + +import eu.kanade.tachiyomi.source.model.Filter + +class Genre(title: String, val id: Int) : Filter.CheckBox(title) + +class GenreFilter(genres: List) : Filter.Group("Gêneros", genres) + +data class Country(val name: String, val id: Int) { + override fun toString(): String = name +} + +open class EnhancedSelect(name: String, values: Array) : Filter.Select(name, values) { + val selected: T + get() = values[state] +} + +class CountryFilter(countries: List) : EnhancedSelect( + "Nacionalidade", + countries.toTypedArray() +) + +data class Status(val name: String, val id: Int) { + override fun toString(): String = name +} + +class StatusFilter(statuses: List) : EnhancedSelect( + "Status", + statuses.toTypedArray() +) + +data class SortProperty(val name: String, val slug: String) { + override fun toString(): String = name +} + +class SortByFilter(val sortProperties: List) : Filter.Sort( + "Ordenar por", + sortProperties.map { it.name }.toTypedArray(), + Selection(2, ascending = false) +)