[Shinigami] Fix search with tags + Refactor

master
dragonx943 1 year ago
parent 6418422157
commit fecb1db3be

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