diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/Luscious.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/Luscious.kt index f41b44224..82fc4325b 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/Luscious.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/Luscious.kt @@ -16,48 +16,38 @@ 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.HttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import org.jsoup.nodes.Document import rx.Observable -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale abstract class Luscious( override val name: String, override val baseUrl: String, override val lang: String ) : HttpSource() { - //Based on Luscios single source extension form https://github.com/tachiyomiorg/tachiyomi-extensions/commit/aacf56d0c0ddb173372aac69d798ae998f178377 - //with modifiaction to make it support multisrc + //Based on Luscios single source extension form https://github.com/tachiyomiorg/tachiyomi-extensions/commit/aacf56d0c0ddb173372aac69d798ae998f178377 + //with modifiaction to make it support multisrc override val supportsLatest: Boolean = true private val apiBaseUrl: String = "$baseUrl/graphql/nobatch/" private val gson = Gson() override val client: OkHttpClient = network.cloudflareClient - private val lusLang: String = lusLang(lang) - private fun lusLang(lang: String): String { - return when (lang) { - "en" -> "1" - "ja" -> "2" - "es" -> "3" - "it" -> "4" - "de" -> "5" - "fr" -> "6" - "zh" -> "8" - "ko" -> "9" - "pt" -> "100" - "th" -> "101" - else -> "99" - } + private val lusLang: String = when (lang) { + "en" -> "1" + "ja" -> "2" + "es" -> "3" + "it" -> "4" + "de" -> "5" + "fr" -> "6" + "zh" -> "8" + "ko" -> "9" + "pt" -> "100" + "th" -> "101" + else -> "99" } - // Common private fun buildAlbumListRequestInput(page: Int, filters: FilterList, query: String = ""): JsonObject { @@ -68,6 +58,8 @@ abstract class Luscious( val tagsFilter = filters.findInstance()!! val genreFilter = filters.findInstance()!! val contentTypeFilter = filters.findInstance()!! + val albumSizeFilter = filters.findInstance()!! + val restrictGenresFilter = filters.findInstance()!! return JsonObject().apply { add( @@ -85,6 +77,12 @@ abstract class Luscious( if (albumTypeFilter.selected != FILTER_VALUE_IGNORE) add(albumTypeFilter.toJsonObject("album_type")) + if (albumSizeFilter.selected != FILTER_VALUE_IGNORE) + add(albumSizeFilter.toJsonObject("picture_count_rank")) + + if (restrictGenresFilter.selected != FILTER_VALUE_IGNORE) + add(restrictGenresFilter.toJsonObject("restrict_genres")) + with(interestsFilter) { if (this.selected.isEmpty()) { throw Exception("Please select an Interest") @@ -147,6 +145,22 @@ abstract class Luscious( } } + private fun buildAlbumInfoRequestInput(id: String): JsonObject { + return JsonObject().apply { + addProperty("id", id) + } + } + + private fun buildAlbumInfoRequest(id: String): Request { + val input = buildAlbumInfoRequestInput(id) + val url = HttpUrl.parse(apiBaseUrl)!!.newBuilder() + .addQueryParameter("operationName", "AlbumGet") + .addQueryParameter("query", albumInfoQuery) + .addQueryParameter("variables", input.toString()) + .toString() + return GET(url, headers) + } + // Latest override fun latestUpdatesRequest(page: Int): Request = buildAlbumListRequest(page, getSortFilters(LATEST_DEFAULT_SORT_STATE)) @@ -155,20 +169,47 @@ abstract class Luscious( // Chapters - override fun chapterListParse(response: Response): List { - val document = response.asJsoup() - return listOf( - SChapter.create().apply { - url = response.request().url().toString() - name = "Chapter" - date_upload = document.select(".album-info-item:contains(Created:)")?.first()?.ownText()?.trim()?.let { - DATE_FORMATS_WITH_ORDINAL_SUFFIXES.mapNotNull { format -> format.parseOrNull(it) }.firstOrNull()?.time - } ?: 0L - chapter_number = 1f - } - ) + override fun fetchChapterList(manga: SManga): Observable> { + val id = manga.url.substringAfterLast("_").removeSuffix("/") + + return client.newCall(GET(buildAlbumPicturesPageUrl(id, 1, "position"))) + .asObservableSuccess() + .map { parseAlbumPicturesResponse(it, "position") } } + private fun parseAlbumPicturesResponse(response: Response, sortPagesByOption: String): List { + val chapters = mutableListOf() + var nextPage = true + var page = 2 + val id = response.request().url().queryParameter("variables").toString() + .let { gson.fromJson(it)["input"]["filters"].asJsonArray } + .let { it.first { f -> f["name"].asString == "album_id" } } + .let { it["value"].asString } + + var data = gson.fromJson(response.body()!!.string()) + .let { it["data"]["picture"]["list"].asJsonObject } + + while (nextPage) { + nextPage = data["info"]["has_next_page"].asBoolean + data["items"].asJsonArray.map { + val chapter = SChapter.create() + chapter.url = it["url_to_original"].asString + chapter.name = it["title"].asString + //chapter.date_upload = it["created"].asLong // not parsing correctly for some reason + chapter.chapter_number = it["position"].asInt.toFloat() + chapters.add(chapter) + } + if (nextPage) { + val newPage = client.newCall(GET(buildAlbumPicturesPageUrl(id, page, sortPagesByOption))).execute() + data = gson.fromJson(newPage.body()!!.string()) + .let { it["data"]["picture"]["list"].asJsonObject } + } + page++ + } + return chapters.reversed() + } + + override fun chapterListParse(response: Response): List = throw UnsupportedOperationException("Not used") // Pages private fun buildAlbumPicturesRequestInput(id: String, page: Int, sortPagesByOption: String): JsonObject { @@ -203,43 +244,8 @@ abstract class Luscious( .toString() } - private fun parseAlbumPicturesResponse(response: Response, sortPagesByOption: String): List { - - val id = response.request().url().queryParameter("variables").toString() - .let { gson.fromJson(it)["input"]["filters"].asJsonArray } - .let { it.first { f -> f["name"].asString == "album_id" } } - .let { it["value"].asString } - - val data = gson.fromJson(response.body()!!.string()) - .let { it["data"]["picture"]["list"].asJsonObject } - - return data["items"].asJsonArray.mapIndexed { index, it -> - Page(index, imageUrl = it["thumbnails"][0]["url"].asString) - } + if (data["info"]["total_pages"].asInt > 1) { // get 2nd page onwards - (ITEMS_PER_PAGE until data["info"]["total_items"].asInt).chunked(ITEMS_PER_PAGE).mapIndexed { page, indices -> - indices.map { Page(it, url = buildAlbumPicturesPageUrl(id, page + 2, sortPagesByOption)) } - }.flatten() - } else emptyList() - } - - private fun getAlbumSortPagesOption(chapter: SChapter): Observable { - return client.newCall(GET(chapter.url)) - .asObservableSuccess() - .map { - val sortByKey = it.asJsoup().select(".o-input-select:contains(Sorted By) .o-select-value")?.text() ?: "" - ALBUM_PICTURES_SORT_OPTIONS.getValue(sortByKey) - } - } - override fun fetchPageList(chapter: SChapter): Observable> { - val id = chapter.url.substringAfterLast("_").removeSuffix("/") - - return getAlbumSortPagesOption(chapter) - .concatMap { sortPagesByOption -> - client.newCall(GET(buildAlbumPicturesPageUrl(id, 1, sortPagesByOption))) - .asObservableSuccess() - .map { parseAlbumPicturesResponse(it, sortPagesByOption) } - } + return Observable.just(listOf(Page(0, chapter.url, chapter.url))) } override fun pageListParse(response: Response): List = throw UnsupportedOperationException("Not used") @@ -257,52 +263,47 @@ abstract class Luscious( val data = gson.fromJson(it.body()!!.string()).let { data -> data["data"]["picture"]["list"].asJsonObject } - data["items"].asJsonArray[page.index % 50].asJsonObject["thumbnails"][0]["url"].asString + data["items"].asJsonArray[page.index % 50].asJsonObject["url_to_original"].asString } } // Details - private fun parseMangaGenre(document: Document): String { - return listOf( - document.select(".o-tag--secondary").map { it.text().substringBefore("(").trim() }, - document.select(".o-tag:not([href *= /tags/artist])").map { it.text() }, - document.select(".album-info-item:contains(Content:) .o-tag").map { it.text() } - ).flatten().joinToString() - } - - private fun parseMangaDescription(document: Document): String { - val pageCount: String? = ( - document.select(".album-info-item:contains(pictures)").firstOrNull() - ?: document.select(".album-info-item:contains(gifs)").firstOrNull() - )?.text() - - return listOf( - Pair("Description", document.select(".album-description:last-of-type")?.text()), - Pair("Pages", pageCount) - ).let { - it + listOf("Parody", "Character", "Ethnicity") - .map { key -> key to document.select(".o-tag--category:contains($key) .o-tag").joinToString { t -> t.text() } } - }.filter { desc -> !desc.second.isNullOrBlank() } - .joinToString("\n\n") { "${it.first}:\n${it.second}" } + override fun mangaDetailsRequest(manga: SManga): Request { + val id = manga.url.substringAfterLast("_").removeSuffix("/") + return buildAlbumInfoRequest(id) } override fun mangaDetailsParse(response: Response): SManga { - val document = response.asJsoup() - return SManga.create().apply { - - artist = document.select(".o-tag--category:contains(Artist:) .o-tag")?.joinToString { it.text() } - author = artist - - genre = parseMangaGenre(document) - - title = document.select("a[title]").text() - status = when { - title.contains("ongoing", true) -> SManga.ONGOING - else -> SManga.COMPLETED + val data = gson.fromJson(response.body()!!.string()) + with(data["data"]["album"]["get"]) { + val manga = SManga.create() + manga.url = this["url"].asString + manga.title = this["title"].asString + manga.thumbnail_url = this["cover"]["url"].asString + manga.status = 0 + manga.description = "${this["description"].asString}\n\nPictures: ${this["number_of_pictures"].asString}\nAnimated Pictures: ${this["number_of_animated_pictures"].asString}" + var genreList = this["language"]["title"].asString + for ((i, _) in this["labels"].asJsonArray.withIndex()) { + genreList = "$genreList, ${this["labels"][i].asString}" } + for ((i, _) in this["genres"].asJsonArray.withIndex()) { + genreList = "$genreList, ${this["genres"][i]["title"].asString}" + } + for ((i, _) in this["audiences"].asJsonArray.withIndex()) { + genreList = "$genreList, ${this["audiences"][i]["title"].asString}" + } + for ((i, _) in this["tags"].asJsonArray.withIndex()) { + genreList = "$genreList, ${this["tags"][i]["text"].asString}" + if (this["tags"][i]["text"].asString.contains("Artist:")){ + manga.artist = this["tags"][i]["text"].asString.substringAfter(":").trim() + manga.author = manga.artist + } + } + genreList = "$genreList, ${this["content"]["title"].asString}" + manga.genre = genreList - description = parseMangaDescription(document) + return manga } } @@ -371,6 +372,8 @@ abstract class Luscious( class SortBySelectFilter(options: List, default: Int) : SelectFilter("Sort By", options, default) class AlbumTypeSelectFilter(options: List) : SelectFilter("Album Type", options) class ContentTypeSelectFilter(options: List) : SelectFilter("Content Type", options) + class RestrictGenresSelectFilter(options: List) : SelectFilter("Restrict Genres", options) + class AlbumSizeSelectFilter(options: List) : SelectFilter("Album Size", options) override fun getFilterList(): FilterList = getSortFilters(POPULAR_DEFAULT_SORT_STATE) @@ -378,6 +381,8 @@ abstract class Luscious( SortBySelectFilter(getSortFilters(), sortState), AlbumTypeSelectFilter(getAlbumTypeFilters()), ContentTypeSelectFilter(getContentTypeFilters()), + AlbumSizeSelectFilter(getAlbumSizeFilters()), + RestrictGenresSelectFilter(getRestrictGenresFilters()), InterestGroupFilter(getInterestFilters()), LanguageGroupFilter(getLanguageFilters()), TagGroupFilter(getTagFilters()), @@ -391,8 +396,8 @@ abstract class Luscious( SelectFilterOption("Rating - Last 30 Days", "rating_30_days"), SelectFilterOption("Rating - Last 90 Days", "rating_90_days"), SelectFilterOption("Rating - Last Year", "rating_1_year"), - SelectFilterOption("Rating - Last Year", "rating_1_year"), SelectFilterOption("Date - Newest First", "date_newest"), + SelectFilterOption("Date - 2020", "date_2021"), SelectFilterOption("Date - 2020", "date_2020"), SelectFilterOption("Date - 2019", "date_2019"), SelectFilterOption("Date - 2018", "date_2018"), @@ -405,7 +410,39 @@ abstract class Luscious( SelectFilterOption("Date - Upcoming", "date_upcoming"), SelectFilterOption("Date - Trending", "date_trending"), SelectFilterOption("Date - Featured", "date_featured"), - SelectFilterOption("Date - Last Viewed", "date_last_interaction") + SelectFilterOption("Date - Last Viewed", "date_last_interaction"), + SelectFilterOption("First Letter - Any", "alpha_any"), + SelectFilterOption("First Letter - A", "alpha_a"), + SelectFilterOption("First Letter - B", "alpha_b"), + SelectFilterOption("First Letter - C", "alpha_c"), + SelectFilterOption("First Letter - D", "alpha_d"), + SelectFilterOption("First Letter - Any", "alpha_any"), + SelectFilterOption("First Letter - A", "alpha_a"), + SelectFilterOption("First Letter - B", "alpha_b"), + SelectFilterOption("First Letter - C", "alpha_c"), + SelectFilterOption("First Letter - D", "alpha_d"), + SelectFilterOption("First Letter - E", "alpha_e"), + SelectFilterOption("First Letter - F", "alpha_f"), + SelectFilterOption("First Letter - G", "alpha_g"), + SelectFilterOption("First Letter - H", "alpha_h"), + SelectFilterOption("First Letter - I", "alpha_i"), + SelectFilterOption("First Letter - J", "alpha_j"), + SelectFilterOption("First Letter - K", "alpha_k"), + SelectFilterOption("First Letter - L", "alpha_l"), + SelectFilterOption("First Letter - M", "alpha_m"), + SelectFilterOption("First Letter - N", "alpha_n"), + SelectFilterOption("First Letter - O", "alpha_o"), + SelectFilterOption("First Letter - P", "alpha_p"), + SelectFilterOption("First Letter - Q", "alpha_q"), + SelectFilterOption("First Letter - R", "alpha_r"), + SelectFilterOption("First Letter - S", "alpha_s"), + SelectFilterOption("First Letter - T", "alpha_t"), + SelectFilterOption("First Letter - U", "alpha_u"), + SelectFilterOption("First Letter - V", "alpha_v"), + SelectFilterOption("First Letter - W", "alpha_w"), + SelectFilterOption("First Letter - X", "alpha_x"), + SelectFilterOption("First Letter - Y", "alpha_y"), + SelectFilterOption("First Letter - Z", "alpha_z"), ) fun getAlbumTypeFilters() = listOf( @@ -414,6 +451,12 @@ abstract class Luscious( SelectFilterOption("Pictures", "pictures") ) + private fun getRestrictGenresFilters() = listOf( + SelectFilterOption("None", FILTER_VALUE_IGNORE), + SelectFilterOption("Loose", "loose"), + SelectFilterOption("Strict", "strict") + ) + fun getContentTypeFilters() = listOf( SelectFilterOption("All", FILTER_VALUE_IGNORE), SelectFilterOption("Hentai", "0"), @@ -421,6 +464,17 @@ abstract class Luscious( SelectFilterOption("Real People", "6") ) + private fun getAlbumSizeFilters() = listOf( + SelectFilterOption("All", FILTER_VALUE_IGNORE), + SelectFilterOption("0-25", "0"), + SelectFilterOption("0-50", "1"), + SelectFilterOption("50-100", "2"), + SelectFilterOption("100-200", "3"), + SelectFilterOption("200-800", "4"), + SelectFilterOption("800-3200", "5"), + SelectFilterOption("3200-12800", "6"), + ) + fun getInterestFilters() = listOf( CheckboxFilterOption("Straight Sex", "1"), CheckboxFilterOption("Trans x Girl", "10", false), @@ -531,28 +585,8 @@ abstract class Luscious( private inline fun Iterable<*>.findInstance() = find { it is T } as? T - private fun SimpleDateFormat.parseOrNull(string: String): Date? { - return try { - parse(string) - } catch (e: ParseException) { - null - } - } - companion object { - private val ALBUM_PICTURES_SORT_OPTIONS = hashMapOf( - Pair("Sort By Newest", "date_newest"), - Pair("Sort By Rating", "rating_all_time") - ).withDefault { "position" } - - private const val ITEMS_PER_PAGE = 50 - - private val ORDINAL_SUFFIXES = listOf("st", "nd", "rd", "th") - private val DATE_FORMATS_WITH_ORDINAL_SUFFIXES = ORDINAL_SUFFIXES.map { - SimpleDateFormat("MMMM dd'$it', yyyy", Locale.US) - } - const val ENGLISH_LUS_LANG_VAL = "1" const val JAPANESE_LUS_LANG_VAL = "2" const val SPANISH_LUS_LANG_VAL = "3" @@ -594,8 +628,13 @@ abstract class Luscious( total_pages page has_next_page + items_per_page } items { + created + title + url_to_original + position thumbnails { url } @@ -604,5 +643,23 @@ abstract class Luscious( } } """.replace("\n", " ").replace("\\s+".toRegex(), " ") + + val albumInfoQuery = """ + query AlbumGet(${"$"}id: ID!) { + album { + get(id: ${"$"}id) { + ... on Album { ...AlbumStandard } + ... on MutationError { + errors { + code message + } + } + } + } + } + fragment AlbumStandard on Album { + __typename id title labels description created modified like_status number_of_favorites number_of_dislikes rating moderation_status marked_for_deletion marked_for_processing number_of_pictures number_of_animated_pictures number_of_duplicates slug is_manga url download_url permissions cover { width height size url } created_by { id url name display_name user_title avatar { url size } } content { id title url } language { id title url } tags { category text url count } genres { id title slug url } audiences { id title url url } last_viewed_picture { id position url } is_featured featured_date featured_by { id url name display_name user_title avatar { url size } } + } + """.trimIndent() } } diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/LusciousGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/LusciousGenerator.kt index f1bc21bb8..02a957c03 100644 --- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/LusciousGenerator.kt +++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/luscious/LusciousGenerator.kt @@ -10,7 +10,7 @@ class LusciousGenerator : ThemeSourceGenerator { override val themeClass = "Luscious" - override val baseVersionCode: Int = 2 + override val baseVersionCode: Int = 3 override val sources = listOf( MultiLang("Luscious", "https://www.luscious.net", listOf("en","ja", "es", "it", "de", "fr", "zh", "ko", "other", "pt", "th"), isNsfw = true, className = "LusciousFactory", overrideVersionCode = 2),