Pixiv: new filters, group-by-series support (#17256)
* Pixiv: new filters, group-by-series support * requested changes * whoops * requested changes #2
This commit is contained in:
parent
a59ef3a817
commit
2df5bdfa68
|
@ -6,7 +6,7 @@ ext {
|
|||
extName = 'Pixiv'
|
||||
pkgNameSuffix = 'all.pixiv'
|
||||
extClass = '.PixivFactory'
|
||||
extVersionCode = 3
|
||||
extVersionCode = 4
|
||||
isNsfw = true
|
||||
}
|
||||
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pixiv
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
internal class FilterType : Filter.Select<String>("Type", values, 2) {
|
||||
companion object {
|
||||
val keys = arrayOf("all", "illust", "manga")
|
||||
val values = arrayOf("All", "Illustrations", "Manga")
|
||||
}
|
||||
|
||||
val value: String get() = keys[state]
|
||||
}
|
||||
|
||||
internal class FilterRating : Filter.Select<String>("Rating", values, 0) {
|
||||
companion object {
|
||||
val keys = arrayOf("all", "safe", "r18")
|
||||
val values = arrayOf("All", "All ages", "R-18")
|
||||
}
|
||||
|
||||
val value: String get() = keys[state]
|
||||
}
|
||||
|
||||
internal class FilterSearchMode : Filter.Select<String>("Mode", values, 1) {
|
||||
companion object {
|
||||
val keys = arrayOf("s_tag", "s_tag_full", "s_tc")
|
||||
val values = arrayOf("Tags (partial)", "Tags (full)", "Title, description")
|
||||
}
|
||||
|
||||
val value: String get() = keys[state]
|
||||
}
|
||||
|
||||
internal class FilterOrder : Filter.Sort("Order", arrayOf("Date posted")) {
|
||||
val value: String get() = if (state?.ascending == true) "date" else "date_d"
|
||||
}
|
||||
|
||||
internal class FilterDateBefore : Filter.Text("Posted before") {
|
||||
val value: String? get() = state.ifEmpty { null }
|
||||
}
|
||||
|
||||
internal class FilterDateAfter : Filter.Text("Posted after") {
|
||||
val value: String? get() = state.ifEmpty { null }
|
||||
}
|
|
@ -1,216 +1,399 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pixiv
|
||||
|
||||
import android.util.LruCache
|
||||
import eu.kanade.tachiyomi.network.asObservable
|
||||
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.model.UpdateStrategy
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.Jsoup
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class Pixiv(override val lang: String) : HttpSource() {
|
||||
override val name = "Pixiv"
|
||||
override val baseUrl = "https://www.pixiv.net"
|
||||
override val supportsLatest = true
|
||||
|
||||
private val siteLang: String = if (lang == "all") "ja" else lang
|
||||
private val illustCache by lazy { LruCache<String, PixivIllust>(50) }
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
private val dateFormat by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH) }
|
||||
|
||||
override fun headersBuilder() = super.headersBuilder()
|
||||
.add("Referer", "$baseUrl/")
|
||||
.add("Accept-Language", siteLang)
|
||||
override fun headersBuilder(): Headers.Builder =
|
||||
super.headersBuilder().add("Referer", "$baseUrl/")
|
||||
|
||||
private fun apiRequest(method: String, path: String, params: Map<String, String> = emptyMap()) = Request(
|
||||
url = baseUrl.toHttpUrl().newBuilder()
|
||||
.addEncodedPathSegments("ajax$path")
|
||||
.addEncodedQueryParameter("lang", siteLang)
|
||||
.apply { params.forEach { (k, v) -> addEncodedQueryParameter(k, v) } }
|
||||
.build(),
|
||||
private open inner class HttpCall(href: String?) {
|
||||
val url: HttpUrl.Builder = baseUrl.toHttpUrl()
|
||||
.run { href?.let { newBuilder(it)!! } ?: newBuilder() }
|
||||
|
||||
headers = headersBuilder().add("Accept", "application/json").build(),
|
||||
method = method,
|
||||
)
|
||||
val request: Request.Builder = Request.Builder()
|
||||
.headers(headersBuilder().build())
|
||||
|
||||
private inline fun <reified T> apiResponseParse(response: Response): T {
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception(response.message)
|
||||
fun execute(): Response =
|
||||
client.newCall(request.url(url.build()).build()).execute()
|
||||
}
|
||||
|
||||
private inner class ApiCall(href: String?) : HttpCall(href) {
|
||||
init {
|
||||
url.addEncodedQueryParameter("lang", lang)
|
||||
request.addHeader("Accept", "application/json")
|
||||
}
|
||||
|
||||
return response.body.string()
|
||||
.let { json.decodeFromString<PixivApiResponse<T>>(it) }
|
||||
.apply { if (error) throw Exception(message ?: response.message) }
|
||||
.let { it.body!! }
|
||||
inline fun <reified T> executeApi(): T =
|
||||
json.decodeFromString<PixivApiResponse<T>>(execute().body.string()).body!!
|
||||
}
|
||||
|
||||
private fun illustUrlToId(url: String): String =
|
||||
url.substringAfterLast("/")
|
||||
private var popularMangaNextPage = 1
|
||||
private lateinit var popularMangaIterator: Iterator<SManga>
|
||||
|
||||
private fun urlEncode(string: String): String =
|
||||
URLEncoder.encode(string, "UTF-8").replace("+", "%20")
|
||||
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
if (page == 1) {
|
||||
popularMangaIterator = sequence {
|
||||
val call = ApiCall("/touch/ajax/ranking/illust?mode=daily&type=manga")
|
||||
|
||||
private fun parseTimestamp(string: String) =
|
||||
runCatching { dateFormat.parse(string)?.time!! }.getOrDefault(0)
|
||||
for (p in countUp(start = 1)) {
|
||||
call.url.setEncodedQueryParameter("page", p.toString())
|
||||
|
||||
private fun parseSearchResult(result: PixivSearchResult) = SManga.create().apply {
|
||||
url = "/artworks/${result.id!!}"
|
||||
title = result.title ?: ""
|
||||
thumbnail_url = result.url
|
||||
val entries = call.executeApi<PixivRankings>().ranking!!
|
||||
if (entries.isEmpty()) break
|
||||
|
||||
val call = ApiCall("/touch/ajax/illust/details/many")
|
||||
entries.forEach { call.url.addEncodedQueryParameter("illust_ids[]", it.illustId!!) }
|
||||
|
||||
call.executeApi<PixivIllustsDetails>().illust_details!!.forEach { yield(it) }
|
||||
}
|
||||
}
|
||||
.toSManga()
|
||||
.iterator()
|
||||
|
||||
popularMangaNextPage = 2
|
||||
} else {
|
||||
require(page == popularMangaNextPage++)
|
||||
}
|
||||
|
||||
val mangas = popularMangaIterator.truncateToList(50)
|
||||
return Observable.just(MangasPage(mangas, hasNextPage = mangas.isNotEmpty()))
|
||||
}
|
||||
|
||||
private fun fetchIllust(url: String): Observable<PixivIllust> =
|
||||
Observable.fromCallable { illustCache.get(url) }.filter { it != null }.switchIfEmpty(
|
||||
Observable.defer {
|
||||
client.newCall(illustRequest(url)).asObservable()
|
||||
.map { illustParse(it) }
|
||||
.doOnNext { illustCache.put(url, it) }
|
||||
},
|
||||
)
|
||||
private var searchNextPage = 1
|
||||
private var searchHash: Int? = null
|
||||
private lateinit var searchIterator: Iterator<SManga>
|
||||
|
||||
private fun illustRequest(url: String): Request =
|
||||
apiRequest("GET", "/illust/${illustUrlToId(url)}")
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
val filters = filters.list as PixivFilters
|
||||
val hash = Pair(query, filters).hashCode()
|
||||
|
||||
private fun illustParse(response: Response): PixivIllust =
|
||||
apiResponseParse(response)
|
||||
if (hash != searchHash || page == 1) {
|
||||
searchHash = hash
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList())
|
||||
lateinit var searchSequence: Sequence<PixivIllust>
|
||||
lateinit var predicates: List<(PixivIllust) -> Boolean>
|
||||
|
||||
override fun popularMangaParse(response: Response) = MangasPage(
|
||||
mangas = apiResponseParse<PixivSearchResults>(response)
|
||||
.popular?.run { recent.orEmpty() + permanent.orEmpty() }
|
||||
?.map(::parseSearchResult)
|
||||
.orEmpty(),
|
||||
if (query.isNotBlank()) {
|
||||
searchSequence = makeIllustSearchSequence(
|
||||
word = query,
|
||||
order = filters.order.toSearchParameter(),
|
||||
mode = filters.rating.toSearchParameter(),
|
||||
sMode = "s_tc",
|
||||
type = filters.type.toSearchParameter(),
|
||||
dateBefore = filters.dateBefore.state.ifBlank { null },
|
||||
dateAfter = filters.dateAfter.state.ifBlank { null },
|
||||
)
|
||||
|
||||
hasNextPage = false,
|
||||
)
|
||||
predicates = buildList {
|
||||
filters.tags.toPredicate()?.let(::add)
|
||||
filters.users.toPredicate()?.let(::add)
|
||||
}
|
||||
} else if (filters.users.state.isNotBlank()) {
|
||||
searchSequence = makeUserIllustSearchSequence(
|
||||
nick = query,
|
||||
type = filters.type.toSearchParameter(),
|
||||
)
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val word = urlEncode(query.ifBlank { "漫画" })
|
||||
predicates = buildList {
|
||||
filters.tags.toPredicate()?.let(::add)
|
||||
filters.rating.toPredicate()?.let(::add)
|
||||
}
|
||||
} else {
|
||||
searchSequence = makeIllustSearchSequence(
|
||||
word = filters.tags.state.ifBlank { "漫画" },
|
||||
order = filters.order.toSearchParameter(),
|
||||
mode = filters.rating.toSearchParameter(),
|
||||
sMode = "s_tag_full",
|
||||
type = filters.type.toSearchParameter(),
|
||||
dateBefore = filters.dateBefore.state.ifBlank { null },
|
||||
dateAfter = filters.dateAfter.state.ifBlank { null },
|
||||
)
|
||||
|
||||
val parameters = mutableMapOf(
|
||||
"word" to query,
|
||||
"order" to "date_d",
|
||||
"mode" to "all",
|
||||
"p" to page.toString(),
|
||||
"s_mode" to "s_tag_full",
|
||||
"type" to "manga",
|
||||
)
|
||||
predicates = emptyList()
|
||||
}
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is FilterType -> parameters["type"] = filter.value
|
||||
is FilterRating -> parameters["mode"] = filter.value
|
||||
is FilterSearchMode -> parameters["s_mode"] = filter.value
|
||||
is FilterOrder -> parameters["order"] = filter.value
|
||||
is FilterDateBefore -> filter.value?.let { parameters["ecd"] = it }
|
||||
is FilterDateAfter -> filter.value?.let { parameters["scd"] = it }
|
||||
else -> {}
|
||||
if (predicates.isNotEmpty()) {
|
||||
searchSequence = searchSequence.filter { predicates.all { p -> p(it) } }
|
||||
}
|
||||
|
||||
searchIterator = searchSequence.toSManga().iterator()
|
||||
searchNextPage = 2
|
||||
} else {
|
||||
require(page == searchNextPage++)
|
||||
}
|
||||
|
||||
val mangas = searchIterator.truncateToList(50).toList()
|
||||
return Observable.just(MangasPage(mangas, hasNextPage = mangas.isNotEmpty()))
|
||||
}
|
||||
|
||||
private fun makeIllustSearchSequence(
|
||||
word: String,
|
||||
sMode: String,
|
||||
order: String?,
|
||||
mode: String?,
|
||||
type: String?,
|
||||
dateBefore: String?,
|
||||
dateAfter: String?,
|
||||
) = sequence<PixivIllust> {
|
||||
val call = ApiCall("/touch/ajax/search/illusts")
|
||||
|
||||
call.url.addQueryParameter("word", word)
|
||||
call.url.addEncodedQueryParameter("s_mode", sMode)
|
||||
type?.let { call.url.addEncodedQueryParameter("type", it) }
|
||||
order?.let { call.url.addEncodedQueryParameter("order", it) }
|
||||
mode?.let { call.url.addEncodedQueryParameter("mode", it) }
|
||||
dateBefore?.let { call.url.addEncodedQueryParameter("ecd", it) }
|
||||
dateAfter?.let { call.url.addEncodedQueryParameter("scd", it) }
|
||||
|
||||
for (p in countUp(start = 1)) {
|
||||
call.url.setEncodedQueryParameter("p", p.toString())
|
||||
|
||||
val illusts = call.executeApi<PixivResults>().illusts!!
|
||||
if (illusts.isEmpty()) break
|
||||
|
||||
for (illust in illusts) {
|
||||
if (illust.is_ad_container == 1) continue
|
||||
if (illust.type == "2") continue
|
||||
|
||||
yield(illust)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeUserIllustSearchSequence(nick: String, type: String?) = sequence<PixivIllust> {
|
||||
val searchUsers = HttpCall("/search_user.php?s_mode=s_usr")
|
||||
.apply { url.addQueryParameter("nick", nick) }
|
||||
|
||||
val fetchUserIllusts = ApiCall("/touch/ajax/user/illusts")
|
||||
.apply { type?.let { url.setEncodedQueryParameter("type", it) } }
|
||||
|
||||
for (p in countUp(start = 1)) {
|
||||
searchUsers.url.setEncodedQueryParameter("p", p.toString())
|
||||
|
||||
val userIds = Jsoup.parse(searchUsers.execute().body.string())
|
||||
.select(".user-recommendation-item > a").eachAttr("href")
|
||||
.map { it.substringAfterLast('/') }
|
||||
|
||||
if (userIds.isEmpty()) break
|
||||
|
||||
for (userId in userIds) {
|
||||
fetchUserIllusts.url.setEncodedQueryParameter("id", userId)
|
||||
|
||||
for (p in countUp(start = 1)) {
|
||||
fetchUserIllusts.url.setEncodedQueryParameter("p", p.toString())
|
||||
|
||||
val illusts = fetchUserIllusts.executeApi<PixivResults>().illusts!!
|
||||
if (illusts.isEmpty()) break
|
||||
|
||||
yieldAll(illusts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilterList() = FilterList(PixivFilters())
|
||||
|
||||
private fun Sequence<PixivIllust>.toSManga() = sequence<SManga> {
|
||||
val seriesIdsSeen = mutableSetOf<String>()
|
||||
|
||||
forEach { illust ->
|
||||
val series = illust.series
|
||||
|
||||
if (series == null) {
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain("/artworks/${illust.id!!}")
|
||||
manga.title = illust.title ?: "(null)"
|
||||
manga.thumbnail_url = illust.url
|
||||
yield(manga)
|
||||
} else if (seriesIdsSeen.add(series.id!!)) {
|
||||
val manga = SManga.create()
|
||||
manga.setUrlWithoutDomain("/user/${series.userId!!}/series/${series.id}")
|
||||
manga.title = series.title ?: "(null)"
|
||||
manga.thumbnail_url = series.coverImage ?: illust.url
|
||||
yield(manga)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var latestMangaNextPage = 1
|
||||
private lateinit var latestMangaIterator: Iterator<SManga>
|
||||
|
||||
override fun fetchLatestUpdates(page: Int): Observable<MangasPage> {
|
||||
if (page == 1) {
|
||||
latestMangaIterator = sequence {
|
||||
val call = ApiCall("/touch/ajax/latest?type=manga")
|
||||
|
||||
for (p in countUp(start = 1)) {
|
||||
call.url.setEncodedQueryParameter("p", p.toString())
|
||||
|
||||
val illusts = call.executeApi<PixivResults>().illusts!!
|
||||
if (illusts.isEmpty()) break
|
||||
|
||||
for (illust in illusts) {
|
||||
if (illust.is_ad_container == 1) continue
|
||||
yield(illust)
|
||||
}
|
||||
}
|
||||
}
|
||||
.toSManga()
|
||||
.iterator()
|
||||
|
||||
latestMangaNextPage = 2
|
||||
} else {
|
||||
require(page == latestMangaNextPage++)
|
||||
}
|
||||
|
||||
val mangas = latestMangaIterator.truncateToList(50).toList()
|
||||
return Observable.just(MangasPage(mangas, hasNextPage = mangas.isNotEmpty()))
|
||||
}
|
||||
|
||||
private val illustsCache = object : LruCache<String, PixivIllust>(25) {
|
||||
override fun create(illustId: String): PixivIllust {
|
||||
val call = ApiCall("/touch/ajax/illust/details?illust_id=$illustId")
|
||||
return call.executeApi<PixivIllustDetails>().illust_details!!
|
||||
}
|
||||
}
|
||||
|
||||
private val seriesIllustsCache = object : LruCache<String, List<PixivIllust>>(25) {
|
||||
override fun create(seriesId: String): List<PixivIllust> {
|
||||
val call = ApiCall("/touch/ajax/illust/series_content/$seriesId")
|
||||
var lastOrder = 0
|
||||
|
||||
return buildList {
|
||||
while (true) {
|
||||
call.url.setEncodedQueryParameter("last_order", lastOrder.toString())
|
||||
|
||||
val illusts = call.executeApi<PixivSeriesContents>().series_contents!!
|
||||
if (illusts.isEmpty()) break
|
||||
|
||||
addAll(illusts)
|
||||
lastOrder += illusts.size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
val (id, isSeries) = parseSMangaUrl(manga.url)
|
||||
|
||||
if (isSeries) {
|
||||
val series = ApiCall("/touch/ajax/illust/series/$id").executeApi<PixivSeries>()
|
||||
val illusts = seriesIllustsCache.get(id)
|
||||
|
||||
if (series.id != null && series.userId != null) {
|
||||
manga.setUrlWithoutDomain("/user/${series.userId}/series/${series.id}")
|
||||
}
|
||||
|
||||
series.title?.let { manga.title = it }
|
||||
series.caption?.let { manga.description = it }
|
||||
|
||||
illusts.firstOrNull()?.author_details?.user_name?.let {
|
||||
manga.artist = it
|
||||
manga.author = it
|
||||
}
|
||||
|
||||
val tags = illusts.flatMap { it.tags ?: emptyList() }.toSet()
|
||||
if (tags.isNotEmpty()) manga.genre = tags.joinToString()
|
||||
|
||||
(series.coverImage ?: illusts.firstOrNull()?.url)?.let { manga.thumbnail_url = it }
|
||||
} else {
|
||||
val illust = illustsCache.get(id)
|
||||
|
||||
illust.id?.let { manga.setUrlWithoutDomain("/artworks/$it") }
|
||||
illust.title?.let { manga.title = it }
|
||||
|
||||
illust.author_details?.user_name?.let {
|
||||
manga.artist = it
|
||||
manga.author = it
|
||||
}
|
||||
|
||||
illust.comment?.let { manga.description = it }
|
||||
illust.tags?.let { manga.genre = it.joinToString() }
|
||||
illust.url?.let { manga.thumbnail_url = it }
|
||||
}
|
||||
|
||||
return Observable.just(manga)
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
val (id, isSeries) = parseSMangaUrl(manga.url)
|
||||
|
||||
val illusts = when (isSeries) {
|
||||
true -> seriesIllustsCache.get(id)
|
||||
false -> listOf(illustsCache.get(id))
|
||||
}
|
||||
|
||||
val chapters = illusts.mapIndexed { i, illust ->
|
||||
SChapter.create().apply {
|
||||
setUrlWithoutDomain("/artworks/${illust.id!!}")
|
||||
name = illust.title ?: "(null)"
|
||||
date_upload = illust.upload_timestamp ?: 0
|
||||
chapter_number = i.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
val endpoint = when (parameters["type"]) {
|
||||
"all" -> "artworks"
|
||||
"illust" -> "illustrations"
|
||||
"manga" -> "manga"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
return apiRequest("GET", "/search/$endpoint/$word", parameters)
|
||||
return Observable.just(chapters)
|
||||
}
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage {
|
||||
val mangas = apiResponseParse<PixivSearchResults>(response)
|
||||
.run { illustManga ?: illust ?: manga }?.data
|
||||
?.filter { it.isAdContainer != true }
|
||||
?.map(::parseSearchResult)
|
||||
.orEmpty()
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
val illustId = chapter.url.substringAfterLast('/')
|
||||
|
||||
return MangasPage(mangas, hasNextPage = mangas.isNotEmpty())
|
||||
val pages = ApiCall("/ajax/illust/$illustId/pages")
|
||||
.executeApi<List<PixivIllustPage>>()
|
||||
.mapIndexed { i, it -> Page(i, chapter.url, it.urls!!.original!!) }
|
||||
|
||||
return Observable.just(pages)
|
||||
}
|
||||
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
searchMangaRequest(page, "", FilterList())
|
||||
|
||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||
searchMangaParse(response)
|
||||
|
||||
override fun mangaDetailsRequest(manga: SManga): Request =
|
||||
illustRequest(manga.url)
|
||||
|
||||
override fun mangaDetailsParse(response: Response) = SManga.create().apply {
|
||||
val illust = illustParse(response)
|
||||
|
||||
url = "/artworks/${illust.id!!}"
|
||||
title = illust.title ?: ""
|
||||
artist = illust.userName
|
||||
author = illust.userName
|
||||
description = illust.description?.let { Jsoup.parseBodyFragment(it).wholeText() }
|
||||
genre = illust.tags?.tags?.mapNotNull { it.tag }?.joinToString()
|
||||
thumbnail_url = illust.urls?.thumb
|
||||
update_strategy = UpdateStrategy.ONLY_FETCH_ONCE
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||
fetchIllust(manga.url).map { illust ->
|
||||
listOf(
|
||||
SChapter.create().apply {
|
||||
url = manga.url
|
||||
name = "Oneshot"
|
||||
date_upload = illust.uploadDate?.let(::parseTimestamp) ?: 0
|
||||
chapter_number = 0F
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun chapterListRequest(manga: SManga): Request =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
override fun chapterListParse(response: Response): List<SChapter> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
override fun pageListRequest(chapter: SChapter): Request =
|
||||
apiRequest("GET", "/illust/${illustUrlToId(chapter.url)}/pages")
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> =
|
||||
apiResponseParse<List<PixivPage>>(response)
|
||||
.mapIndexed { i, it -> Page(index = i, imageUrl = it.urls?.original) }
|
||||
|
||||
override fun imageUrlRequest(page: Page): Request =
|
||||
throw IllegalStateException("Not used")
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun imageUrlParse(response: Response): String =
|
||||
throw IllegalStateException("Not used")
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun getMangaUrl(manga: SManga): String =
|
||||
baseUrl + manga.url
|
||||
override fun latestUpdatesParse(response: Response): MangasPage =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun getChapterUrl(chapter: SChapter): String =
|
||||
baseUrl + chapter.url
|
||||
override fun latestUpdatesRequest(page: Int): Request =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun getFilterList() = FilterList(
|
||||
listOf(
|
||||
FilterType(),
|
||||
FilterRating(),
|
||||
FilterSearchMode(),
|
||||
FilterOrder(),
|
||||
FilterDateBefore(),
|
||||
FilterDateAfter(),
|
||||
),
|
||||
)
|
||||
override fun mangaDetailsParse(response: Response): SManga =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun pageListParse(response: Response): List<Page> =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun popularMangaParse(response: Response): MangasPage =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun popularMangaRequest(page: Int): Request =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun searchMangaParse(response: Response): MangasPage =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
|
||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||
throw UnsupportedOperationException("Not used.")
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@ import eu.kanade.tachiyomi.source.SourceFactory
|
|||
|
||||
class PixivFactory : SourceFactory {
|
||||
override fun createSources(): List<Source> =
|
||||
listOf("all", "ja", "en", "ko", "zh").map { lang -> Pixiv(lang) }
|
||||
listOf("ja", "en", "ko", "zh").map { lang -> Pixiv(lang) }
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pixiv
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
|
||||
internal class PixivFilters : MutableList<Filter<*>> by mutableListOf() {
|
||||
class Type : Filter.Select<String>("Type", values, 2) {
|
||||
companion object {
|
||||
private val values: Array<String> =
|
||||
arrayOf("All", "Illustrations", "Manga")
|
||||
|
||||
private val searchParams: Array<String?> =
|
||||
arrayOf(null, "illust", "manga")
|
||||
}
|
||||
|
||||
fun toSearchParameter(): String? = searchParams[state]
|
||||
}
|
||||
|
||||
val type = Type().also(::add)
|
||||
|
||||
class Tags : Filter.Text("Tags") {
|
||||
fun toPredicate(): ((PixivIllust) -> Boolean)? {
|
||||
if (state.isBlank()) return null
|
||||
|
||||
val tags = state.split(' ')
|
||||
return { it.tags?.containsAll(tags) == true }
|
||||
}
|
||||
}
|
||||
|
||||
val tags = Tags().also(::add)
|
||||
|
||||
class Users : Filter.Text("Users") {
|
||||
fun toPredicate(): ((PixivIllust) -> Boolean)? {
|
||||
if (state.isBlank()) return null
|
||||
val regex = Regex(state.split(' ').joinToString("|") { Regex.escape(it) })
|
||||
|
||||
return { it.author_details?.user_name?.contains(regex) == true }
|
||||
}
|
||||
}
|
||||
|
||||
val users = Users().also(::add)
|
||||
|
||||
class Rating : Filter.Select<String>("Rating", values, 0) {
|
||||
companion object {
|
||||
private val searchParams: Array<String?> =
|
||||
arrayOf(null, "all", "r18")
|
||||
|
||||
private val values: Array<String> =
|
||||
arrayOf("All", "All ages", "R-18")
|
||||
|
||||
private val predicates: Array<((PixivIllust) -> Boolean)?> =
|
||||
arrayOf(null, { it.x_restrict == "0" }, { it.x_restrict == "1" })
|
||||
}
|
||||
|
||||
fun toPredicate(): ((PixivIllust) -> Boolean)? = predicates[state]
|
||||
fun toSearchParameter(): String? = searchParams[state]
|
||||
}
|
||||
|
||||
val rating = Rating().also(::add)
|
||||
|
||||
init { add(Filter.Header("(the following are ignored when the users filter is in use)")) }
|
||||
|
||||
class Order : Filter.Sort("Order", arrayOf("Date posted")) {
|
||||
fun toSearchParameter(): String? = state?.ascending?.let { "date" }
|
||||
}
|
||||
|
||||
val order = Order().also(::add)
|
||||
|
||||
class DateBefore : Filter.Text("Posted before")
|
||||
val dateBefore = DateBefore().also(::add)
|
||||
|
||||
class DateAfter : Filter.Text("Posted after")
|
||||
val dateAfter = DateAfter().also(::add)
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pixiv
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class PixivApiResponse<T>(
|
||||
val body: T? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivResults(
|
||||
val illusts: List<PixivIllust>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivIllust(
|
||||
val author_details: PixivAuthorDetails? = null,
|
||||
val comment: String? = null,
|
||||
val id: String? = null,
|
||||
val is_ad_container: Int? = null,
|
||||
val series: PixivSearchResultSeries? = null,
|
||||
val tags: List<String>? = null,
|
||||
val title: String? = null,
|
||||
val type: String? = null,
|
||||
val upload_timestamp: Long? = null,
|
||||
val url: String? = null,
|
||||
val x_restrict: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSearchResultSeries(
|
||||
val coverImage: String? = null,
|
||||
val id: String? = null,
|
||||
val title: String? = null,
|
||||
val userId: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivIllustDetails(
|
||||
val illust_details: PixivIllust? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivIllustsDetails(
|
||||
val illust_details: List<PixivIllust>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivIllustPage(
|
||||
val urls: PixivIllustPageUrls? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivIllustPageUrls(
|
||||
val original: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivAuthorDetails(
|
||||
val user_name: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSeries(
|
||||
val caption: String? = null,
|
||||
val coverImage: String? = null,
|
||||
val id: String? = null,
|
||||
val title: String? = null,
|
||||
val userId: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSeriesContents(
|
||||
val series_contents: List<PixivIllust>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivRankings(
|
||||
val ranking: List<PixivRankingEntry>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivRankingEntry(
|
||||
val illustId: String? = null,
|
||||
)
|
|
@ -1,69 +0,0 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pixiv
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal data class PixivApiResponse<T>(
|
||||
val error: Boolean,
|
||||
val body: T? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivIllust(
|
||||
val id: Int? = null,
|
||||
val title: String? = null,
|
||||
val userName: String? = null,
|
||||
val description: String? = null,
|
||||
val tags: PixivTags? = null,
|
||||
val urls: PixivImageUrls? = null,
|
||||
val uploadDate: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSearchResult(
|
||||
val id: Int? = null,
|
||||
val title: String? = null,
|
||||
val url: String? = null,
|
||||
val isAdContainer: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivTag(
|
||||
val tag: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivTags(
|
||||
val tags: List<PixivTag>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSearchResults(
|
||||
val illustManga: PixivSearchResultsIllusts? = null,
|
||||
val illust: PixivSearchResultsIllusts? = null,
|
||||
val manga: PixivSearchResultsIllusts? = null,
|
||||
val popular: PixivSearchResultsPopular? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSearchResultsIllusts(
|
||||
val data: List<PixivSearchResult>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivSearchResultsPopular(
|
||||
val permanent: List<PixivSearchResult>? = null,
|
||||
val recent: List<PixivSearchResult>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivPage(
|
||||
val urls: PixivImageUrls? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
internal data class PixivImageUrls(
|
||||
val original: String? = null,
|
||||
val thumb: String? = null,
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
package eu.kanade.tachiyomi.extension.all.pixiv
|
||||
|
||||
internal fun countUp(start: Int = 0) = sequence<Int> {
|
||||
yieldAll(start..Int.MAX_VALUE)
|
||||
throw RuntimeException("Overflow")
|
||||
}
|
||||
|
||||
internal fun <T> Iterator<T>.truncateToList(count: Int): List<T> = buildList {
|
||||
repeat(count) {
|
||||
if (!hasNext()) return@buildList
|
||||
add(next())
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseSMangaUrl(url: String): Pair<String, Boolean> {
|
||||
val isSeries = url.getOrNull(1) != 'a'
|
||||
return Pair(url.substringAfterLast('/'), isSeries)
|
||||
}
|
Loading…
Reference in New Issue