parent
41e64ac576
commit
fe47d20f67
8
src/all/yabai/build.gradle
Normal file
8
src/all/yabai/build.gradle
Normal file
@ -0,0 +1,8 @@
|
||||
ext {
|
||||
extName = 'Yabai'
|
||||
extClass = '.Yabai'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
@ -0,0 +1,69 @@
|
||||
package eu.kanade.tachiyomi.extension.all.yabai
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
|
||||
fun getFilters(): FilterList {
|
||||
return FilterList(
|
||||
SelectFilter("Category", categories.keys.toList()),
|
||||
SelectFilter("Language", languages.keys.toList()),
|
||||
)
|
||||
}
|
||||
|
||||
internal open class SelectFilter(name: String, val vals: List<String>, state: Int = 0) :
|
||||
Filter.Select<String>(name, vals.map { it }.toTypedArray(), state)
|
||||
|
||||
val categories = mapOf(
|
||||
"All" to "",
|
||||
"Doujinshi" to 1,
|
||||
"Manga" to 2,
|
||||
"Artist CG" to 3,
|
||||
"Game CG" to 4,
|
||||
"Western" to 5,
|
||||
"Non-H" to 6,
|
||||
"Image Set" to 7,
|
||||
"Cosplay" to 8,
|
||||
"Misc" to 9,
|
||||
"Asian Porn" to 10,
|
||||
"Private" to 11,
|
||||
)
|
||||
|
||||
val languages = mapOf(
|
||||
"All" to "",
|
||||
"Japanese" to "jp",
|
||||
"English" to "gb",
|
||||
"Korean" to "kr",
|
||||
"Russian" to "ru",
|
||||
"Chinese" to "cn",
|
||||
"French" to "fr",
|
||||
"Italian" to "it",
|
||||
"Spanish" to "es",
|
||||
"Portuguese" to "pt",
|
||||
"German" to "de",
|
||||
"Thai" to "th",
|
||||
"Arabic" to "sa",
|
||||
"Turkish" to "tr",
|
||||
"Hebrew" to "il",
|
||||
"Tagalog" to "ph",
|
||||
"Ukrainian" to "ua",
|
||||
"Bulgarian" to "bg",
|
||||
"Dutch" to "nl",
|
||||
"Mongolian" to "mn",
|
||||
"Vietnamese" to "vn",
|
||||
"Macedonian" to "mk",
|
||||
"Polish" to "pl",
|
||||
"Hungarian" to "hu",
|
||||
"Norwegian" to "no",
|
||||
"Indonesian" to "id",
|
||||
"Lithuanian" to "lt",
|
||||
"Serbian" to "rs",
|
||||
"Persian" to "ir",
|
||||
"Croatian" to "hr",
|
||||
"Czech" to "cz",
|
||||
"Slovak" to "sk",
|
||||
"Romanian" to "ro",
|
||||
"Finnish" to "fi",
|
||||
"Greek" to "gr",
|
||||
"Swedish" to "se",
|
||||
"Latin" to "va",
|
||||
)
|
@ -0,0 +1,219 @@
|
||||
package eu.kanade.tachiyomi.extension.all.yabai
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import keiyoushi.utils.parseAs
|
||||
import keiyoushi.utils.toJsonString
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
class Yabai : HttpSource() {
|
||||
override val name = "Yabai"
|
||||
|
||||
override val baseUrl = "https://yabai.si"
|
||||
|
||||
override val lang = "all"
|
||||
|
||||
override val supportsLatest = false
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.addInterceptor(::tokenInterceptor)
|
||||
.build()
|
||||
|
||||
private var popularNextHash: String? = null
|
||||
|
||||
private var searchNextHash: String? = null
|
||||
|
||||
private var storedToken: String? = null
|
||||
|
||||
private fun tokenInterceptor(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
|
||||
val modifiedRequest = request.newBuilder()
|
||||
.addHeader("X-Requested-With", "XMLHttpRequest")
|
||||
.addHeader("X-Inertia", "true")
|
||||
.addHeader("X-Inertia-Version", "b6320c13b244af5aafcd16668b9b38e4")
|
||||
|
||||
if (request.method == "POST") {
|
||||
modifiedRequest.addHeader("Content-Type", "application/json")
|
||||
|
||||
if (request.header("X-XSRF-TOKEN") == null) {
|
||||
val token = getToken()
|
||||
val response = chain.proceed(
|
||||
modifiedRequest
|
||||
.addHeader("X-XSRF-TOKEN", token)
|
||||
.build(),
|
||||
)
|
||||
|
||||
if (!response.isSuccessful && response.code == 419) {
|
||||
response.close()
|
||||
storedToken = null
|
||||
val newToken = getToken()
|
||||
return chain.proceed(
|
||||
modifiedRequest
|
||||
.addHeader("X-XSRF-TOKEN", newToken)
|
||||
.build(),
|
||||
)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
return chain.proceed(modifiedRequest.build())
|
||||
}
|
||||
|
||||
private fun getToken(): String {
|
||||
if (storedToken.isNullOrEmpty()) {
|
||||
val request = GET(baseUrl, headers)
|
||||
val response = client.newCall(request).execute()
|
||||
|
||||
var found = false
|
||||
|
||||
val headers = response.headers("Set-Cookie")
|
||||
headers.forEach {
|
||||
if (it.startsWith("XSRF-TOKEN=")) {
|
||||
storedToken = it
|
||||
.split(";")
|
||||
.first()
|
||||
.substringAfter("=")
|
||||
.replace("%3D", "=")
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
throw IOException("Unable to find CSRF token")
|
||||
}
|
||||
}
|
||||
|
||||
return storedToken!!
|
||||
}
|
||||
|
||||
override fun getFilterList() = getFilters()
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
if (page == 1) {
|
||||
searchNextHash = null
|
||||
}
|
||||
|
||||
val queryBody = QueryDto(
|
||||
qry = query,
|
||||
cursor = searchNextHash,
|
||||
).apply {
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is SelectFilter -> {
|
||||
when (filter.name) {
|
||||
"Category" -> {
|
||||
categories[filter.vals[filter.state]]?.let {
|
||||
cat = it.toString()
|
||||
}
|
||||
}
|
||||
|
||||
"Language" -> {
|
||||
languages[filter.vals[filter.state]]?.let {
|
||||
lng = it
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return POST(
|
||||
"$baseUrl/g",
|
||||
headers,
|
||||
queryBody.toJsonString().toRequestBody(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<DataResponse<IndexProps>>()
|
||||
|
||||
val galleries = data.props.postList.data.map {
|
||||
it.toSManga()
|
||||
}
|
||||
|
||||
searchNextHash = data.props.postList.meta.nextCursor
|
||||
|
||||
return MangasPage(galleries, searchNextHash != null)
|
||||
}
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request {
|
||||
if (page == 1) {
|
||||
popularNextHash = null
|
||||
}
|
||||
|
||||
val queryBody = QueryDto(
|
||||
cursor = popularNextHash,
|
||||
)
|
||||
|
||||
return POST(
|
||||
"$baseUrl/g",
|
||||
headers,
|
||||
queryBody.toJsonString().toRequestBody(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage {
|
||||
val data = response.parseAs<DataResponse<IndexProps>>()
|
||||
|
||||
val galleries = data.props.postList.data.map {
|
||||
it.toSManga()
|
||||
}
|
||||
|
||||
popularNextHash = data.props.postList.meta.nextCursor
|
||||
|
||||
return MangasPage(galleries, popularNextHash != null)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) = throw UnsupportedOperationException()
|
||||
|
||||
override fun latestUpdatesParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) = GET(
|
||||
"$baseUrl${manga.url}",
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) =
|
||||
response.parseAs<DataResponse<DetailProps>>().props.post.data.toSManga()
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = mangaDetailsRequest(manga)
|
||||
|
||||
override fun chapterListParse(response: Response) =
|
||||
listOf(response.parseAs<DataResponse<DetailProps>>().props.post.data.toSChapter())
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) = GET(
|
||||
"$baseUrl${chapter.url}/read",
|
||||
headers,
|
||||
)
|
||||
|
||||
override fun pageListParse(response: Response) =
|
||||
response.parseAs<DataResponse<ReaderProps>>().props.pages.data.list.toPages()
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
companion object {
|
||||
val createdAtFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.ENGLISH).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
package eu.kanade.tachiyomi.extension.all.yabai
|
||||
|
||||
import eu.kanade.tachiyomi.extension.all.yabai.Yabai.Companion.createdAtFormat
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import okhttp3.internal.format
|
||||
import java.util.Date
|
||||
|
||||
@Serializable
|
||||
class QueryDto(
|
||||
var cat: String = "",
|
||||
var lng: String = "",
|
||||
private val qry: String = "",
|
||||
private val tag: String = "[]",
|
||||
private val cursor: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class DataResponse<T>(
|
||||
val props: T,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class IndexProps(
|
||||
@SerialName("post_list")
|
||||
val postList: PostList,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PostList(
|
||||
val data: List<GalleryItem>,
|
||||
val meta: Meta,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class GalleryItem(
|
||||
private val slug: String,
|
||||
private val name: String,
|
||||
private val cover: String,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
title = name
|
||||
url = format("/g/$slug")
|
||||
thumbnail_url = cover
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Meta(
|
||||
@SerialName("next_cursor")
|
||||
val nextCursor: String?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class DetailProps(
|
||||
val post: Post,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Post(
|
||||
val data: Gallery,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Gallery(
|
||||
private val slug: String,
|
||||
private val name: String,
|
||||
private val cover: String,
|
||||
private val tags: Map<String, List<Tag>>?,
|
||||
private val date: PostDate,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
title = name
|
||||
url = format("/g/$slug")
|
||||
thumbnail_url = cover
|
||||
author = tags
|
||||
?.filterKeys { it == "Group" }
|
||||
?.flatMap { it.value }
|
||||
?.joinToString { it.name }
|
||||
artist = tags
|
||||
?.filterKeys { it == "Artist" }
|
||||
?.flatMap { it.value }
|
||||
?.joinToString { it.name }
|
||||
genre = tags
|
||||
?.filterKeys { it != "Group" && it != "Artist" }
|
||||
?.flatMap { it.value }
|
||||
?.joinToString { it.fullName ?: it.name }
|
||||
status = SManga.COMPLETED
|
||||
}
|
||||
|
||||
fun toSChapter() = SChapter.create().apply {
|
||||
name = "Chapter"
|
||||
url = format("/g/$slug")
|
||||
date_upload = try {
|
||||
date.toDate()!!.time
|
||||
} catch (e: Exception) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Tag(
|
||||
val name: String,
|
||||
@SerialName("full_name")
|
||||
val fullName: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PostDate(
|
||||
private val default: String,
|
||||
) {
|
||||
fun toDate(): Date? = createdAtFormat.parse(default)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ReaderProps(
|
||||
val pages: Pages,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class Pages(
|
||||
val data: PagesData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PagesData(
|
||||
val list: PagesList,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class PagesList(
|
||||
private val root: String,
|
||||
private val code: Int,
|
||||
private val head: List<String>,
|
||||
private val hash: List<String>,
|
||||
private val rand: List<String>,
|
||||
private val type: List<String>,
|
||||
) {
|
||||
fun toPages() = head
|
||||
.mapIndexed { index, pageNumber -> Triple(pageNumber, index, index) }
|
||||
.sortedBy { it.first.toInt() }
|
||||
.mapIndexed { sortedIndex, (pageNumber, originalIndex, _) ->
|
||||
Page(
|
||||
sortedIndex,
|
||||
imageUrl = format(
|
||||
"$root/$code/${
|
||||
pageNumber.padStart(4, '0')
|
||||
}-${hash[originalIndex]}-${rand[originalIndex]}.${type[originalIndex]}",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user