diff --git a/src/all/pixiv/build.gradle b/src/all/pixiv/build.gradle index 0215f94db..9f92accc0 100644 --- a/src/all/pixiv/build.gradle +++ b/src/all/pixiv/build.gradle @@ -6,7 +6,7 @@ ext { extName = 'Pixiv' pkgNameSuffix = 'all.pixiv' extClass = '.PixivFactory' - extVersionCode = 3 + extVersionCode = 4 isNsfw = true } diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Filters.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Filters.kt deleted file mode 100644 index bea80d1ce..000000000 --- a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Filters.kt +++ /dev/null @@ -1,41 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.pixiv -import eu.kanade.tachiyomi.source.model.Filter - -internal class FilterType : Filter.Select("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("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("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 } -} diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Pixiv.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Pixiv.kt index 81cfa939d..35ca49e32 100644 --- a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Pixiv.kt +++ b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Pixiv.kt @@ -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(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 = 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 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>(it) } - .apply { if (error) throw Exception(message ?: response.message) } - .let { it.body!! } + inline fun executeApi(): T = + json.decodeFromString>(execute().body.string()).body!! } - private fun illustUrlToId(url: String): String = - url.substringAfterLast("/") + private var popularMangaNextPage = 1 + private lateinit var popularMangaIterator: Iterator - private fun urlEncode(string: String): String = - URLEncoder.encode(string, "UTF-8").replace("+", "%20") + override fun fetchPopularManga(page: Int): Observable { + 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().ranking!! + if (entries.isEmpty()) break + + val call = ApiCall("/touch/ajax/illust/details/many") + entries.forEach { call.url.addEncodedQueryParameter("illust_ids[]", it.illustId!!) } + + call.executeApi().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 = - 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 - private fun illustRequest(url: String): Request = - apiRequest("GET", "/illust/${illustUrlToId(url)}") + override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { + 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 + lateinit var predicates: List<(PixivIllust) -> Boolean> - override fun popularMangaParse(response: Response) = MangasPage( - mangas = apiResponseParse(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 { + 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().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 { + 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().illusts!! + if (illusts.isEmpty()) break + + yieldAll(illusts) + } + } + } + } + + override fun getFilterList() = FilterList(PixivFilters()) + + private fun Sequence.toSManga() = sequence { + val seriesIdsSeen = mutableSetOf() + + 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 + + override fun fetchLatestUpdates(page: Int): Observable { + 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().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(25) { + override fun create(illustId: String): PixivIllust { + val call = ApiCall("/touch/ajax/illust/details?illust_id=$illustId") + return call.executeApi().illust_details!! + } + } + + private val seriesIllustsCache = object : LruCache>(25) { + override fun create(seriesId: String): List { + 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().series_contents!! + if (illusts.isEmpty()) break + + addAll(illusts) + lastOrder += illusts.size + } + } + } + } + + override fun fetchMangaDetails(manga: SManga): Observable { + val (id, isSeries) = parseSMangaUrl(manga.url) + + if (isSeries) { + val series = ApiCall("/touch/ajax/illust/series/$id").executeApi() + 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> { + 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(response) - .run { illustManga ?: illust ?: manga }?.data - ?.filter { it.isAdContainer != true } - ?.map(::parseSearchResult) - .orEmpty() + override fun fetchPageList(chapter: SChapter): Observable> { + val illustId = chapter.url.substringAfterLast('/') - return MangasPage(mangas, hasNextPage = mangas.isNotEmpty()) + val pages = ApiCall("/ajax/illust/$illustId/pages") + .executeApi>() + .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> = - 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 = - throw IllegalStateException("Not used") - - override fun pageListRequest(chapter: SChapter): Request = - apiRequest("GET", "/illust/${illustUrlToId(chapter.url)}/pages") - - override fun pageListParse(response: Response): List = - apiResponseParse>(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 = + 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.") } diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFactory.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFactory.kt index 578b73272..e4b10cec2 100644 --- a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFactory.kt +++ b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFactory.kt @@ -5,5 +5,5 @@ import eu.kanade.tachiyomi.source.SourceFactory class PixivFactory : SourceFactory { override fun createSources(): List = - listOf("all", "ja", "en", "ko", "zh").map { lang -> Pixiv(lang) } + listOf("ja", "en", "ko", "zh").map { lang -> Pixiv(lang) } } diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFilters.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFilters.kt new file mode 100644 index 000000000..45e23af66 --- /dev/null +++ b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivFilters.kt @@ -0,0 +1,72 @@ +package eu.kanade.tachiyomi.extension.all.pixiv +import eu.kanade.tachiyomi.source.model.Filter + +internal class PixivFilters : MutableList> by mutableListOf() { + class Type : Filter.Select("Type", values, 2) { + companion object { + private val values: Array = + arrayOf("All", "Illustrations", "Manga") + + private val searchParams: Array = + 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("Rating", values, 0) { + companion object { + private val searchParams: Array = + arrayOf(null, "all", "r18") + + private val values: Array = + 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) +} diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivTypes.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivTypes.kt new file mode 100644 index 000000000..9e6bcfcb7 --- /dev/null +++ b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/PixivTypes.kt @@ -0,0 +1,84 @@ +package eu.kanade.tachiyomi.extension.all.pixiv +import kotlinx.serialization.Serializable + +@Serializable +internal data class PixivApiResponse( + val body: T? = null, +) + +@Serializable +internal data class PixivResults( + val illusts: List? = 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? = 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? = 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? = null, +) + +@Serializable +internal data class PixivRankings( + val ranking: List? = null, +) + +@Serializable +internal data class PixivRankingEntry( + val illustId: String? = null, +) diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Structures.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Structures.kt deleted file mode 100644 index f5dca8fe8..000000000 --- a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Structures.kt +++ /dev/null @@ -1,69 +0,0 @@ -package eu.kanade.tachiyomi.extension.all.pixiv - -import kotlinx.serialization.Serializable - -@Serializable -internal data class PixivApiResponse( - 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? = 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? = null, -) - -@Serializable -internal data class PixivSearchResultsPopular( - val permanent: List? = null, - val recent: List? = null, -) - -@Serializable -internal data class PixivPage( - val urls: PixivImageUrls? = null, -) - -@Serializable -internal data class PixivImageUrls( - val original: String? = null, - val thumb: String? = null, -) diff --git a/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Util.kt b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Util.kt new file mode 100644 index 000000000..86dcc1a94 --- /dev/null +++ b/src/all/pixiv/src/eu/kanade/tachiyomi/extension/all/pixiv/Util.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.extension.all.pixiv + +internal fun countUp(start: Int = 0) = sequence { + yieldAll(start..Int.MAX_VALUE) + throw RuntimeException("Overflow") +} + +internal fun Iterator.truncateToList(count: Int): List = buildList { + repeat(count) { + if (!hasNext()) return@buildList + add(next()) + } +} + +internal fun parseSMangaUrl(url: String): Pair { + val isSeries = url.getOrNull(1) != 'a' + return Pair(url.substringAfterLast('/'), isSeries) +}