Atsumaru: Use new API (#10705)

* update api

* add manga type to genre

* oops

* show type first
This commit is contained in:
bapeey 2025-09-26 00:34:54 -05:00 committed by Draff
parent d824aa0a17
commit e70acec541
Signed by: Draff
GPG Key ID: E8A89F3211677653
3 changed files with 106 additions and 82 deletions

View File

@ -1,7 +1,7 @@
ext {
extName = 'Atsumaru'
extClass = '.Atsumaru'
extVersionCode = 2
extVersionCode = 3
isNsfw = true
}

View File

@ -8,19 +8,18 @@ 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 keiyoushi.utils.parseAs
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class Atsumaru : HttpSource() {
override val versionId = 2
override val name = "Atsumaru"
override val baseUrl = "https://atsu.moe"
private val apiUrl = "$baseUrl/api/v1"
override val lang = "en"
@ -32,29 +31,27 @@ class Atsumaru : HttpSource() {
private fun apiHeadersBuilder() = headersBuilder().apply {
add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host)
add("Host", "atsu.moe")
}
private val apiHeaders by lazy { apiHeadersBuilder().build() }
private val json: Json by injectLazy()
// ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request {
return GET("$apiUrl/layouts/s1/sliders/hotUpdates", apiHeaders)
return GET("$baseUrl/api/infinite/trending?page=${page - 1}", apiHeaders)
}
override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<BrowseMangaDto>().items
return MangasPage(data.map { it.manga.toSManga() }, false)
return MangasPage(data.map { it.toSManga(baseUrl) }, true)
}
// =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request {
return GET("$apiUrl/layouts/s1/latest-updates", apiHeaders)
return GET("$baseUrl/api/infinite/recentlyUpdated?page=${page - 1}", apiHeaders)
}
override fun latestUpdatesParse(response: Response): MangasPage {
@ -64,31 +61,38 @@ class Atsumaru : HttpSource() {
// =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/search".toHttpUrl().newBuilder()
.addPathSegment(query)
val url = "$baseUrl/collections/manga/documents/search".toHttpUrl().newBuilder()
.addQueryParameter("q", query)
.addQueryParameter("query_by", "title,englishTitle,otherNames")
.addQueryParameter("limit", "24")
.addQueryParameter("page", page.toString())
.addQueryParameter("query_by_weights", "3,2,1")
.addQueryParameter("include_fields", "id,title,englishTitle,poster")
.addQueryParameter("num_typos", "4,3,2")
.addQueryParameter("page", page.toString())
.build()
return GET(url, apiHeaders)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<SearchResultsDto>().hits
val data = response.parseAs<SearchResultsDto>()
return MangasPage(data.map { it.info.toSManga() }, false)
return MangasPage(data.hits.map { it.document.toSManga(baseUrl) }, data.hasNextPage())
}
// =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url
return "$baseUrl/manga/${manga.url}"
}
override fun mangaDetailsRequest(manga: SManga): Request {
return GET(apiUrl + manga.url, apiHeaders)
return GET("$baseUrl/api/manga/page?id=${manga.url}", apiHeaders)
}
override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<MangaObjectDto>().manga.toSManga()
return response.parseAs<MangaObjectDto>().mangaPage.toSManga(baseUrl)
}
// ============================== Chapters ==============================
@ -98,38 +102,31 @@ class Atsumaru : HttpSource() {
}
override fun chapterListParse(response: Response): List<SChapter> {
val chapterList = response.parseAs<MangaObjectDto>().manga.chapters!!.map {
return response.parseAs<MangaObjectDto>().mangaPage.chapters!!.map {
it.toSChapter(response.request.url.pathSegments.last())
}
return chapterList.sortedWith(
compareBy(
{ it.chapter_number },
{ it.scanlator },
),
).reversed()
}
override fun getChapterUrl(chapter: SChapter): String {
val (slug, name) = chapter.url.split("/")
return "$baseUrl/read/s1/$slug/$name/1"
return "$baseUrl/read/$slug/$name"
}
// =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request {
val (slug, name) = chapter.url.split("/")
return GET("$apiUrl/manga/s1/$slug#$name", apiHeaders)
val url = "$baseUrl/api/read/chapter".toHttpUrl().newBuilder()
.addQueryParameter("mangaId", slug)
.addQueryParameter("chapterId", name)
return GET(url.build(), apiHeaders)
}
override fun pageListParse(response: Response): List<Page> {
val chapter = response.parseAs<MangaObjectDto>().manga.chapters!!.first {
it.name == response.request.url.fragment
return response.parseAs<PageObjectDto>().readChapter.pages.mapIndexed { index, page ->
Page(index, imageUrl = baseUrl + page.image)
}
return chapter.pages.map { page ->
Page(page.name.toInt(), imageUrl = page.pageURLs.first())
}.sortedBy { it.index }
}
override fun imageRequest(page: Page): Request {
@ -144,10 +141,4 @@ class Atsumaru : HttpSource() {
override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException()
}
// ============================= Utilities ==============================
private inline fun <reified T> Response.parseAs(): T {
return json.decodeFromString(body.string())
}
}

View File

@ -2,92 +2,116 @@ package eu.kanade.tachiyomi.extension.en.atsumaru
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNames
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
class BrowseMangaDto(
val items: List<MangaObjectDto>,
val items: List<MangaDto>,
)
@Serializable
class MangaObjectDto(
val manga: MangaDto,
val mangaPage: MangaDto,
)
@Serializable
class SearchResultsDto(
val page: Int,
val found: Int,
val hits: List<SearchMangaDto>,
@SerialName("request_params") val requestParams: RequestParamsDto,
) {
fun hasNextPage(): Boolean {
return page * requestParams.perPage < found
}
@Serializable
class SearchMangaDto(
val info: MangaDto,
val document: MangaDto,
)
@Serializable
class RequestParamsDto(
@SerialName("per_page") val perPage: Int,
)
}
@Serializable
class MangaDto(
// Common
private val id: String,
private val title: String,
private val cover: String,
private val slug: String,
@JsonNames("poster", "image")
private val imagePath: JsonElement,
// Details
private val authors: List<String>? = null,
private val description: String? = null,
private val genres: List<String>? = null,
private val statuses: List<String>? = null,
private val authors: List<AuthorDto>? = null,
private val synopsis: String? = null,
private val tags: List<TagDto>? = null,
private val status: String? = null,
private val type: String? = null,
// Chapters
val chapters: List<ChapterDto>? = null,
) {
fun toSManga(): SManga = SManga.create().apply {
title = this@MangaDto.title
thumbnail_url = cover
url = "/manga/s1/$slug"
private fun getImagePath(): String? = when (imagePath) {
is JsonPrimitive -> imagePath.content
is JsonObject -> imagePath["image"]?.jsonPrimitive?.content
else -> null
}
fun toSManga(baseUrl: String): SManga = SManga.create().apply {
url = id
title = this@MangaDto.title
thumbnail_url = getImagePath().let { it -> baseUrl + it }
description = synopsis
genre = buildList {
type?.let { add(it) }
tags?.forEach { add(it.name) }
}.joinToString()
authors?.let {
author = it.joinToString()
author = it.joinToString { author -> author.name }
}
description = this@MangaDto.description
genres?.let {
genre = it.joinToString()
}
statuses?.let {
status = when (it.first().lowercase().substringBefore(" ")) {
this@MangaDto.status?.let {
status = when (it.lowercase().trim()) {
"ongoing" -> SManga.ONGOING
"complete" -> SManga.COMPLETED
else -> SManga.UNKNOWN
}
}
}
@Serializable
class TagDto(
val name: String,
)
@Serializable
class AuthorDto(
val name: String,
)
}
@Serializable
class ChapterDto(
val pages: List<PageDto>,
val name: String,
private val type: String,
private val title: String? = null,
private val date: String? = null,
private val id: String,
private val number: Float,
private val title: String,
@SerialName("createdAt") private val date: String? = null,
) {
fun toSChapter(slug: String): SChapter = SChapter.create().apply {
val chapterNumber = this@ChapterDto.name.replace("_", ".")
.filter { it.isDigit() || it == '.' }
name = buildString {
append("Chapter ")
append(chapterNumber)
if (title != null) {
append(" - ")
append(title)
}
}
url = "$slug/${this@ChapterDto.name}"
chapter_number = chapterNumber.toFloat()
scanlator = type.takeUnless { it == "Chapter" }
url = "$slug/$id"
chapter_number = number
name = title
date?.let {
date_upload = parseDate(it)
}
@ -109,7 +133,16 @@ class ChapterDto(
}
@Serializable
class PageDto(
val pageURLs: List<String>,
val name: String,
class PageObjectDto(
val readChapter: PageDto,
)
@Serializable
class PageDto(
val pages: List<PageDataDto>,
)
@Serializable
class PageDataDto(
val image: String,
)