Add Doujin.io - J18 (#1891)
* Add Doujin.io - J18 * Apply corrections * Reduce indentation
This commit is contained in:
parent
3f73aec7cf
commit
ebf7e277e3
8
src/en/doujinio/build.gradle
Normal file
8
src/en/doujinio/build.gradle
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
ext {
|
||||||
|
extName = 'Doujin.io - J18'
|
||||||
|
extClass = '.Doujinio'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
BIN
src/en/doujinio/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
src/en/doujinio/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
BIN
src/en/doujinio/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
src/en/doujinio/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
src/en/doujinio/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
src/en/doujinio/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
BIN
src/en/doujinio/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
src/en/doujinio/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
BIN
src/en/doujinio/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
src/en/doujinio/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
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…
x
Reference in New Issue
Block a user