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:
Solitai7e 2023-07-27 12:33:41 +00:00 committed by GitHub
parent a59ef3a817
commit 2df5bdfa68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 515 additions and 268 deletions

View File

@ -6,7 +6,7 @@ ext {
extName = 'Pixiv'
pkgNameSuffix = 'all.pixiv'
extClass = '.PixivFactory'
extVersionCode = 3
extVersionCode = 4
isNsfw = true
}

View File

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

View File

@ -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())
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")
}
inline fun <reified T> executeApi(): T =
json.decodeFromString<PixivApiResponse<T>>(execute().body.string()).body!!
}
private var popularMangaNextPage = 1
private lateinit var popularMangaIterator: Iterator<SManga>
override fun fetchPopularManga(page: Int): Observable<MangasPage> {
if (page == 1) {
popularMangaIterator = sequence {
val call = ApiCall("/touch/ajax/ranking/illust?mode=daily&type=manga")
for (p in countUp(start = 1)) {
call.url.setEncodedQueryParameter("page", p.toString())
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 var searchNextPage = 1
private var searchHash: Int? = null
private lateinit var searchIterator: Iterator<SManga>
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val filters = filters.list as PixivFilters
val hash = Pair(query, filters).hashCode()
if (hash != searchHash || page == 1) {
searchHash = hash
lateinit var searchSequence: Sequence<PixivIllust>
lateinit var predicates: List<(PixivIllust) -> Boolean>
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 },
)
private inline fun <reified T> apiResponseParse(response: Response): T {
if (!response.isSuccessful) {
throw Exception(response.message)
predicates = buildList {
filters.tags.toPredicate()?.let(::add)
filters.users.toPredicate()?.let(::add)
}
return response.body.string()
.let { json.decodeFromString<PixivApiResponse<T>>(it) }
.apply { if (error) throw Exception(message ?: response.message) }
.let { it.body!! }
}
private fun illustUrlToId(url: String): String =
url.substringAfterLast("/")
private fun urlEncode(string: String): String =
URLEncoder.encode(string, "UTF-8").replace("+", "%20")
private fun parseTimestamp(string: String) =
runCatching { dateFormat.parse(string)?.time!! }.getOrDefault(0)
private fun parseSearchResult(result: PixivSearchResult) = SManga.create().apply {
url = "/artworks/${result.id!!}"
title = result.title ?: ""
thumbnail_url = result.url
}
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) }
},
} else if (filters.users.state.isNotBlank()) {
searchSequence = makeUserIllustSearchSequence(
nick = query,
type = filters.type.toSearchParameter(),
)
private fun illustRequest(url: String): Request =
apiRequest("GET", "/illust/${illustUrlToId(url)}")
private fun illustParse(response: Response): PixivIllust =
apiResponseParse(response)
override fun popularMangaRequest(page: Int): Request =
searchMangaRequest(page, "", FilterList())
override fun popularMangaParse(response: Response) = MangasPage(
mangas = apiResponseParse<PixivSearchResults>(response)
.popular?.run { recent.orEmpty() + permanent.orEmpty() }
?.map(::parseSearchResult)
.orEmpty(),
hasNextPage = false,
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 },
)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val word = urlEncode(query.ifBlank { "漫画" })
predicates = emptyList()
}
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",
)
if (predicates.isNotEmpty()) {
searchSequence = searchSequence.filter { predicates.all { p -> p(it) } }
}
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 -> {}
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)
}
}
}
val endpoint = when (parameters["type"]) {
"all" -> "artworks"
"illust" -> "illustrations"
"manga" -> "manga"
else -> ""
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)
}
}
}
}
return apiRequest("GET", "/search/$endpoint/$word", parameters)
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)
}
}
}
override fun searchMangaParse(response: Response): MangasPage {
val mangas = apiResponseParse<PixivSearchResults>(response)
.run { illustManga ?: illust ?: manga }?.data
?.filter { it.isAdContainer != true }
?.map(::parseSearchResult)
.orEmpty()
private var latestMangaNextPage = 1
private lateinit var latestMangaIterator: Iterator<SManga>
return MangasPage(mangas, hasNextPage = mangas.isNotEmpty())
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++)
}
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
val mangas = latestMangaIterator.truncateToList(50).toList()
return Observable.just(MangasPage(mangas, hasNextPage = mangas.isNotEmpty()))
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
fetchIllust(manga.url).map { illust ->
listOf(
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 {
url = manga.url
name = "Oneshot"
date_upload = illust.uploadDate?.let(::parseTimestamp) ?: 0
chapter_number = 0F
},
)
setUrlWithoutDomain("/artworks/${illust.id!!}")
name = illust.title ?: "(null)"
date_upload = illust.upload_timestamp ?: 0
chapter_number = i.toFloat()
}
}
override fun chapterListRequest(manga: SManga): Request =
throw IllegalStateException("Not used")
return Observable.just(chapters)
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
val illustId = chapter.url.substringAfterLast('/')
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 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.")
}

View File

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

View File

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

View File

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

View File

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

View File

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