Add Doujin.io - J18 (#1891)
* Add Doujin.io - J18 * Apply corrections * Reduce indentation
This commit is contained in:
parent
3f73aec7cf
commit
ebf7e277e3
|
@ -0,0 +1,8 @@
|
|||
ext {
|
||||
extName = 'Doujin.io - J18'
|
||||
extClass = '.Doujinio'
|
||||
extVersionCode = 1
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -0,0 +1,144 @@
|
|||
package eu.kanade.tachiyomi.extension.en.doujinio
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.interceptor.rateLimitHost
|
||||
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.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
|
||||
const val LATEST_LIMIT = 20
|
||||
|
||||
class Doujinio : HttpSource() {
|
||||
override val name = "Doujin.io - J18"
|
||||
|
||||
override val baseUrl = "https://doujin.io"
|
||||
|
||||
override val lang = "en"
|
||||
|
||||
override val supportsLatest = true
|
||||
|
||||
override val client = network.cloudflareClient.newBuilder()
|
||||
.rateLimitHost(baseUrl.toHttpUrl(), 2)
|
||||
.build()
|
||||
|
||||
// Latest
|
||||
|
||||
override fun latestUpdatesRequest(page: Int) =
|
||||
POST(
|
||||
"$baseUrl/api/mangas/newest",
|
||||
headers,
|
||||
body = json.encodeToString(
|
||||
LatestRequest(
|
||||
limit = LATEST_LIMIT,
|
||||
offset = (page - 1) * LATEST_LIMIT,
|
||||
),
|
||||
).toRequestBody("application/json".toMediaType()),
|
||||
)
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||
val latest = response.parseAs<List<Manga>>().map { it.toSManga() }
|
||||
|
||||
return MangasPage(latest, hasNextPage = latest.size >= LATEST_LIMIT)
|
||||
}
|
||||
|
||||
// Popular
|
||||
|
||||
override fun popularMangaRequest(page: Int) = GET("$baseUrl/api/mangas/popular", headers)
|
||||
|
||||
override fun popularMangaParse(response: Response) = MangasPage(
|
||||
response.parseAs<List<Manga>>().map { it.toSManga() },
|
||||
hasNextPage = false,
|
||||
)
|
||||
|
||||
// Search
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) = POST(
|
||||
"$baseUrl/api/mangas/search",
|
||||
headers,
|
||||
body = json.encodeToString(
|
||||
SearchRequest(
|
||||
keyword = query,
|
||||
page = page,
|
||||
tags = filters.findInstance<TagGroup>()?.state?.filter { it.state }?.mapNotNull {
|
||||
tags.find { tag -> tag.name == it.name }?.id
|
||||
} ?: emptyList(),
|
||||
),
|
||||
).toRequestBody("application/json".toMediaType()),
|
||||
)
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val result = response.parseAs<SearchResponse>()
|
||||
|
||||
return MangasPage(
|
||||
result.data.map { it.toSManga() },
|
||||
hasNextPage = result.to?.let { it < result.total } ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
// Details
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga) =
|
||||
GET("https://doujin.io/api/mangas/${getIdFromUrl(manga.url)}", headers)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = response.parseAs<Manga>().toSManga()
|
||||
|
||||
override fun getMangaUrl(manga: SManga) = "$baseUrl/manga/${getIdFromUrl(manga.url)}"
|
||||
|
||||
// Chapter
|
||||
|
||||
override fun chapterListRequest(manga: SManga) =
|
||||
GET("$baseUrl/api/chapters?manga_id=${getIdFromUrl(manga.url)}", headers)
|
||||
|
||||
override fun chapterListParse(response: Response) =
|
||||
response.parseAs<List<Chapter>>().map { it.toSChapter() }.reversed()
|
||||
|
||||
// Page List
|
||||
|
||||
override fun pageListRequest(chapter: SChapter) =
|
||||
GET(
|
||||
"$baseUrl/api/mangas/${getIdsFromUrl(chapter.url)}/manifest",
|
||||
headers.newBuilder().apply {
|
||||
add(
|
||||
"referer",
|
||||
"https://doujin.io/manga/${getIdsFromUrl(chapter.url).split("/").joinToString("/chapter/")}",
|
||||
)
|
||||
}.build(),
|
||||
)
|
||||
|
||||
override fun pageListParse(response: Response) =
|
||||
if (response.headers["content-type"] == "text/html; charset=UTF-8") {
|
||||
throw Exception("You need to login first through the WebView to read the chapter.")
|
||||
} else {
|
||||
json.decodeFromString<ChapterManifest>(
|
||||
response.body.string(),
|
||||
).toPageList()
|
||||
}
|
||||
|
||||
override fun imageUrlParse(response: Response) = throw UnsupportedOperationException()
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
TagGroup(),
|
||||
)
|
||||
|
||||
private class TagFilter(name: String) : Filter.CheckBox(name, false)
|
||||
|
||||
private class TagGroup : Filter.Group<TagFilter>(
|
||||
"Tags",
|
||||
tags.map { TagFilter(it.name) },
|
||||
)
|
||||
|
||||
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||
|
||||
private inline fun <reified T> Response.parseAs(): T =
|
||||
json.decodeFromString<PageResponse<T>>(body.string()).data
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package eu.kanade.tachiyomi.extension.en.doujinio
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
class PageResponse<T>(val data: T)
|
||||
|
||||
@Serializable
|
||||
class Manga(
|
||||
@SerialName("optimus_id")
|
||||
private val id: Int,
|
||||
private val title: String,
|
||||
private val description: String,
|
||||
private val thumb: String,
|
||||
private val tags: List<Tag>,
|
||||
@SerialName("creator_name")
|
||||
private val artist: String,
|
||||
) {
|
||||
fun toSManga() = SManga.create().apply {
|
||||
url = "/manga/$id"
|
||||
title = this@Manga.title
|
||||
description = this@Manga.description
|
||||
thumbnail_url = this@Manga.thumb
|
||||
artist = this@Manga.artist
|
||||
genre = this@Manga.tags.joinToString(", ") { it.name }
|
||||
status = SManga.COMPLETED
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class Tag(val id: Int, val name: String)
|
||||
|
||||
@Serializable
|
||||
class Chapter(
|
||||
@SerialName("optimus_id")
|
||||
private val id: Int,
|
||||
@SerialName("manga_optimus_id")
|
||||
private val mangaId: Int,
|
||||
@SerialName("chapter_name")
|
||||
private val name: String,
|
||||
@SerialName("chapter_order")
|
||||
private val order: Int,
|
||||
@SerialName("published_at")
|
||||
private val publishedAt: String,
|
||||
) {
|
||||
fun toSChapter() = SChapter.create().apply {
|
||||
url = "manga/$mangaId/chapter/$id"
|
||||
name = this@Chapter.name
|
||||
chapter_number = (order + 1).toFloat()
|
||||
date_upload = parseDate(publishedAt)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class ChapterMetadata(val identifier: String)
|
||||
|
||||
@Serializable
|
||||
class ChapterPage(val href: String)
|
||||
|
||||
@Serializable
|
||||
class ChapterManifest(
|
||||
private val metadata: ChapterMetadata,
|
||||
@SerialName("readingOrder")
|
||||
private val pages: List<ChapterPage>,
|
||||
) {
|
||||
fun toPageList() = pages.mapIndexed { i, page ->
|
||||
Page(
|
||||
index = i,
|
||||
url = metadata.identifier,
|
||||
imageUrl = page.href,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
class LatestRequest(
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class SearchRequest(
|
||||
val keyword: String,
|
||||
val page: Int,
|
||||
val tags: List<Int> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
class SearchResponse(
|
||||
val data: List<Manga>,
|
||||
val to: Int?,
|
||||
val total: Int,
|
||||
)
|
|
@ -0,0 +1,82 @@
|
|||
package eu.kanade.tachiyomi.extension.en.doujinio
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
val json: Json by injectLazy()
|
||||
|
||||
val tags = listOf(
|
||||
Tag(id = 22, name = "Aggressive Sex"),
|
||||
Tag(id = 23, name = "Anal"),
|
||||
Tag(id = 104, name = "BBM"),
|
||||
Tag(id = 105, name = "BSS"),
|
||||
Tag(id = 62, name = "Big Breasts"),
|
||||
Tag(id = 26, name = "Blowjob"),
|
||||
Tag(id = 27, name = "Bondage"),
|
||||
Tag(id = 29, name = "Cheating"),
|
||||
Tag(id = 32, name = "Creampie"),
|
||||
Tag(id = 33, name = "Crossdressing"),
|
||||
Tag(id = 34, name = "Cunnilingus"),
|
||||
Tag(id = 35, name = "Dark Skin"),
|
||||
Tag(id = 36, name = "Defloration"),
|
||||
Tag(id = 38, name = "Demon Girl"),
|
||||
Tag(id = 51, name = "Dickgirl"),
|
||||
Tag(id = 112, name = "Doll Joints"),
|
||||
Tag(id = 41, name = "Elf"),
|
||||
Tag(id = 106, name = "Exhibitionism"),
|
||||
Tag(id = 107, name = "Family"),
|
||||
Tag(id = 44, name = "Femdom"),
|
||||
Tag(id = 46, name = "Footjob"),
|
||||
Tag(id = 49, name = "Full Color"),
|
||||
Tag(id = 50, name = "Furry"),
|
||||
Tag(id = 53, name = "Gender Bender"),
|
||||
Tag(id = 54, name = "Group"),
|
||||
Tag(id = 55, name = "Gyaru"),
|
||||
Tag(id = 56, name = "Gym Uniform"),
|
||||
Tag(id = 114, name = "Kemonomimi"),
|
||||
Tag(id = 61, name = "Lactation"),
|
||||
Tag(id = 9, name = "Maid Uniform"),
|
||||
Tag(id = 65, name = "Mind Control"),
|
||||
Tag(id = 108, name = "Mindbreak"),
|
||||
Tag(id = 109, name = "Monster Girl"),
|
||||
Tag(id = 69, name = "Muscle"),
|
||||
Tag(id = 71, name = "Netorare"),
|
||||
Tag(id = 73, name = "Ninja Outfit"),
|
||||
Tag(id = 74, name = "Non-H"),
|
||||
Tag(id = 75, name = "Nun Outfit"),
|
||||
Tag(id = 76, name = "Nurse Outfit"),
|
||||
Tag(id = 78, name = "Old Man"),
|
||||
Tag(id = 82, name = "Pay To Play"),
|
||||
Tag(id = 80, name = "Petite"),
|
||||
Tag(id = 81, name = "Pregnant"),
|
||||
Tag(id = 83, name = "Rimjob"),
|
||||
Tag(id = 84, name = "School Uniform"),
|
||||
Tag(id = 110, name = "Small Breasts"),
|
||||
Tag(id = 63, name = "Solo Action"),
|
||||
Tag(id = 90, name = "Swimsuit"),
|
||||
Tag(id = 91, name = "Tanlines"),
|
||||
Tag(id = 92, name = "Tentacles"),
|
||||
Tag(id = 93, name = "Titjob"),
|
||||
Tag(id = 94, name = "Toys"),
|
||||
Tag(id = 95, name = "Urination"),
|
||||
Tag(id = 99, name = "Yaoi"),
|
||||
)
|
||||
|
||||
fun parseDate(dateStr: String): Long {
|
||||
return try {
|
||||
dateFormat.parse(dateStr)!!.time
|
||||
} catch (_: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat by lazy {
|
||||
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH)
|
||||
}
|
||||
|
||||
fun getIdFromUrl(url: String) = url.split("/").last()
|
||||
|
||||
fun getIdsFromUrl(url: String) = "${url.split("/")[1]}/${url.split("/").last()}"
|
Loading…
Reference in New Issue