From e70acec541f672959e2422b39cf3e7dd01b9ab93 Mon Sep 17 00:00:00 2001 From: bapeey <90949336+bapeey@users.noreply.github.com> Date: Fri, 26 Sep 2025 00:34:54 -0500 Subject: [PATCH] Atsumaru: Use new API (#10705) * update api * add manga type to genre * oops * show type first --- src/en/atsumaru/build.gradle | 2 +- .../extension/en/atsumaru/Atsumaru.kt | 69 +++++------ .../tachiyomi/extension/en/atsumaru/Dto.kt | 117 +++++++++++------- 3 files changed, 106 insertions(+), 82 deletions(-) diff --git a/src/en/atsumaru/build.gradle b/src/en/atsumaru/build.gradle index 0a34fd26d..97f4604ed 100644 --- a/src/en/atsumaru/build.gradle +++ b/src/en/atsumaru/build.gradle @@ -1,7 +1,7 @@ ext { extName = 'Atsumaru' extClass = '.Atsumaru' - extVersionCode = 2 + extVersionCode = 3 isNsfw = true } diff --git a/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Atsumaru.kt b/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Atsumaru.kt index 2e7f33540..479425093 100644 --- a/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Atsumaru.kt +++ b/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Atsumaru.kt @@ -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().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().hits + val data = response.parseAs() - 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().manga.toSManga() + return response.parseAs().mangaPage.toSManga(baseUrl) } // ============================== Chapters ============================== @@ -98,38 +102,31 @@ class Atsumaru : HttpSource() { } override fun chapterListParse(response: Response): List { - val chapterList = response.parseAs().manga.chapters!!.map { + return response.parseAs().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 { - val chapter = response.parseAs().manga.chapters!!.first { - it.name == response.request.url.fragment + return response.parseAs().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 Response.parseAs(): T { - return json.decodeFromString(body.string()) - } } diff --git a/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Dto.kt b/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Dto.kt index 5928fdeb7..a3aa9cda9 100644 --- a/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Dto.kt +++ b/src/en/atsumaru/src/eu/kanade/tachiyomi/extension/en/atsumaru/Dto.kt @@ -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, + val items: List, ) @Serializable class MangaObjectDto( - val manga: MangaDto, + val mangaPage: MangaDto, ) @Serializable class SearchResultsDto( + val page: Int, + val found: Int, val hits: List, + @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? = null, - private val description: String? = null, - private val genres: List? = null, - private val statuses: List? = null, + private val authors: List? = null, + private val synopsis: String? = null, + private val tags: List? = null, + private val status: String? = null, + private val type: String? = null, // Chapters val chapters: List? = 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, - 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, - val name: String, +class PageObjectDto( + val readChapter: PageDto, +) + +@Serializable +class PageDto( + val pages: List, +) + +@Serializable +class PageDataDto( + val image: String, )