diff --git a/src/ja/rawdevartart/build.gradle b/src/ja/rawdevartart/build.gradle new file mode 100644 index 000000000..90dd6a353 --- /dev/null +++ b/src/ja/rawdevartart/build.gradle @@ -0,0 +1,8 @@ +ext { + extName = "Rawdevart.art" + extClass = ".Rawdevartart" + extVersionCode = 1 + isNsfw = true +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/rawdevartart/res/mipmap-hdpi/ic_launcher.png b/src/ja/rawdevartart/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..96349c2d3 Binary files /dev/null and b/src/ja/rawdevartart/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src/ja/rawdevartart/res/mipmap-mdpi/ic_launcher.png b/src/ja/rawdevartart/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..753515c7e Binary files /dev/null and b/src/ja/rawdevartart/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src/ja/rawdevartart/res/mipmap-xhdpi/ic_launcher.png b/src/ja/rawdevartart/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..6d28247e4 Binary files /dev/null and b/src/ja/rawdevartart/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src/ja/rawdevartart/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/rawdevartart/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..e16ffeb04 Binary files /dev/null and b/src/ja/rawdevartart/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src/ja/rawdevartart/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/rawdevartart/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..5821c963f Binary files /dev/null and b/src/ja/rawdevartart/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/Rawdevartart.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/Rawdevartart.kt new file mode 100644 index 000000000..fec3fb0bf --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/Rawdevartart.kt @@ -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() + + return data.toMangasPage() + } + + override fun mangaDetailsParse(response: Response): SManga { + val data = response.parseAs() + + return data.toSManga() + } + + override fun chapterListParse(response: Response): List { + val data = response.parseAs() + + return data.toSChapterList() + } + + override fun pageListParse(response: Response): List { + val data = response.parseAs() + + 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 Response.parseAs(): T = + json.decodeFromString(body.string()) +} diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/RawdevartartFilters.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/RawdevartartFilters.kt new file mode 100644 index 000000000..bc5534fc1 --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/RawdevartartFilters.kt @@ -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>, + state: Int = 0, +) : UriFilter, Filter.Select(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) : Filter.Select("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"), +) diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/RawdevartartUtils.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/RawdevartartUtils.kt new file mode 100644 index 000000000..0d2b24dfc --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/RawdevartartUtils.kt @@ -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 { + 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) +} diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/ChapterDetailsDto.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/ChapterDetailsDto.kt new file mode 100644 index 000000000..69ff1ce8a --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/ChapterDetailsDto.kt @@ -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, +) diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/ChapterDto.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/ChapterDto.kt new file mode 100644 index 000000000..17834a2e9 --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/ChapterDto.kt @@ -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, +) diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/MangaDetailsDto.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/MangaDetailsDto.kt new file mode 100644 index 000000000..0bcba45f4 --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/MangaDetailsDto.kt @@ -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, + val authors: List, + val chapters: List, +) diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/MangaDto.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/MangaDto.kt new file mode 100644 index 000000000..3478609e6 --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/MangaDto.kt @@ -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, +) diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/PaginatedMangaList.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/PaginatedMangaList.kt new file mode 100644 index 000000000..7220db7c9 --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/PaginatedMangaList.kt @@ -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, + val pagi: PaginationData, +) diff --git a/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/TagDto.kt b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/TagDto.kt new file mode 100644 index 000000000..d3aca79ec --- /dev/null +++ b/src/ja/rawdevartart/src/eu/kanade/tachiyomi/extension/ja/rawdevartart/dto/TagDto.kt @@ -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, +)