mirror of
https://github.com/keiyoushi/extensions-source.git
synced 2024-11-25 11:42:47 +01:00
Add VapoScans (#3471)
* Add VapoScans * Fix chapterUrl * Cleanup * Add searchManga
This commit is contained in:
parent
8e58e461a7
commit
caaff1df38
22
src/pt/vaposcans/AndroidManifest.xml
Normal file
22
src/pt/vaposcans/AndroidManifest.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<activity
|
||||
android:name=".pt.vaposcans.VapoScansUrlActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="true"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="vaposcans.site"
|
||||
android:pathPattern="/series/..*"
|
||||
android:scheme="https" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
7
src/pt/vaposcans/build.gradle
Normal file
7
src/pt/vaposcans/build.gradle
Normal file
@ -0,0 +1,7 @@
|
||||
ext {
|
||||
extName = 'Vapo Scans'
|
||||
extClass = '.VapoScans'
|
||||
extVersionCode = 1
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
BIN
src/pt/vaposcans/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/pt/vaposcans/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
src/pt/vaposcans/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/pt/vaposcans/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
src/pt/vaposcans/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/pt/vaposcans/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
BIN
src/pt/vaposcans/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/pt/vaposcans/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
src/pt/vaposcans/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/pt/vaposcans/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -0,0 +1,198 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.vaposcans
|
||||
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||
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.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
class VapoScans : HttpSource() {
|
||||
override val name = "Vapo Scans"
|
||||
|
||||
override val baseUrl = "https://vaposcans.site"
|
||||
|
||||
override val lang = "pt-BR"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimit(2)
|
||||
.build()
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
// Keeps the behavior of the web page
|
||||
private val emptyPayload = "{}".toRequestBody()
|
||||
|
||||
private var popularMangaCache: List<SManga> = mutableListOf()
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.set("Origin", baseUrl)
|
||||
.set("Referer", "$baseUrl/")
|
||||
|
||||
override fun popularMangaRequest(page: Int) =
|
||||
POST("$apiUrl/api/series/", headers, emptyPayload)
|
||||
|
||||
override fun popularMangaParse(response: Response) =
|
||||
MangasPage(
|
||||
response.parseAs<List<MangaDto>>()
|
||||
.map(::sMangaParse)
|
||||
.also {
|
||||
popularMangaCache = it
|
||||
},
|
||||
false,
|
||||
)
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
POST("$apiUrl/api/recent-chapters/", headers, emptyPayload)
|
||||
|
||||
override fun latestUpdatesParse(response: Response) =
|
||||
MangasPage(
|
||||
response.parseAs<List<LatestMangaDto>>()
|
||||
.map { sMangaParse(it.mangaDto) },
|
||||
false,
|
||||
)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
if (query.startsWith(URL_SEARCH_PREFIX)) {
|
||||
val manga = SManga.create().apply {
|
||||
url = query.substringAfter(URL_SEARCH_PREFIX)
|
||||
}
|
||||
|
||||
return fetchMangaDetails(manga).map {
|
||||
MangasPage(listOf(it), false)
|
||||
}
|
||||
}
|
||||
|
||||
if (popularMangaCache.isNotEmpty()) {
|
||||
return Observable.just(findMangaByTitle(query))
|
||||
}
|
||||
|
||||
return super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||
POST("$apiUrl/api/series/#$query", headers, emptyPayload)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangas = popularMangaParse(response).mangas
|
||||
val query = response.request.url.toString().substringAfter("#")
|
||||
return findMangaByTitle(query, mangas)
|
||||
}
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String = "$baseUrl/series/${manga.url}"
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request {
|
||||
val payload = MangaCode(manga.url).toRequestBody()
|
||||
return POST("$apiUrl/api/serie/", headers, payload)
|
||||
}
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
response.parseAs<MangaDetailsDto>().let {
|
||||
title = it.title
|
||||
description = it.synopsis
|
||||
url = it.code
|
||||
genre = it.genres.joinToString()
|
||||
artist = it.artist
|
||||
author = it.author
|
||||
thumbnail_url = it.cover
|
||||
status = when (it.status) {
|
||||
"completed" -> SManga.COMPLETED
|
||||
"ongoing" -> SManga.ONGOING
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter) = "$baseUrl/reader/${chapter.url}"
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request {
|
||||
val payload = MangaCode(manga.url).toRequestBody()
|
||||
return POST("$apiUrl/api/serie/chapters/", headers, payload)
|
||||
}
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> {
|
||||
return response.parseAs<List<ChapterDto>>().map {
|
||||
SChapter.create().apply {
|
||||
name = it.number
|
||||
url = it.code
|
||||
date_upload = parseDate(it.upload_date)
|
||||
chapter_number = it.number.toFloat()
|
||||
}
|
||||
}.sortedBy { it.chapter_number }.reversed()
|
||||
}
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request {
|
||||
val payload = MangaCode(chapter.url).toRequestBody()
|
||||
return POST("$apiUrl/api/chapter_details/", headers, payload)
|
||||
}
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> {
|
||||
val dto = response.parseAs<PagesDto>()
|
||||
val chapterUrl = "$baseUrl/reader/${dto.chapter_code}"
|
||||
return dto.images.mapIndexed { index, image ->
|
||||
Page(index, chapterUrl, "$apiUrl/$image")
|
||||
}
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = ""
|
||||
|
||||
private fun findMangaByTitle(query: String, collection: List<SManga> = popularMangaCache): MangasPage {
|
||||
val mangas = collection
|
||||
.filter { it.title.contains(query, ignoreCase = true) }
|
||||
|
||||
return MangasPage(mangas, false)
|
||||
}
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
json.decodeFromString(body.string())
|
||||
|
||||
private inline fun <reified T : Any> T.toRequestBody(): RequestBody =
|
||||
json.encodeToString(this)
|
||||
.toRequestBody(JSON_MEDIA_TYPE)
|
||||
|
||||
private fun sMangaParse(dto: MangaDto) = SManga.create().apply {
|
||||
title = dto.title
|
||||
thumbnail_url = "$apiUrl/${dto.cover}"
|
||||
url = dto.code
|
||||
}
|
||||
|
||||
private fun parseDate(date: String): Long =
|
||||
try { dateFormat.parse(date)!!.time } catch (_: Exception) { parseRelativeDate(date) }
|
||||
|
||||
private fun parseRelativeDate(date: String): Long {
|
||||
val number = RELATIVE_DATE_REGEX.find(date)?.value?.toIntOrNull() ?: return 0
|
||||
val cal = Calendar.getInstance()
|
||||
return when {
|
||||
date.contains("dia", ignoreCase = true) -> cal.apply { add(Calendar.DATE, -number) }.timeInMillis
|
||||
date.contains("mes", ignoreCase = true) -> cal.apply { add(Calendar.MONTH, -number) }.timeInMillis
|
||||
date.contains("ano", ignoreCase = true) -> cal.apply { add(Calendar.YEAR, -number) }.timeInMillis
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val apiUrl = "https://api.vaposcans.site"
|
||||
const val URL_SEARCH_PREFIX = "slug:"
|
||||
val JSON_MEDIA_TYPE = "application/json".toMediaTypeOrNull()
|
||||
val RELATIVE_DATE_REGEX = """(\d+)""".toRegex()
|
||||
|
||||
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale("pt", "BR"))
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.vaposcans
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class MangaDto(
|
||||
val code: String,
|
||||
val cover: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class LatestMangaDto(
|
||||
val serie_code: String,
|
||||
val serie_cover: String,
|
||||
val serie_title: String,
|
||||
) {
|
||||
val mangaDto get() = MangaDto(serie_code, serie_cover, serie_title)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class MangaCode(val code: String)
|
||||
|
||||
@Serializable
|
||||
class MangaDetailsDto(
|
||||
val artist: String,
|
||||
val author: String,
|
||||
val code: String,
|
||||
val cover: String,
|
||||
val genres: List<String>,
|
||||
val status: String,
|
||||
val synopsis: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class ChapterDto(
|
||||
val number: String,
|
||||
val code: String,
|
||||
val upload_date: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PagesDto(
|
||||
val chapter_code: String,
|
||||
val images: List<String>,
|
||||
)
|
@ -0,0 +1,37 @@
|
||||
package eu.kanade.tachiyomi.extension.pt.vaposcans
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class VapoScansUrlActivity : Activity() {
|
||||
|
||||
private val tag = javaClass.simpleName
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val pathSegments = intent?.data?.pathSegments
|
||||
if (pathSegments != null && pathSegments.size > 1) {
|
||||
val item = pathSegments[1]
|
||||
val mainIntent = Intent().apply {
|
||||
action = "eu.kanade.tachiyomi.SEARCH"
|
||||
putExtra("query", "${VapoScans.URL_SEARCH_PREFIX}$item")
|
||||
putExtra("filter", packageName)
|
||||
}
|
||||
|
||||
try {
|
||||
startActivity(mainIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(tag, e.toString())
|
||||
}
|
||||
} else {
|
||||
Log.e(tag, "could not parse uri from intent $intent")
|
||||
}
|
||||
|
||||
finish()
|
||||
exitProcess(0)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user