diff --git a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt index 4db9659c..aa008dc5 100644 --- a/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt +++ b/src/main/kotlin/org/koitharu/kotatsu/parsers/site/id/Shinigami.kt @@ -16,249 +16,272 @@ import org.koitharu.kotatsu.parsers.Broken @Broken("TODO: Add author search") @MangaSourceParser("SHINIGAMI", "Shinigami", "id") internal class Shinigami(context: MangaLoaderContext) : - LegacyPagedMangaParser(context, MangaParserSource.SHINIGAMI, 24) { + LegacyPagedMangaParser(context, MangaParserSource.SHINIGAMI, 24) { - override val configKeyDomain = ConfigKey.Domain("id.shinigami.asia") - private val apiSuffix = "api.shngm.io/v1" - private val cdnSuffix = "storage.shngm.id" + override val configKeyDomain = ConfigKey.Domain("id.shinigami.asia") + private val apiSuffix = "api.shngm.io/v1" + private val cdnSuffix = "storage.shngm.id" - override fun onCreateConfig(keys: MutableCollection>) { - super.onCreateConfig(keys) - keys.add(userAgentKey) - } - - override fun getRequestHeaders(): Headers = Headers.Builder() - .add("referer", "https://$domain/") - .add("sec-fetch-dest", "empty") - .build() + override fun onCreateConfig(keys: MutableCollection>) { + super.onCreateConfig(keys) + keys.add(userAgentKey) + } + + override fun getRequestHeaders(): Headers = Headers.Builder() + .add("referer", "https://$domain/") + .add("sec-fetch-dest", "empty") + .build() - override val availableSortOrders: Set = EnumSet.of( - SortOrder.POPULARITY, - SortOrder.POPULARITY_ASC, - SortOrder.NEWEST, - SortOrder.NEWEST_ASC, - SortOrder.RATING, - SortOrder.RATING_ASC, - ) + override val availableSortOrders: Set = EnumSet.of( + SortOrder.POPULARITY, + SortOrder.POPULARITY_ASC, + SortOrder.NEWEST, + SortOrder.NEWEST_ASC, + SortOrder.RATING, + SortOrder.RATING_ASC, + ) - override suspend fun getFilterOptions(): MangaListFilterOptions { - return MangaListFilterOptions( - availableTags = fetchTags(), - availableStates = EnumSet.of(MangaState.ONGOING, MangaState.FINISHED, MangaState.PAUSED), - availableContentTypes = EnumSet.of( - ContentType.MANGA, - ContentType.MANHWA, - ContentType.MANHUA - ), - ) - } + override suspend fun getFilterOptions(): MangaListFilterOptions { + return MangaListFilterOptions( + availableTags = fetchTags(), + availableStates = EnumSet.of( + MangaState.ONGOING, + MangaState.FINISHED, + MangaState.PAUSED + ), + availableContentTypes = EnumSet.of( + ContentType.MANGA, + ContentType.MANHWA, + ContentType.MANHUA + ), + ) + } - override val filterCapabilities: MangaListFilterCapabilities - get() = MangaListFilterCapabilities( - isMultipleTagsSupported = true, - isTagsExclusionSupported = true, - isSearchSupported = true, - ) + override val filterCapabilities: MangaListFilterCapabilities + get() = MangaListFilterCapabilities( + isMultipleTagsSupported = true, + isTagsExclusionSupported = true, + isSearchSupported = true, + isSearchWithFiltersSupported = true, + ) - override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { - val url = buildString { - append("https://") - append(apiSuffix) - append("/manga/list") - append("?page=") - append(page.toString()) - append("&page_size=") - append(pageSize) + override suspend fun getListPage(page: Int, order: SortOrder, filter: MangaListFilter): List { + val url = buildString { + append("https://") + append(apiSuffix) + append("/manga/list") + append("?page=") + append(page.toString()) + append("&page_size=") + append(pageSize) - append("&sort=") - append( - when (order) { - SortOrder.POPULARITY, SortOrder.POPULARITY_ASC -> "popularity" - SortOrder.NEWEST, SortOrder.NEWEST_ASC -> "latest" - SortOrder.RATING, SortOrder.RATING_ASC -> "rating" - else -> "latest" - } - ) - append("&sort_order=") - append( - when (order) { - SortOrder.POPULARITY_ASC, SortOrder.NEWEST_ASC, SortOrder.RATING_ASC -> "asc" - else -> "desc" - } - ) + append("&sort=") + append( + when (order) { + SortOrder.POPULARITY, SortOrder.POPULARITY_ASC -> "popularity" + SortOrder.NEWEST, SortOrder.NEWEST_ASC -> "latest" + SortOrder.RATING, SortOrder.RATING_ASC -> "rating" + else -> "latest" + } + ) + append("&sort_order=") + append( + when (order) { + SortOrder.POPULARITY_ASC, SortOrder.NEWEST_ASC, SortOrder.RATING_ASC -> "asc" + else -> "desc" + } + ) - filter.states.oneOrThrowIfMany()?.let { - append("&status=") - append( - when (it) { - MangaState.FINISHED -> "completed" - MangaState.ONGOING -> "ongoing" - MangaState.PAUSED -> "hiatus" - else -> "" - } - ) - } + filter.states.oneOrThrowIfMany()?.let { + append("&status=") + append( + when (it) { + MangaState.FINISHED -> "completed" + MangaState.ONGOING -> "ongoing" + MangaState.PAUSED -> "hiatus" + else -> "" + } + ) + } - if (filter.types.isNotEmpty()) { - filter.types.forEach { - append("&format=") - append( - when (it) { - ContentType.MANGA -> "manga" - ContentType.MANHUA -> "manhua" - ContentType.MANHWA -> "manhwa" - else -> "" - } - ) - } - } + if (filter.types.isNotEmpty()) { + filter.types.forEach { + append("&format=") + append( + when (it) { + ContentType.MANGA -> "manga" + ContentType.MANHUA -> "manhua" + ContentType.MANHWA -> "manhwa" + else -> "" + } + ) + } + } - if (filter.tags.isNotEmpty()) { - append("&genres_include=") - filter.tags.joinTo(this, ",") { it.key } - append("&genres_include_mode=and") - } + if (filter.tags.isNotEmpty()) { + append("&genre_include=") + filter.tags.joinTo(this, separator = ",") { it.key } + append("&genre_include_mode=and") + } - if (filter.tagsExclude.isNotEmpty()) { - append("&genres_exclude=") - filter.tagsExclude.joinTo(this, ",") { it.key } - append("&genres_exclude_mode=and") - } + if (filter.tagsExclude.isNotEmpty()) { + append("&genre_exclude=") + filter.tagsExclude.joinTo(this, separator = ",") { it.key } + append("&genre_exclude_mode=and") + } - if (!filter.query.isNullOrEmpty()) { - append("&q=") - val encodedQuery = filter.query.splitByWhitespace().joinToString(separator = "+") { part -> - part.urlEncoded() - } - append(encodedQuery) - } - } + if (!filter.query.isNullOrEmpty()) { + append("&q=") + val encodedQuery = filter.query.splitByWhitespace() + .joinToString(separator = "+") { part -> + part.urlEncoded() + } + append(encodedQuery) + } + } - val json = webClient.httpGet(url).parseJson() - val data = json.getJSONArray("data") - return data.mapJSON { jo -> - val id = jo.getString("manga_id") - Manga( - id = generateUid(id), - url = "/manga/detail/$id", - publicUrl = "https://$domain/series/$id", - title = jo.getString("title"), - altTitles = setOf(jo.optString("alternative_title") ?: ""), - coverUrl = jo.getString("cover_image_url"), - largeCoverUrl = jo.optString("cover_portrait_url").takeIf { it.isNotEmpty() }, - authors = jo.optJSONObject("taxonomy")?.optJSONArray("Author")?.mapJSONToSet { x -> - x.getString("name") - }.orEmpty(), - tags = jo.optJSONObject("taxonomy")?.optJSONArray("Genre")?.mapJSONToSet { x -> - MangaTag( - key = x.getString("slug"), - title = x.getString("name"), - source = source - ) - }.orEmpty(), - state = when (jo.getInt("status")) { - 1 -> MangaState.ONGOING - 2 -> MangaState.FINISHED - 3 -> MangaState.PAUSED - else -> null - }, - description = jo.optString("description"), - contentRating = null, - source = source, - rating = RATING_UNKNOWN - ) - } - } + val json = webClient.httpGet(url).parseJson() + val data = json.getJSONArray("data") + + return data.mapJSON { jo -> + val id = jo.getString("manga_id") + Manga( + id = generateUid(id), + url = "/manga/detail/$id", + publicUrl = "https://$domain/series/$id", + title = jo.getString("title"), + altTitles = setOf(jo.optString("alternative_title") ?: ""), + coverUrl = jo.getString("cover_image_url"), + largeCoverUrl = jo.optString("cover_portrait_url") + .takeIf { it.isNotEmpty() }, + authors = jo.optJSONObject("taxonomy") + ?.optJSONArray("Author") + ?.mapJSONToSet { x -> + x.getString("name") + }.orEmpty(), + tags = jo.optJSONObject("taxonomy") + ?.optJSONArray("Genre") + ?.mapJSONToSet { x -> + MangaTag( + key = x.getString("slug"), + title = x.getString("name"), + source = source + ) + }.orEmpty(), + state = when (jo.getInt("status")) { + 1 -> MangaState.ONGOING + 2 -> MangaState.FINISHED + 3 -> MangaState.PAUSED + else -> null + }, + description = jo.optString("description"), + contentRating = null, + source = source, + rating = RATING_UNKNOWN + ) + } + } - override suspend fun getDetails(manga: Manga): Manga { - val json = webClient.httpGet("https://$apiSuffix" + manga.url).parseJson() - val jo = json.getJSONObject("data") - val id = jo.getString("manga_id") - return manga.copy( - tags = jo.optJSONObject("taxonomy")?.optJSONArray("Genre")?.mapJSONToSet { x -> - MangaTag( - key = x.getString("slug"), - title = x.getString("name"), - source = source, - ) - }.orEmpty(), - authors = jo.optJSONObject("taxonomy")?.optJSONArray("Author")?.mapJSONToSet { x -> - x.getString("name") - }.orEmpty(), - state = when (jo.getInt("status")) { - 1 -> MangaState.ONGOING - 2 -> MangaState.FINISHED - 3 -> MangaState.PAUSED - else -> null - }, - description = jo.optString("description"), - largeCoverUrl = jo.optString("cover_portrait_url").takeIf { it.isNotEmpty() }, - chapters = getChapters(id) - ) - } + override suspend fun getDetails(manga: Manga): Manga { + val json = webClient.httpGet("https://$apiSuffix" + manga.url).parseJson() + val jo = json.getJSONObject("data") + val id = jo.getString("manga_id") + + return manga.copy( + tags = jo.optJSONObject("taxonomy") + ?.optJSONArray("Genre") + ?.mapJSONToSet { x -> + MangaTag( + key = x.getString("slug"), + title = x.getString("name"), + source = source, + ) + }.orEmpty(), + authors = jo.optJSONObject("taxonomy") + ?.optJSONArray("Author") + ?.mapJSONToSet { x -> + x.getString("name") + }.orEmpty(), + state = when (jo.getInt("status")) { + 1 -> MangaState.ONGOING + 2 -> MangaState.FINISHED + 3 -> MangaState.PAUSED + else -> null + }, + description = jo.optString("description"), + largeCoverUrl = jo.optString("cover_portrait_url") + .takeIf { it.isNotEmpty() }, + chapters = getChapters(id) + ) + } - private suspend fun getChapters(mangaId: String): List { - val url = "https://$apiSuffix/chapter/$mangaId/list?page=1&page_size=9999&sort_by=chapter_number&sort_order=asc" - val json = webClient.httpGet(url).parseJson() - val data = json.getJSONArray("data") - - return data.mapJSON { jo -> - val chapterId = jo.getString("chapter_id") - val number = jo.getInt("chapter_number") - val title = jo.optString("chapter_title").takeIf { it.isNotEmpty() } - ?: "Chapter $number" - - MangaChapter( - id = generateUid(chapterId), - title = title, - number = number.toFloat(), - url = "chapter/detail/$chapterId", - scanlator = null, - uploadDate = jo.getString("release_date").parseDate(), - branch = null, - source = source, - volume = 0 - ) - } - } + private suspend fun getChapters(mangaId: String): List { + val url = "https://$apiSuffix/chapter/$mangaId/list?page=1&page_size=9999&sort_by=chapter_number&sort_order=asc" + val json = webClient.httpGet(url).parseJson() + val data = json.getJSONArray("data") + + return data.mapJSON { jo -> + val chapterId = jo.getString("chapter_id") + val number = jo.getInt("chapter_number") + val title = jo.optString("chapter_title") + .takeIf { it.isNotEmpty() } + ?: "Chapter $number" + + MangaChapter( + id = generateUid(chapterId), + title = title, + number = number.toFloat(), + url = "chapter/detail/$chapterId", + scanlator = null, + uploadDate = jo.getString("release_date").parseDate(), + branch = null, + source = source, + volume = 0 + ) + } + } - override suspend fun getPages(chapter: MangaChapter): List { - val json = webClient.httpGet("https://$apiSuffix/${chapter.url}").parseJson() - val data = json.getJSONObject("data") - val chapterData = data.getJSONObject("chapter") - val basePath = chapterData.getString("path") - val datas = chapterData.getJSONArray("data") - - return List(datas.length()) { i -> - val imgExt = datas.getString(i) - val imageUrl = "https://$cdnSuffix" + basePath + imgExt - MangaPage( - id = generateUid(imageUrl), - url = imageUrl, - preview = null, - source = source, - ) - } - } + override suspend fun getPages(chapter: MangaChapter): List { + val json = webClient.httpGet("https://$apiSuffix/${chapter.url}").parseJson() + val data = json.getJSONObject("data") + val chapterData = data.getJSONObject("chapter") + val basePath = chapterData.getString("path") + val datas = chapterData.getJSONArray("data") + + return List(datas.length()) { i -> + val imgExt = datas.getString(i) + val imageUrl = "https://$cdnSuffix" + basePath + imgExt + + MangaPage( + id = generateUid(imageUrl), + url = imageUrl, + preview = null, + source = source, + ) + } + } - private suspend fun fetchTags(): Set { - val json = webClient.httpGet("https://$apiSuffix/genre/list").parseJson() - return json.getJSONArray("data").mapJSONToSet { x -> - MangaTag( - key = x.getString("slug"), - title = x.getString("name"), - source = source, - ) - } - } + private suspend fun fetchTags(): Set { + val json = webClient.httpGet("https://$apiSuffix/genre/list").parseJson() + + return json.getJSONArray("data").mapJSONToSet { x -> + MangaTag( + key = x.getString("slug"), + title = x.getString("name"), + source = source, + ) + } + } - private fun String.parseDate(): Long { - return try { - SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - }.parse(this)?.time ?: 0L - } catch (e: Exception) { - 0L - } - } + private fun String.parseDate(): Long { + return try { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US) + .apply { + timeZone = TimeZone.getTimeZone("UTC") + } + .parse(this)?.time ?: 0L + } catch (e: Exception) { + 0L + } + } } \ No newline at end of file