Add MayoTune (#9106)

* Add MayoTune

* Use fallback for text contents

* Use DTO and refactor

* Use `selectFirst` for status

* Update fallback thumbnail URL

* Lint code

* Implement as single source

* Encapsulate `ChapterDto`

* Use relative URL

* Correctly handle request endpoints
This commit is contained in:
Nam Anh 2025-06-09 09:11:59 +07:00 committed by Draff
parent 5406227f0f
commit ef9d26cfe8
Signed by: Draff
GPG Key ID: E8A89F3211677653
8 changed files with 168 additions and 0 deletions

View File

@ -0,0 +1,9 @@
ext {
extName = 'MayoTune'
extClass = '.MayoTune'
extVersionCode = 1
baseUrl = 'https://mayotune.xyz'
isNsfw = false
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.extension.en.mayotune
import keiyoushi.utils.tryParse
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
@Serializable
data class ChapterDto(
val id: String,
val title: String,
val number: Float,
val pageCount: Int,
val date: String,
) {
@Contextual
private val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
fun getChapterURL(): String = "/chapter/${this.id}"
fun getNumberStr(): String = if (this.number % 1 == 0f) {
this.number.toInt().toString()
} else {
this.number.toString()
}
fun getChapterTitle(): String = if (!this.title.isEmpty()) {
"Chapter ${this.getNumberStr()}: ${this.title}"
} else {
"Chapter ${this.getNumberStr()}"
}
fun getDateTimestamp(): Long = this.sdf.tryParse(this.date)
}

View File

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.extension.en.mayotune
import eu.kanade.tachiyomi.network.GET
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 eu.kanade.tachiyomi.util.asJsoup
import keiyoushi.utils.parseAs
import okhttp3.Request
import okhttp3.Response
import rx.Observable
class MayoTune() : HttpSource() {
override val name: String = "MayoTune"
override val baseUrl: String = "https://mayotune.xyz"
override val lang: String = "en"
override val versionId: Int = 1
private val source = SManga.create().apply {
title = "Mayonaka Heart Tune"
url = "/"
thumbnail_url = "$baseUrl/img/cover.jpg"
author = "Masakuni Igarashi"
}
// Popular
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
return Observable.just(MangasPage(listOf(source), false))
}
override fun popularMangaRequest(page: Int): Request = throw UnsupportedOperationException()
override fun popularMangaParse(response: Response): MangasPage =
throw UnsupportedOperationException()
// Latest
override val supportsLatest: Boolean = true
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
return Observable.just(MangasPage(listOf(source), false))
}
override fun latestUpdatesRequest(page: Int): Request = throw UnsupportedOperationException()
override fun latestUpdatesParse(response: Response): MangasPage =
throw UnsupportedOperationException()
// Search
override fun fetchSearchManga(
page: Int,
query: String,
filters: FilterList,
): Observable<MangasPage> {
val mangas = mutableListOf<SManga>()
if (source.title.lowercase().contains(query.lowercase()) ||
source.author?.lowercase()?.contains(query.lowercase()) == true
) {
mangas.add(source)
}
return Observable.just(MangasPage(mangas, false))
}
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
throw UnsupportedOperationException()
override fun searchMangaParse(response: Response): MangasPage =
throw UnsupportedOperationException()
// Get Override
override fun chapterListRequest(manga: SManga): Request {
return GET("$baseUrl/api/chapters", headers)
}
// Details
override fun mangaDetailsParse(response: Response): SManga = SManga.create().apply {
val document = response.asJsoup()
val statusText =
document.selectFirst("div.text-center:contains(Status)")?.text()
?.substringBefore("Status")
?.trim()
url = source.url
title = source.title
artist = source.artist
author = source.author
description = document.selectFirst(".text-lg")?.text()
genre = document.selectFirst("span.text-sm:nth-child(2)")?.text()?.replace("", ",")
status = when (statusText) {
"Ongoing" -> SManga.ONGOING
"Completed" -> SManga.COMPLETED
"Cancelled" -> SManga.CANCELLED
"Hiatus" -> SManga.ON_HIATUS
"Finished" -> SManga.PUBLISHING_FINISHED
else -> SManga.UNKNOWN
}
thumbnail_url = document.selectFirst("img.object-contain")?.absUrl("src")
?.ifEmpty { source.thumbnail_url }
}
// Chapters
override fun chapterListParse(response: Response): List<SChapter> {
val chapters = response.parseAs<List<ChapterDto>>()
return chapters.sortedByDescending { it.number }.map { chapter ->
SChapter.create().apply {
url = chapter.getChapterURL()
name = chapter.getChapterTitle()
chapter_number = chapter.number
date_upload = chapter.getDateTimestamp()
}
}
}
// Pages
override fun pageListParse(response: Response): List<Page> {
val document = response.asJsoup()
return document.select("div.w-full > img").mapIndexed { index, img ->
Page(index, imageUrl = img.absUrl("src"))
}
}
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException()
}