Add Rawdevart.art (#813)

* Add Rawdevart.art

* Optimize icons
This commit is contained in:
beerpsi 2024-01-30 23:15:19 +07:00 committed by Draff
parent a548a4fb4a
commit dccbfba8a6
15 changed files with 402 additions and 0 deletions

View File

@ -0,0 +1,8 @@
ext {
extName = "Rawdevart.art"
extClass = ".Rawdevartart"
extVersionCode = 1
isNsfw = true
}
apply from: "$rootDir/common.gradle"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -0,0 +1,115 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.ChapterDetailsDto
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.MangaDetailsDto
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.PaginatedMangaList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Filter
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.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class Rawdevartart : HttpSource() {
override val name = "Rawdevart.art"
override val lang = "ja"
override val baseUrl = "https://rawdevart.art"
override val supportsLatest = true
override fun headersBuilder() = super.headersBuilder()
.add("Referer", "$baseUrl/")
private val json: Json by injectLazy()
override fun popularMangaRequest(page: Int) = searchMangaRequest(
page,
"",
FilterList(
SortFilter(1),
GenreFilter(genres),
),
)
override fun popularMangaParse(response: Response) = searchMangaParse(response)
override fun latestUpdatesRequest(page: Int) = searchMangaRequest(
page,
"",
FilterList(
SortFilter(0),
GenreFilter(genres),
),
)
override fun latestUpdatesParse(response: Response) = searchMangaParse(response)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val url = "$baseUrl/spa".toHttpUrl().newBuilder().apply {
if (query.isNotEmpty()) {
addPathSegment("search")
addQueryParameter("query", query)
addQueryParameter("page", page.toString())
return@apply
}
(if (filters.isEmpty()) getFilterList() else filters).forEach { f ->
when (f) {
is UriFilter -> f.addToUri(this)
is GenreFilter -> addPathSegments(f.values[f.state].path)
else -> {}
}
}
}.build()
return GET(url, headers)
}
override fun searchMangaParse(response: Response): MangasPage {
val data = response.parseAs<PaginatedMangaList>()
return data.toMangasPage()
}
override fun mangaDetailsParse(response: Response): SManga {
val data = response.parseAs<MangaDetailsDto>()
return data.toSManga()
}
override fun chapterListParse(response: Response): List<SChapter> {
val data = response.parseAs<MangaDetailsDto>()
return data.toSChapterList()
}
override fun pageListParse(response: Response): List<Page> {
val data = response.parseAs<ChapterDetailsDto>()
return data.toPageList(baseUrl)
}
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
override fun getFilterList() = FilterList(
Filter.Header("Filters are ignored when using text search."),
StatusFilter(),
SortFilter(),
GenreFilter(genres),
)
private inline fun <reified T> Response.parseAs(): T =
json.decodeFromString(body.string())
}

View File

@ -0,0 +1,106 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart
import eu.kanade.tachiyomi.source.model.Filter
import okhttp3.HttpUrl
interface UriFilter {
fun addToUri(builder: HttpUrl.Builder)
}
open class UriPartFilter(
name: String,
private val query: String,
private val vals: Array<Pair<String, String>>,
state: Int = 0,
) : UriFilter, Filter.Select<String>(name, vals.map { it.first }.toTypedArray(), state) {
override fun addToUri(builder: HttpUrl.Builder) {
builder.addQueryParameter(query, vals[state].second)
}
}
class StatusFilter : UriPartFilter(
"Status",
"status",
arrayOf(
"All" to "",
"Ongoing" to "ongoing",
"Completed" to "completed",
),
)
class SortFilter(state: Int = 1) : UriPartFilter(
"Sort by",
"sort",
arrayOf(
"Recently updated" to "",
"Most viewed" to "most_viewed",
"Most viewed today" to "most_viewed_today",
),
state,
)
data class Genre(val name: String, val path: String) {
override fun toString() = name
}
class GenreFilter(genres: Array<Genre>) : Filter.Select<Genre>("Genre", genres)
// copy([...$0.querySelectorAll("option")].filter((e) => e.value !== "/all").map((e) => `Genre("${e.textContent}", "${e.value.split("/").slice(1, 3).join("/")}"),`).join("\n"))
val genres = arrayOf(
Genre("All", "genres"),
Genre("action", "genre/85"),
Genre("adult", "genre/139"),
Genre("adventure", "genre/86"),
Genre("Alternative World", "genre/149"),
Genre("animated", "genre/140"),
Genre("comedy", "genre/87"),
Genre("cooking", "genre/134"),
Genre("drama", "genre/114"),
Genre("ecchi", "genre/88"),
Genre("Elves", "genre/150"),
Genre("fantasy", "genre/89"),
Genre("Food", "genre/152"),
Genre("Game", "genre/155"),
Genre("gender bender", "genre/111"),
Genre("harem", "genre/90"),
Genre("historical", "genre/115"),
Genre("horror", "genre/127"),
Genre("Isekai", "genre/144"),
Genre("josei", "genre/130"),
Genre("loli", "genre/91"),
Genre("Lolicon", "genre/148"),
Genre("Magic", "genre/151"),
Genre("manhua", "genre/128"),
Genre("manhwa", "genre/125"),
Genre("martial arts", "genre/126"),
Genre("mature", "genre/112"),
Genre("mecha", "genre/143"),
Genre("medical", "genre/132"),
Genre("moe", "genre/141"),
Genre("mystery", "genre/121"),
Genre("N/A", "genre/156"),
Genre("one shot", "genre/142"),
Genre("Oneshot", "genre/157"),
Genre("psychological", "genre/119"),
Genre("romance", "genre/106"),
Genre("school life", "genre/108"),
Genre("sci fi", "genre/122"),
Genre("Sci-fi", "genre/146"),
Genre("seinen", "genre/107"),
Genre("Shotacon", "genre/154"),
Genre("shoujo", "genre/120"),
Genre("shoujo ai", "genre/131"),
Genre("shounen", "genre/118"),
Genre("shounen ai", "genre/109"),
Genre("slice of life", "genre/92"),
Genre("smut", "genre/123"),
Genre("sports", "genre/124"),
Genre("supernatural", "genre/93"),
Genre("tragedy", "genre/135"),
Genre("trap (crossdressing)", "genre/138"),
Genre("Updating", "genre/147"),
Genre("War", "genre/153"),
Genre("webtoons", "genre/116"),
Genre("Yaoi", "genre/161"),
Genre("yuri", "genre/110"),
)

View File

@ -0,0 +1,88 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.ChapterDetailsDto
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.ChapterDto
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.MangaDetailsDto
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.MangaDto
import eu.kanade.tachiyomi.extension.ja.rawdevartart.dto.PaginatedMangaList
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 org.jsoup.Jsoup
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.text.SimpleDateFormat
import java.util.Locale
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
fun PaginatedMangaList.toMangasPage(): MangasPage {
val manga = mangaList.map { it.toSManga() }
val hasNextPage = (pagi.button?.next ?: 0) != 0
return MangasPage(manga, hasNextPage)
}
private fun MangaDto.toSManga() = SManga.create().apply {
// The website URL is manually calculated using a slugify function that I am too
// lazy to reimplement.
url = "/spa/manga/$id"
title = name
thumbnail_url = coverImage
}
fun MangaDetailsDto.toSManga() = SManga.create().apply {
title = detail.name
author = authors.joinToString { it.name }
description = buildString {
if (!detail.alternativeName.isNullOrEmpty()) {
append("Alternative Title: ")
appendLine(detail.alternativeName)
appendLine()
}
if (!detail.description.isNullOrEmpty()) {
append(detail.description)
}
}
genre = tags.joinToString { it.name }
status = if (detail.status) SManga.COMPLETED else SManga.ONGOING
thumbnail_url = detail.coverImageFull ?: detail.coverImage
}
fun MangaDetailsDto.toSChapterList() = chapters.map { it.toSChapter(detail.id) }
private fun ChapterDto.toSChapter(mangaId: Int) = SChapter.create().apply {
url = "/spa/manga/$mangaId/$number"
name = buildString {
append("Chapter ")
append(formatChapterNumber(number))
if (title.isNotEmpty()) {
append(": ")
append(title)
}
}
chapter_number = number
date_upload = runCatching {
dateFormat.parse(datePublished)!!.time
}.getOrDefault(0L)
}
fun ChapterDetailsDto.toPageList(baseUrl: String): List<Page> {
val document = Jsoup.parseBodyFragment(detail.content!!, baseUrl)
return document.select("div.chapter-img canvas").mapIndexed { i, it ->
Page(i, imageUrl = it.absUrl("data-srcset"))
}
}
private val formatter = DecimalFormat(
"#.###",
DecimalFormatSymbols().apply { decimalSeparator = '.' },
)
fun formatChapterNumber(chapterNumber: Float): String {
return formatter.format(chapterNumber)
}

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ChapterDetailsDto(
@SerialName("chapter_detail") val detail: ChapterDto,
)

View File

@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ChapterDto(
@SerialName("chapter_id") val id: String,
@SerialName("chapter_title") val title: String,
@SerialName("chapter_number") val number: Float,
@SerialName("chapter_date_published") val datePublished: String,
@SerialName("chapter_content") val content: String? = null,
)

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart.dto
import kotlinx.serialization.Serializable
@Serializable
data class MangaDetailsDto(
val detail: MangaDto,
val tags: List<TagDto>,
val authors: List<AuthorDto>,
val chapters: List<ChapterDto>,
)

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class MangaDto(
@SerialName("manga_name") val name: String,
@SerialName("manga_cover_img") val coverImage: String,
@SerialName("manga_id") val id: Int,
@SerialName("manga_others_name") val alternativeName: String? = null,
@SerialName("manga_status") val status: Boolean = false,
@SerialName("manga_description") val description: String? = null,
@SerialName("manga_cover_img_full") val coverImageFull: String? = null,
)

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ButtonData(
val prev: Int,
val next: Int,
)
@Serializable
data class PaginationData(
val button: ButtonData? = null,
)
@Serializable
data class PaginatedMangaList(
@SerialName("manga_list") val mangaList: List<MangaDto>,
val pagi: PaginationData,
)

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.extension.ja.rawdevartart.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TagDto(
@SerialName("tag_name") val name: String,
@SerialName("tag_id") val id: Int,
)
@Serializable
data class AuthorDto(
@SerialName("author_name") val name: String,
@SerialName("author_id") val id: Int,
)