move sunshinebutterflyscans away from multisrc (#3092)
* move sunshinebutterflyscans away from multisrc * suggestions
This commit is contained in:
parent
9440fc3b1a
commit
1d6a48c189
@ -1,10 +1,12 @@
|
|||||||
ext {
|
ext {
|
||||||
extName = 'Sunshine Butterfly Scans'
|
extName = 'Sunshine Butterfly Scans'
|
||||||
extClass = '.SunshineButterflyScans'
|
extClass = '.SunshineButterflyScans'
|
||||||
themePkg = 'madara'
|
extVersionCode = 38
|
||||||
baseUrl = 'https://sunshinebutterflyscan.com'
|
|
||||||
overrideVersionCode = 1
|
|
||||||
isNsfw = true
|
isNsfw = true
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(':lib:cryptoaes'))
|
||||||
|
}
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
package eu.kanade.tachiyomi.extension.en.sunshinebutterflyscans
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class EntryDto(
|
||||||
|
val series: String,
|
||||||
|
val timestamp: String,
|
||||||
|
val num: Int,
|
||||||
|
@SerialName("chname") val chapterName: String,
|
||||||
|
@SerialName("AlbumID") val albumID: String,
|
||||||
|
@SerialName("projectname") val projectName: String,
|
||||||
|
@SerialName("projectdesc") val projectDesc: String,
|
||||||
|
@SerialName("projectaltname") val altName: String,
|
||||||
|
@SerialName("projectauthor") val projectAuthor: String,
|
||||||
|
@SerialName("projectartist") val projectArtist: String,
|
||||||
|
@SerialName("projectthumb") val projectThumb: String,
|
||||||
|
@SerialName("projectstatus") val projectStatus: String,
|
||||||
|
@SerialName("projecttags") val projectTags: String,
|
||||||
|
) {
|
||||||
|
fun toSManga(cdnUrl: String): SManga = SManga.create().apply {
|
||||||
|
title = series
|
||||||
|
thumbnail_url = cdnUrl + projectThumb
|
||||||
|
url = "/projects?n=$projectName"
|
||||||
|
description = buildString {
|
||||||
|
projectDesc.nonEmpty()?.let { append(it) }
|
||||||
|
if (altName.isNotEmpty()) {
|
||||||
|
append("\n\n")
|
||||||
|
append("Alternative name: ")
|
||||||
|
append(altName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
genre = projectTags.nonEmpty()?.replace(",", ", ")
|
||||||
|
status = projectStatus.toStatus()
|
||||||
|
author = projectAuthor.nonEmpty()
|
||||||
|
artist = projectArtist.nonEmpty()
|
||||||
|
initialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toStatus(): Int = when (this) {
|
||||||
|
"current" -> SManga.ONGOING
|
||||||
|
"complete" -> SManga.COMPLETED
|
||||||
|
"dropped" -> SManga.CANCELLED
|
||||||
|
"licensed" -> SManga.LICENSED
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toSChapter(): SChapter = SChapter.create().apply {
|
||||||
|
name = chapterName
|
||||||
|
chapter_number = num.toFloat()
|
||||||
|
date_upload = timestamp.nonEmpty()?.toLong()?.times(1000) ?: 0L
|
||||||
|
url = "/read?series=$projectName&num=$num"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class GoogleDriveResponseDto(
|
||||||
|
val files: List<FileDto>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class FileDto(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
@SerialName("imageMediaMetadata") val metadata: MetadataDto,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class MetadataDto(
|
||||||
|
val width: Int,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class ImgurResponseDto(
|
||||||
|
val data: List<DataDto>,
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
class DataDto(
|
||||||
|
val link: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.nonEmpty() = this.takeIf { it.isNotEmpty() }
|
@ -1,5 +1,270 @@
|
|||||||
package eu.kanade.tachiyomi.extension.en.sunshinebutterflyscans
|
package eu.kanade.tachiyomi.extension.en.sunshinebutterflyscans
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.multisrc.madara.Madara
|
import android.util.Base64
|
||||||
|
import eu.kanade.tachiyomi.lib.cryptoaes.CryptoAES
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.interceptor.rateLimit
|
||||||
|
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
|
||||||
|
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.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import rx.Observable
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class SunshineButterflyScans : Madara("Sunshine Butterfly Scans", "https://sunshinebutterflyscan.com", "en")
|
class SunshineButterflyScans : HttpSource() {
|
||||||
|
|
||||||
|
override val name = "Sunshine Butterfly Scans"
|
||||||
|
|
||||||
|
override val baseUrl = "https://wings.sbs"
|
||||||
|
private val cdnUrl = "$baseUrl/images/projcoverjpeg/"
|
||||||
|
|
||||||
|
override val lang = "en"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
// Madara -> custom theme
|
||||||
|
override val versionId = 2
|
||||||
|
|
||||||
|
override val client = network.cloudflareClient.newBuilder()
|
||||||
|
.rateLimit(2)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun headersBuilder() = super.headersBuilder()
|
||||||
|
.add("Referer", "$baseUrl/")
|
||||||
|
|
||||||
|
private fun apiHeadersBuilder() = headersBuilder().apply {
|
||||||
|
add("Accept", "*/*")
|
||||||
|
add("Host", baseUrl.toHttpUrl().host)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val apiHeaders by lazy { apiHeadersBuilder().build() }
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
|
private val chaptersData by lazy {
|
||||||
|
client.newCall(
|
||||||
|
GET("$baseUrl/json/chapters.json", apiHeaders),
|
||||||
|
).execute().parseAs<List<EntryDto>>().groupBy {
|
||||||
|
it.series
|
||||||
|
}.values.map { it.sortedByDescending { it.num } }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================== Popular ===============================
|
||||||
|
|
||||||
|
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||||
|
val mangaList = chaptersData.sortedBy {
|
||||||
|
it.first().series
|
||||||
|
}.map {
|
||||||
|
it.first().toSManga(cdnUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.just(MangasPage(mangaList, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int): Request =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response): MangasPage =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Latest ===============================
|
||||||
|
|
||||||
|
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||||
|
val mangaList = chaptersData.sortedByDescending {
|
||||||
|
it.first().timestamp.toLongOrNull() ?: Long.MAX_VALUE
|
||||||
|
}.map {
|
||||||
|
it.first().toSManga(cdnUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.just(MangasPage(mangaList, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int): Request =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Search ===============================
|
||||||
|
|
||||||
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
|
val selectedStatus = filters.filterIsInstance<StatusFilter>().first().toUriPart()
|
||||||
|
val selectedSort = filters.filterIsInstance<SortFilter>().first().getSelection()
|
||||||
|
|
||||||
|
val sortedList = if (selectedSort.first == "Name") {
|
||||||
|
chaptersData.sortedBy { it.first().series }
|
||||||
|
} else {
|
||||||
|
chaptersData.sortedByDescending {
|
||||||
|
it.first().timestamp.toLongOrNull() ?: Long.MAX_VALUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val filteredList = sortedList
|
||||||
|
.filter { it.first().series.contains(query, true) }
|
||||||
|
.filter { it.first().projectStatus.contains(selectedStatus) }
|
||||||
|
|
||||||
|
val reversedList = if (selectedSort.second) {
|
||||||
|
filteredList.reversed()
|
||||||
|
} else {
|
||||||
|
filteredList
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaList = reversedList.map {
|
||||||
|
it.first().toSManga(cdnUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.just(MangasPage(mangaList, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response): MangasPage =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Filters ==============================
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList = FilterList(
|
||||||
|
StatusFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
)
|
||||||
|
|
||||||
|
open class UriPartFilter(displayName: String, private val vals: Array<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusFilter : UriPartFilter(
|
||||||
|
"Status",
|
||||||
|
arrayOf(
|
||||||
|
Pair("All", ""),
|
||||||
|
Pair("Current", "current"),
|
||||||
|
Pair("Complete", "complete"),
|
||||||
|
Pair("Dropped", "dropped"),
|
||||||
|
Pair("Licensed", "licensed"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class SortFilter : Filter.Sort(
|
||||||
|
"Sort by",
|
||||||
|
VALUES,
|
||||||
|
Selection(0, false),
|
||||||
|
) {
|
||||||
|
fun getSelection() = Pair(VALUES[state!!.index], state!!.ascending)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val VALUES = arrayOf("Name", "Last Updated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================== Manga Details ============================
|
||||||
|
|
||||||
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
|
val mangaData = chaptersData.first {
|
||||||
|
it.first().projectName == manga.url.substringAfter("?n=")
|
||||||
|
}.first()
|
||||||
|
|
||||||
|
return Observable.just(mangaData.toSManga(cdnUrl))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMangaUrl(manga: SManga): String = baseUrl + manga.url
|
||||||
|
|
||||||
|
override fun mangaDetailsRequest(manga: SManga): Request =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// ============================== Chapters ==============================
|
||||||
|
|
||||||
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
|
val selectedManga = chaptersData.first {
|
||||||
|
it.first().projectName == manga.url.substringAfter("?n=")
|
||||||
|
}
|
||||||
|
val chapterList = selectedManga.map { it.toSChapter() }
|
||||||
|
|
||||||
|
return Observable.just(chapterList)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChapterUrl(chapter: SChapter): String = baseUrl + chapter.url
|
||||||
|
|
||||||
|
override fun chapterListRequest(manga: SManga): Request =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// =============================== Pages ================================
|
||||||
|
|
||||||
|
override fun pageListRequest(chapter: SChapter): Request {
|
||||||
|
val chapterDto = chaptersData.flatten().first {
|
||||||
|
"${it.projectName}&num=${it.num}" == chapter.url.substringAfter("series=")
|
||||||
|
}
|
||||||
|
val decrypted = CryptoAES.decrypt(chapterDto.albumID, KEY, IV)
|
||||||
|
|
||||||
|
val url = if (decrypted.length > 10) {
|
||||||
|
GOOGLE_DRIVE_FIRST + decrypted + GOOGLE_DRIVE_SECOND
|
||||||
|
} else {
|
||||||
|
IMGUR_FIRST + decrypted + IMGUR_SECOND
|
||||||
|
}
|
||||||
|
val headers = headersBuilder().apply {
|
||||||
|
set("Host", url.toHttpUrl().host)
|
||||||
|
add("Origin", baseUrl)
|
||||||
|
if (decrypted.length <= 10) {
|
||||||
|
add("Authorization", "Bearer $IMGUR_BEARER")
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(url, headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
return if (response.request.url.host.contains("googleapis")) {
|
||||||
|
response.parseAs<GoogleDriveResponseDto>().files.sortedBy {
|
||||||
|
it.name
|
||||||
|
}.mapIndexed { index, file ->
|
||||||
|
Page(index, imageUrl = "https://lh3.googleusercontent.com/d/${file.id}=w${file.metadata.width}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.parseAs<ImgurResponseDto>().data.mapIndexed { index, data ->
|
||||||
|
Page(index, imageUrl = data.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageRequest(page: Page): Request {
|
||||||
|
val imgHeaders = headersBuilder().apply {
|
||||||
|
add("Accept", "image/avif,image/webp,*/*")
|
||||||
|
add("Host", page.imageUrl!!.toHttpUrl().host)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
return GET(page.imageUrl!!, imgHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String =
|
||||||
|
throw UnsupportedOperationException()
|
||||||
|
|
||||||
|
// ============================= Utilities ==============================
|
||||||
|
|
||||||
|
private inline fun <reified T> Response.parseAs(): T {
|
||||||
|
return json.decodeFromString(body.string())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val GOOGLE_DRIVE_FIRST = "https://www.googleapis.com/drive/v3/files?q=\""
|
||||||
|
private const val GOOGLE_DRIVE_SECOND = "\"+in+parents&key=AIzaSyDDWjOHN1UPcafkwyJLO7fX1gmVyntIozs&orderBy=name_natural&fields=files(id,name,imageMediaMetadata)&pageSize=250"
|
||||||
|
private const val IMGUR_FIRST = "https://api.imgur.com/3/album/"
|
||||||
|
private const val IMGUR_SECOND = "/images"
|
||||||
|
private val IMGUR_BEARER = "84155230e6a2d98eaea1cee48d97e6ecff0f6c12"
|
||||||
|
private val KEY = Base64.decode("YX+1nM4KgfaYwNE3/MPcTg==", Base64.DEFAULT)
|
||||||
|
private val IV = Base64.decode("279GjT2Xu9LZBkI4zLzIAg==", Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user