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 { ext {
extName = 'Atsumaru' extName = 'Atsumaru'
extClass = '.Atsumaru' extClass = '.Atsumaru'
extVersionCode = 2 extVersionCode = 3
isNsfw = true 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.decodeFromString import keiyoushi.utils.parseAs
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class Atsumaru : HttpSource() { class Atsumaru : HttpSource() {
override val versionId = 2
override val name = "Atsumaru" override val name = "Atsumaru"
override val baseUrl = "https://atsu.moe" override val baseUrl = "https://atsu.moe"
private val apiUrl = "$baseUrl/api/v1"
override val lang = "en" override val lang = "en"
@ -32,29 +31,27 @@ class Atsumaru : HttpSource() {
private fun apiHeadersBuilder() = headersBuilder().apply { private fun apiHeadersBuilder() = headersBuilder().apply {
add("Accept", "*/*") add("Accept", "*/*")
add("Host", apiUrl.toHttpUrl().host) add("Host", "atsu.moe")
} }
private val apiHeaders by lazy { apiHeadersBuilder().build() } private val apiHeaders by lazy { apiHeadersBuilder().build() }
private val json: Json by injectLazy()
// ============================== Popular =============================== // ============================== Popular ===============================
override fun popularMangaRequest(page: Int): Request { 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 { override fun popularMangaParse(response: Response): MangasPage {
val data = response.parseAs<BrowseMangaDto>().items val data = response.parseAs<BrowseMangaDto>().items
return MangasPage(data.map { it.manga.toSManga() }, false) return MangasPage(data.map { it.toSManga(baseUrl) }, true)
} }
// =============================== Latest =============================== // =============================== Latest ===============================
override fun latestUpdatesRequest(page: Int): Request { 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 { override fun latestUpdatesParse(response: Response): MangasPage {
@ -64,31 +61,38 @@ class Atsumaru : HttpSource() {
// =============================== Search =============================== // =============================== Search ===============================
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$apiUrl/search".toHttpUrl().newBuilder() val url = "$baseUrl/collections/manga/documents/search".toHttpUrl().newBuilder()
.addPathSegment(query) .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() .build()
return GET(url, apiHeaders) return GET(url, apiHeaders)
} }
override fun searchMangaParse(response: Response): MangasPage { 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 ============================ // =========================== Manga Details ============================
override fun getMangaUrl(manga: SManga): String { override fun getMangaUrl(manga: SManga): String {
return baseUrl + manga.url return "$baseUrl/manga/${manga.url}"
} }
override fun mangaDetailsRequest(manga: SManga): Request { 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 { override fun mangaDetailsParse(response: Response): SManga {
return response.parseAs<MangaObjectDto>().manga.toSManga() return response.parseAs<MangaObjectDto>().mangaPage.toSManga(baseUrl)
} }
// ============================== Chapters ============================== // ============================== Chapters ==============================
@ -98,38 +102,31 @@ class Atsumaru : HttpSource() {
} }
override fun chapterListParse(response: Response): List<SChapter> { 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()) it.toSChapter(response.request.url.pathSegments.last())
} }
return chapterList.sortedWith(
compareBy(
{ it.chapter_number },
{ it.scanlator },
),
).reversed()
} }
override fun getChapterUrl(chapter: SChapter): String { override fun getChapterUrl(chapter: SChapter): String {
val (slug, name) = chapter.url.split("/") val (slug, name) = chapter.url.split("/")
return "$baseUrl/read/s1/$slug/$name/1" return "$baseUrl/read/$slug/$name"
} }
// =============================== Pages ================================ // =============================== Pages ================================
override fun pageListRequest(chapter: SChapter): Request { override fun pageListRequest(chapter: SChapter): Request {
val (slug, name) = chapter.url.split("/") 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> { override fun pageListParse(response: Response): List<Page> {
val chapter = response.parseAs<MangaObjectDto>().manga.chapters!!.first { return response.parseAs<PageObjectDto>().readChapter.pages.mapIndexed { index, page ->
it.name == response.request.url.fragment 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 { override fun imageRequest(page: Page): Request {
@ -144,10 +141,4 @@ class Atsumaru : HttpSource() {
override fun imageUrlParse(response: Response): String { override fun imageUrlParse(response: Response): String {
throw UnsupportedOperationException() 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.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable 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.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
@Serializable @Serializable
class BrowseMangaDto( class BrowseMangaDto(
val items: List<MangaObjectDto>, val items: List<MangaDto>,
) )
@Serializable @Serializable
class MangaObjectDto( class MangaObjectDto(
val manga: MangaDto, val mangaPage: MangaDto,
) )
@Serializable @Serializable
class SearchResultsDto( class SearchResultsDto(
val page: Int,
val found: Int,
val hits: List<SearchMangaDto>, val hits: List<SearchMangaDto>,
@SerialName("request_params") val requestParams: RequestParamsDto,
) { ) {
fun hasNextPage(): Boolean {
return page * requestParams.perPage < found
}
@Serializable @Serializable
class SearchMangaDto( class SearchMangaDto(
val info: MangaDto, val document: MangaDto,
)
@Serializable
class RequestParamsDto(
@SerialName("per_page") val perPage: Int,
) )
} }
@Serializable @Serializable
class MangaDto( class MangaDto(
// Common // Common
private val id: String,
private val title: String, private val title: String,
private val cover: String, @JsonNames("poster", "image")
private val slug: String, private val imagePath: JsonElement,
// Details // Details
private val authors: List<String>? = null, private val authors: List<AuthorDto>? = null,
private val description: String? = null, private val synopsis: String? = null,
private val genres: List<String>? = null, private val tags: List<TagDto>? = null,
private val statuses: List<String>? = null, private val status: String? = null,
private val type: String? = null,
// Chapters // Chapters
val chapters: List<ChapterDto>? = null, val chapters: List<ChapterDto>? = null,
) { ) {
fun toSManga(): SManga = SManga.create().apply { private fun getImagePath(): String? = when (imagePath) {
title = this@MangaDto.title is JsonPrimitive -> imagePath.content
thumbnail_url = cover is JsonObject -> imagePath["image"]?.jsonPrimitive?.content
url = "/manga/s1/$slug" 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 { authors?.let {
author = it.joinToString() author = it.joinToString { author -> author.name }
} }
description = this@MangaDto.description this@MangaDto.status?.let {
genres?.let { status = when (it.lowercase().trim()) {
genre = it.joinToString()
}
statuses?.let {
status = when (it.first().lowercase().substringBefore(" ")) {
"ongoing" -> SManga.ONGOING "ongoing" -> SManga.ONGOING
"complete" -> SManga.COMPLETED "complete" -> SManga.COMPLETED
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
} }
} }
@Serializable
class TagDto(
val name: String,
)
@Serializable
class AuthorDto(
val name: String,
)
} }
@Serializable @Serializable
class ChapterDto( class ChapterDto(
val pages: List<PageDto>, private val id: String,
val name: String, private val number: Float,
private val type: String, private val title: String,
private val title: String? = null, @SerialName("createdAt") private val date: String? = null,
private val date: String? = null,
) { ) {
fun toSChapter(slug: String): SChapter = SChapter.create().apply { fun toSChapter(slug: String): SChapter = SChapter.create().apply {
val chapterNumber = this@ChapterDto.name.replace("_", ".") url = "$slug/$id"
.filter { it.isDigit() || it == '.' } chapter_number = number
name = title
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" }
date?.let { date?.let {
date_upload = parseDate(it) date_upload = parseDate(it)
} }
@ -109,7 +133,16 @@ class ChapterDto(
} }
@Serializable @Serializable
class PageDto( class PageObjectDto(
val pageURLs: List<String>, val readChapter: PageDto,
val name: String, )
@Serializable
class PageDto(
val pages: List<PageDataDto>,
)
@Serializable
class PageDataDto(
val image: String,
) )