add alandal (#1600)

* add alandal

* clarify
This commit is contained in:
Secozzi 2024-02-29 13:27:58 +00:00 committed by GitHub
parent 5c5be6379c
commit b873136c0a
9 changed files with 403 additions and 0 deletions

View File

@ -0,0 +1,7 @@
ext {
extName = 'Alandal'
extClass = '.Alandal'
extVersionCode = 1
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,189 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.network.GET
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.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class Alandal : HttpSource() {
override val name = "Alandal"
override val baseUrl = "https://alandal.com"
private val apiUrl = "https://qq.alandal.com/api"
override val lang = "en"
override val supportsLatest = true
override val client = network.cloudflareClient.newBuilder()
.rateLimit(1)
.build()
override fun headersBuilder() = super.headersBuilder().apply {
add("Referer", "$baseUrl/")
}
private val apiHeaders by lazy { apiHeadersBuilder.build() }
private val apiHeadersBuilder = headersBuilder().apply {
add("Accept", "application/json")
add("Host", apiUrl.toHttpUrl().host)
add("Origin", baseUrl)
add("Sec-Fetch-Dest", "empty")
add("Sec-Fetch-Mode", "cors")
add("Sec-Fetch-Site", "same-origin")
}
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(SortFilter("popular")))
override fun popularMangaParse(response: Response): MangasPage =
searchMangaParse(response)
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList(SortFilter("new")))
override fun latestUpdatesParse(response: Response): MangasPage =
searchMangaParse(response)
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegment("series")
if (query.isNotBlank()) {
addQueryParameter("name", query)
}
addQueryParameter("type", "comic")
val filterList = filters.ifEmpty { getFilterList() }
filterList.filterIsInstance<UriFilter>().forEach {
it.addToUri(this)
}
addQueryParameter("page", page.toString())
}.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<ResponseDto<SearchSeriesDto>>().data.series
val mangaList = data.data.map { it.toSManga() }
val hasNextPage = data.currentPage < data.lastPage
return MangasPage(mangaList, hasNextPage)
}
// =============================== Filters ==============================
override fun getFilterList(): FilterList = FilterList(
GenreFilter(),
SortFilter(),
StatusFilter(),
)
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String =
baseUrl + manga.url.replace("series/", "series/comic-")
override fun mangaDetailsRequest(manga: SManga): Request {
val url = apiUrl.toHttpUrl().newBuilder().apply {
addPathSegments(manga.url.substringAfter("/"))
addQueryParameter("type", "comic")
}.build()
return GET(url, apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga =
response.parseAs<ResponseDto<MangaDetailsDto>>().data.series.toSManga()
// ============================== Chapters ==============================
override fun getChapterUrl(chapter: SChapter): String {
return baseUrl + chapter.url
.replace("series/", "chapter/comic-")
.replace("chapters/", "")
}
override fun chapterListRequest(manga: SManga): Request {
val url = "$apiUrl${manga.url}".toHttpUrl().newBuilder().apply {
addPathSegment("chapters")
addQueryParameter("type", "comic")
addQueryParameter("from", "0")
addQueryParameter("to", "999")
}.build()
return GET(url, apiHeaders)
}
override fun chapterListParse(response: Response): List<SChapter> {
val slug = response.request.url.newBuilder()
.query(null)
.removePathSegment(0) // Remove /api
.build()
.encodedPath
return response.parseAs<ChapterResponseDto>().data.map {
it.toSChapter(slug)
}.reversed()
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
if (chapter.name.startsWith("[LOCKED]")) {
throw Exception("Log in and unlock chapter in webview, then refresh chapter list")
}
val url = "$apiUrl${chapter.url}".toHttpUrl().newBuilder().apply {
addQueryParameter("type", "comic")
addQueryParameter("traveler", "0")
}.build()
return GET(url, apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<PagesResponseDto>().data.chapter.chapter
return data.pages.mapIndexed { index, s ->
Page(index, imageUrl = s)
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
override fun imageRequest(page: Page): Request {
val pageHeaders = headersBuilder().apply {
add("Accept", "image/avif,image/webp,*/*")
add("Host", page.imageUrl!!.toHttpUrl().host)
}.build()
return GET(page.imageUrl!!, pageHeaders)
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T = use {
json.decodeFromStream(it.body.byteStream())
}
}

View File

@ -0,0 +1,118 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jsoup.Jsoup
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class ResponseDto<T>(
val data: ResultDto<T>,
) {
@Serializable
class ResultDto<T>(
val series: T,
)
}
@Serializable
class SearchSeriesDto(
@SerialName("current_page") val currentPage: Int,
@SerialName("last_page") val lastPage: Int,
val data: List<SearchEntryDto>,
) {
@Serializable
class SearchEntryDto(
val name: String,
val slug: String,
val cover: String,
) {
fun toSManga(): SManga = SManga.create().apply {
title = name
url = "/series/$slug"
thumbnail_url = cover
}
}
}
@Serializable
class MangaDetailsDto(
val name: String,
val summary: String,
val status: NamedObject,
val genres: List<NamedObject>,
val creators: List<NamedObject>,
val cover: String,
) {
@Serializable
class NamedObject(
val name: String,
val type: String? = null,
)
fun toSManga(): SManga = SManga.create().apply {
title = name
thumbnail_url = cover
description = Jsoup.parseBodyFragment(summary).text()
genre = genres.joinToString { it.name }
author = creators.filter { it.type!! == "author" }.joinToString { it.name }
status = this@MangaDetailsDto.status.name.parseStatus()
}
private fun String.parseStatus(): Int = when (this.lowercase()) {
"ongoing" -> SManga.ONGOING
"completed" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
@Serializable
class ChapterResponseDto(
val data: List<ChapterDto>,
) {
@Serializable
class ChapterDto(
val name: String,
@SerialName("published_at") val published: String,
val access: Boolean,
) {
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
val prefix = if (access) "" else "[LOCKED] "
name = "${prefix}Chapter ${this@ChapterDto.name}"
date_upload = try {
dateFormat.parse(published)!!.time
} catch (_: ParseException) {
0L
}
url = "$slug/${this@ChapterDto.name}"
}
companion object {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'", Locale.ENGLISH)
}
}
}
@Serializable
class PagesResponseDto(
val data: PagesDataDto,
) {
@Serializable
class PagesDataDto(
val chapter: PagesChapterDto,
) {
@Serializable
class PagesChapterDto(
val chapter: PagesChapterImagesDto,
) {
@Serializable
class PagesChapterImagesDto(
val pages: List<String>,
)
}
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.extension.en.alandal
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
defaultValue: String? = null,
) : Filter.Select<String>(
name,
vals.map { it.first }.toTypedArray(),
vals.indexOfFirst { it.second == defaultValue }.takeIf { it != -1 } ?: 0,
),
UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(param, vals[state].second)
}
}
open class UriMultiSelectOption(name: String, val value: String) : Filter.CheckBox(name)
open class UriMultiSelectFilter(
name: String,
private val param: String,
private val vals: Array<Pair<String, String>>,
) : Filter.Group<UriMultiSelectOption>(name, vals.map { UriMultiSelectOption(it.first, it.second) }), UriFilter {
override fun addToUri(builder: HttpUrl.Builder) {
val checked = state.filter { it.state }
if (checked.isEmpty()) {
builder.addQueryParameter(param, "-1")
} else {
checked.forEach {
builder.addQueryParameter(param, it.value)
}
}
}
}
class GenreFilter : UriMultiSelectFilter(
"Genre",
"genres",
arrayOf(
Pair("Action", "1"),
Pair("Fantasy", "2"),
Pair("Regression", "3"),
Pair("Overpowered", "4"),
Pair("Ascension", "5"),
Pair("Revenge", "6"),
Pair("Martial Arts", "7"),
Pair("Magic", "8"),
Pair("Necromancer", "9"),
Pair("Adventure", "10"),
Pair("Tower", "11"),
Pair("Dungeons", "12"),
Pair("Psychological", "13"),
Pair("Isekai", "14"),
),
)
class SortFilter(defaultSort: String? = null) : UriPartFilter(
"Sort By",
"sort",
arrayOf(
Pair("Popularity", "popular"),
Pair("Name", "name"),
Pair("Chapters", "chapters"),
Pair("Rating", "Rating"),
Pair("New", "new"),
),
defaultSort,
)
class StatusFilter : UriPartFilter(
"Status",
"status",
arrayOf(
Pair("Any", "-1"),
Pair("Ongoing", "1"),
Pair("Coming Soon", "5"),
Pair("Completed", "6"),
),
)