add alandal ()

* add alandal

* clarify
This commit is contained in:
Secozzi 2024-02-29 13:27:58 +00:00 committed by Draff
parent 12e3079af3
commit 5cb0af3b2d
9 changed files with 403 additions and 0 deletions
src/en/alandal
build.gradle
res
mipmap-hdpi
mipmap-mdpi
mipmap-xhdpi
mipmap-xxhdpi
mipmap-xxxhdpi
src/eu/kanade/tachiyomi/extension/en/alandal

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

Binary file not shown.

After

(image error) Size: 3.3 KiB

Binary file not shown.

After

(image error) Size: 1.8 KiB

Binary file not shown.

After

(image error) Size: 4.7 KiB

Binary file not shown.

After

(image error) Size: 8.3 KiB

Binary file not shown.

After

(image error) Size: 11 KiB

@ -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())
}
}

@ -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>,
)
}
}
}

@ -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"),
),
)