Add Doujin.io - J18 (#1891)

* Add Doujin.io - J18

* Apply corrections

* Reduce indentation
This commit is contained in:
Fermín Cirella 2024-03-16 17:32:46 -03:00 committed by Draff
parent 3f73aec7cf
commit ebf7e277e3
9 changed files with 332 additions and 0 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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,
)

View File

@ -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()}"