Add a new source. (#15219)

This commit is contained in:
Alessandro Jean 2023-02-03 15:05:30 -03:00 committed by GitHub
parent 4cc72c2f98
commit 044762ceb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 269 additions and 0 deletions

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="eu.kanade.tachiyomi.extension" />

View File

@ -0,0 +1,13 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlinx-serialization'
ext {
extName = 'Nix Mangás'
pkgNameSuffix = 'pt.nixmangas'
extClass = '.NixMangas'
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -0,0 +1,166 @@
package eu.kanade.tachiyomi.extension.pt.nixmangas
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.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 okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
class NixMangas : HttpSource() {
override val name = "Nix Mangás"
override val baseUrl = "https://nixmangas.com"
override val lang = "pt-BR"
override val supportsLatest = true
override val client: OkHttpClient = network.cloudflareClient.newBuilder()
.rateLimitHost(baseUrl.toHttpUrl(), 1, 1, TimeUnit.SECONDS)
.rateLimitHost(API_URL.toHttpUrl(), 1, 1, TimeUnit.SECONDS)
.rateLimitHost(CDN_URL.toHttpUrl(), 1, 2, TimeUnit.SECONDS)
.build()
private val json: Json by injectLazy()
private val apiHeaders: Headers by lazy { apiHeadersBuilder().build() }
override fun headersBuilder(): Headers.Builder = Headers.Builder()
.add("Origin", baseUrl)
.add("Referer", baseUrl)
private fun apiHeadersBuilder(): Headers.Builder = headersBuilder()
.add("Accept", ACCEPT_JSON)
/**
* The site doesn't have a popular section, so we use latest instead.
*/
override fun popularMangaRequest(page: Int) = latestUpdatesRequest(page)
override fun popularMangaParse(response: Response) = latestUpdatesParse(response)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$API_URL/mangas?page=$page", apiHeaders)
}
override fun latestUpdatesParse(response: Response): MangasPage {
val result = response.parseAs<NixMangasPaginatedContent<NixMangasWorkDto>>()
val workList = result.data.map(NixMangasWorkDto::toSManga)
return MangasPage(workList, result.hasNextPage)
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
// The search with query isn't working in their direct API for some reason,
// so we use their site wrapped API instead for now.
val apiUrl = "$baseUrl/obras".toHttpUrl().newBuilder()
.addQueryParameter("page", page.toString())
.addQueryParameter("q", query)
.addQueryParameter("_data", "routes/__app/obras/index")
.toString()
return GET(apiUrl, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
val result = response.parseAs<NixMangasSearchDto>()
val workList = result.mangas.data.map(NixMangasWorkDto::toSManga)
return MangasPage(workList, result.mangas.hasNextPage)
}
// Workaround to allow "Open in browser" use the real URL.
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return client.newCall(mangaDetailsApiRequest(manga.url))
.asObservableSuccess()
.map { response ->
mangaDetailsParse(response).apply { initialized = true }
}
}
private fun mangaDetailsApiRequest(mangaUrl: String): Request {
// Their API doesn't have an endpoint for the manga details, so we
// use their site wrapped API instead for now.
val apiUrl = (baseUrl + mangaUrl).toHttpUrl().newBuilder()
.addQueryParameter("_data", "routes/__app/obras/\$slug")
.toString()
return GET(apiUrl, apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga {
val result = response.parseAs<NixMangasDetailsDto>()
return result.manga.toSManga()
}
override fun chapterListRequest(manga: SManga): Request = mangaDetailsApiRequest(manga.url)
override fun chapterListParse(response: Response): List<SChapter> {
val result = response.parseAs<NixMangasDetailsDto>()
val currentTimeStamp = System.currentTimeMillis()
return result.manga.chapters
.filter { it.isPublished }
.map(NixMangasChapterDto::toSChapter)
.filter { it.date_upload <= currentTimeStamp }
.sortedByDescending(SChapter::chapter_number)
}
override fun pageListRequest(chapter: SChapter): Request {
val apiUrl = (baseUrl + chapter.url).toHttpUrl().newBuilder()
.addQueryParameter("_data", "routes/__leitor/ler.\$manga.\$chapter")
.toString()
return GET(apiUrl, apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val result = response.parseAs<NixMangasReaderDto>()
val chapterUrl = "$baseUrl/ler/${result.chapter.slug}"
return result.chapter.pages.mapIndexed { i, pageDto ->
Page(i, chapterUrl, pageDto.pageUrl)
}
}
override fun fetchImageUrl(page: Page): Observable<String> = Observable.just(page.imageUrl!!)
override fun imageUrlParse(response: Response): String = ""
override fun imageRequest(page: Page): Request {
val newHeaders = headersBuilder()
.add("Accept", ACCEPT_IMAGE)
.set("Referer", page.url)
.build()
return GET(page.imageUrl!!, newHeaders)
}
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromString(it.body?.string().orEmpty())
}
companion object {
private const val ACCEPT_IMAGE = "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"
private const val ACCEPT_JSON = "application/json"
private const val API_URL = "https://api.nixmangas.com/v1"
private const val CDN_URL = "https://cdn.nixmangas.com"
}
}

View File

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.extension.pt.nixmangas
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Serializable
data class NixMangasPaginatedContent<T>(
@SerialName("current_page") val currentPage: Int,
val data: List<T> = emptyList(),
@SerialName("last_page") val lastPage: Int,
) {
val hasNextPage: Boolean
get() = currentPage < lastPage
}
@Serializable
data class NixMangasSearchDto(val mangas: NixMangasPaginatedContent<NixMangasWorkDto>)
@Serializable
data class NixMangasDetailsDto(val manga: NixMangasWorkDto)
@Serializable
data class NixMangasReaderDto(val chapter: NixMangasChapterDto)
@Serializable
data class NixMangasWorkDto(
val id: String,
val chapters: List<NixMangasChapterDto> = emptyList(),
val cover: String? = null,
val genres: List<NixMangasGenreDto> = emptyList(),
@SerialName("is_adult") val isAdult: Boolean = false,
val slug: String,
val status: String? = null,
@SerialName("synopses") val synopsis: String? = null,
val thumbnail: String,
val title: String,
) {
fun toSManga(): SManga = SManga.create().apply {
title = this@NixMangasWorkDto.title
description = synopsis
genre = genres.joinToString { it.name }
status = when (this@NixMangasWorkDto.status) {
"ACTIVE" -> SManga.ONGOING
else -> SManga.UNKNOWN
}
thumbnail_url = cover
url = "/obras/$slug"
}
}
@Serializable
data class NixMangasGenreDto(val name: String)
@Serializable
data class NixMangasChapterDto(
@SerialName("is_published") val isPublished: Boolean,
val number: Float,
val pages: List<NixMangasPageDto> = emptyList(),
val slug: String,
@SerialName("published_at") val publishedAt: String? = null,
) {
fun toSChapter(): SChapter = SChapter.create().apply {
name = "Capítulo ${number.toString().replace(".0", "")}"
chapter_number = number
date_upload = runCatching { DATE_FORMATTER.parse(publishedAt!!)?.time }
.getOrNull() ?: 0L
url = "/ler/$slug"
}
companion object {
private val DATE_FORMATTER by lazy {
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.S", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
}
}
}
@Serializable
data class NixMangasPageDto(@SerialName("page_url") val pageUrl: String)