Initial commit of new Doujins extension (#5261)
This commit is contained in:
parent
fc5024e7a3
commit
cfaca77d60
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,13 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'Doujins'
|
||||||
|
pkgNameSuffix = 'en.doujins'
|
||||||
|
extClass = '.Doujins'
|
||||||
|
extVersionCode = 1
|
||||||
|
libVersion = '1.2'
|
||||||
|
containsNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.0 KiB |
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,253 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.en.doujins
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import eu.kanade.tachiyomi.annotations.Nsfw
|
||||||
|
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 eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import org.jsoup.nodes.Document
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Nsfw
|
||||||
|
class Doujins : HttpSource() {
|
||||||
|
|
||||||
|
override val baseUrl: String = "https://doujins.com"
|
||||||
|
|
||||||
|
override val lang: String = "en"
|
||||||
|
|
||||||
|
override val name: String = "Doujins"
|
||||||
|
|
||||||
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
|
private val gson = Gson()
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListParse(response: Response): List<SChapter> {
|
||||||
|
return listOf(
|
||||||
|
SChapter.create().apply {
|
||||||
|
name = "Chapter"
|
||||||
|
setUrlWithoutDomain(response.request().url().toString())
|
||||||
|
|
||||||
|
val date = response.asJsoup().select(".folder-message").last().text().substringBefore(" • ")
|
||||||
|
for (dateFormat in MANGA_DETAILS_DATE_FORMAT) {
|
||||||
|
if (date_upload == 0L)
|
||||||
|
date_upload = dateFormat.parseOrNull(date)?.time ?: 0L
|
||||||
|
else
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(response: Response): String = throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
override fun latestUpdatesParse(response: Response): MangasPage {
|
||||||
|
return MangasPage(
|
||||||
|
gson.fromJson<JsonObject>(response.body()!!.string())["folders"].asJsonArray.map {
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(it["link"].asString)
|
||||||
|
title = it["name"].asString
|
||||||
|
artist = it["artistList"].asString
|
||||||
|
author = artist
|
||||||
|
genre = it["tags"].asJsonArray.joinToString(", ") { it["tag"].asString }
|
||||||
|
thumbnail_url = it["thumbnail2"].asString
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLatestPageUrl(page: Int): String {
|
||||||
|
val endDate = Calendar.getInstance().apply {
|
||||||
|
add(Calendar.DATE, 1)
|
||||||
|
set(Calendar.HOUR_OF_DAY, 0)
|
||||||
|
set(Calendar.MINUTE, 0)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
add(Calendar.DATE, -1 * PAGE_DAYS * (page - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
val endDateSec = endDate.timeInMillis / 1000
|
||||||
|
val startDateSec = endDate.apply {
|
||||||
|
add(Calendar.DATE, -1 * PAGE_DAYS)
|
||||||
|
}.timeInMillis / 1000
|
||||||
|
|
||||||
|
return "$baseUrl/folders?start=$startDateSec&end=$endDateSec"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) = GET(getLatestPageUrl(page))
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(response: Response): SManga {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
return SManga.create().apply {
|
||||||
|
title = document.select(".folder-title a").last().text()
|
||||||
|
artist = document.select(".gallery-artist a").joinToString(", ") { it.text() }
|
||||||
|
author = artist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun pageListParse(response: Response): List<Page> {
|
||||||
|
val document = response.asJsoup()
|
||||||
|
val pageUrl = response.request().url().toString()
|
||||||
|
return document.select(".doujin").mapIndexed { i, page ->
|
||||||
|
Page(i, "$pageUrl${page.attr("data-link")}", page.attr("data-file").replace("amp;", ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun popularMangaParse(response: Response) = parseGalleryPage(response.asJsoup())
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) = GET("$baseUrl/top/month")
|
||||||
|
|
||||||
|
override fun searchMangaParse(response: Response) = parseGalleryPage(response.asJsoup())
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||||
|
val filterList = if (filters.isEmpty()) getFilterList() else filters
|
||||||
|
val seriesFilter = filterList.findInstance<SeriesFilter>()!!
|
||||||
|
val sortFilter = filterList.findInstance<SortFilter>()!!
|
||||||
|
val popularityPeriodFilter = filterList.findInstance<PopularityPeriodFilter>()!!
|
||||||
|
|
||||||
|
return when {
|
||||||
|
query != "" -> {
|
||||||
|
GET("$baseUrl/searches?words=$query&page=$page&sort=${sortFilter.toUriPart()}")
|
||||||
|
}
|
||||||
|
seriesFilter.toUriPart() != "" -> {
|
||||||
|
GET("$baseUrl${seriesFilter.toUriPart()}?sort=${sortFilter.toUriPart()}")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
GET("$baseUrl${popularityPeriodFilter.toUriPart()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseGalleryPage(document: Document): MangasPage {
|
||||||
|
|
||||||
|
val pagination = document.select(".pagination").first()
|
||||||
|
return MangasPage(
|
||||||
|
document.select("a.gallery-visited-from-favorites").map {
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(it.attr("href"))
|
||||||
|
title = it.select("div.title .text").text()
|
||||||
|
artist = it.parent().nextElementSibling().select(".single-line strong")?.last()?.text()?.substringAfter("Artist: ")
|
||||||
|
author = artist
|
||||||
|
thumbnail_url = it.select("img").attr("srcset")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
if (pagination != null) {
|
||||||
|
!pagination.select("li.page-item:last-child").hasClass("disabled")
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList(): FilterList = FilterList(
|
||||||
|
Filter.Header("Text search ignores series and period filters"),
|
||||||
|
Filter.Separator(),
|
||||||
|
|
||||||
|
Filter.Header("Series filter overrides period filter"),
|
||||||
|
SeriesFilter(),
|
||||||
|
Filter.Separator(),
|
||||||
|
|
||||||
|
Filter.Header("Period filter only applies at initial page"),
|
||||||
|
PopularityPeriodFilter(),
|
||||||
|
Filter.Separator(),
|
||||||
|
|
||||||
|
Filter.Header("Sort only works with text search and series filter"),
|
||||||
|
SortFilter()
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SeriesFilter : UriPartFilter(
|
||||||
|
"Series",
|
||||||
|
arrayOf(
|
||||||
|
Pair("None", ""),
|
||||||
|
Pair("Doujins - Original Series", "/doujins-original-series-19934"),
|
||||||
|
Pair("Hentai Magazine Chapters", "/hentai-magazine-chapters-2766"),
|
||||||
|
Pair("Hentai Manga", "/hentai-manga-19"),
|
||||||
|
Pair("Fate Grand Order", "/fate-grand-order-doujins-28615"),
|
||||||
|
Pair("CG Sets - Original Series", "/cg-sets-original-series-14865"),
|
||||||
|
Pair("Touhou", "/touhou-doujins-7748"),
|
||||||
|
Pair("Naruto", "/naruto-doujins-5761"),
|
||||||
|
Pair("Kantai Collection", "/kantai-collection-doujins-22720"),
|
||||||
|
Pair("Hentai Game CG-Sets", "/hentai-game-cg-sets-2422"),
|
||||||
|
Pair("One Piece", "/one-piece-doujins-6080"),
|
||||||
|
Pair("Granblue Fantasy", "/granblue-fantasy-doujins-28177"),
|
||||||
|
Pair("Azur Lane", "/azur-lane-doujins-34298"),
|
||||||
|
Pair("Sword Art Online", "/sword-art-online-doujins-7246"),
|
||||||
|
Pair("Idolmaster", "/idolmaster-4281"),
|
||||||
|
Pair("My Hero Academia", "/my-hero-academia-doujins-28744"),
|
||||||
|
Pair("Love Live", "/love-live-doujins-21865"),
|
||||||
|
Pair("Pokemon", "/pokemon-doujins-6393"),
|
||||||
|
Pair("Dragon Ball", "/dragon-ball-doujins-1238"),
|
||||||
|
Pair("CGs - Mixed Series", "/cgs-mixed-series-35311"),
|
||||||
|
Pair("Doujins - Mixed Series", "/doujins-mixed-series-20091"),
|
||||||
|
Pair("Hentai Magazine Chapters", "/hentai-magazine-chapters-2766"),
|
||||||
|
Pair("Hentai Magazine Chapters - Super-Shorts", "/hentai-magazine-chapters-super-shorts-19933"),
|
||||||
|
Pair("Hentai Manga", "/hentai-manga-19")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class SortFilter : UriPartFilter(
|
||||||
|
"Sort",
|
||||||
|
arrayOf(
|
||||||
|
Pair("Newest First", ""),
|
||||||
|
Pair("Oldest First", "created_at"),
|
||||||
|
Pair("Alphabetical", "name"),
|
||||||
|
Pair("Rating", "-cached_score"),
|
||||||
|
Pair("Popularity", "-cached_views")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private class PopularityPeriodFilter : UriPartFilter(
|
||||||
|
"Period",
|
||||||
|
arrayOf(
|
||||||
|
Pair("This Month", "/top"),
|
||||||
|
Pair("This Year", "/top/year"),
|
||||||
|
Pair("All Time", "/top/all"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private open class UriPartFilter(displayName: String, val vals: Array<Pair<String, String>>) :
|
||||||
|
Filter.Select<String>(displayName, vals.map { it.first }.toTypedArray()) {
|
||||||
|
fun toUriPart() = vals[state].second
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SimpleDateFormat.parseOrNull(string: String): Date? {
|
||||||
|
return try {
|
||||||
|
parse(string)
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> Iterable<*>.findInstance() = find { it is T } as? T
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PAGE_DAYS = 3
|
||||||
|
private val ORDINAL_SUFFIXES = listOf("th", "st", "nd", "rd")
|
||||||
|
private val MANGA_DETAILS_DATE_FORMAT = ORDINAL_SUFFIXES.map {
|
||||||
|
SimpleDateFormat("MMMM dd'$it', yyyy", Locale.US)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue