310 lines
12 KiB
Kotlin
310 lines
12 KiB
Kotlin
package exh.md.handlers
|
|
|
|
import com.elvishew.xlog.XLog
|
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
import eu.kanade.tachiyomi.network.GET
|
|
import eu.kanade.tachiyomi.network.await
|
|
import eu.kanade.tachiyomi.network.parseAs
|
|
import eu.kanade.tachiyomi.source.model.SChapter
|
|
import eu.kanade.tachiyomi.source.model.SManga
|
|
import exh.log.xLogE
|
|
import exh.md.handlers.serializers.AuthorResponseList
|
|
import exh.md.handlers.serializers.ChapterResponse
|
|
import exh.md.handlers.serializers.MangaResponse
|
|
import exh.md.utils.MdUtil
|
|
import exh.metadata.metadata.MangaDexSearchMetadata
|
|
import exh.metadata.metadata.base.RaisedTag
|
|
import exh.metadata.metadata.base.getFlatMetadataForManga
|
|
import exh.metadata.metadata.base.insertFlatMetadata
|
|
import exh.util.executeOnIO
|
|
import exh.util.floor
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Response
|
|
import tachiyomi.source.model.ChapterInfo
|
|
import tachiyomi.source.model.MangaInfo
|
|
import uy.kohesive.injekt.injectLazy
|
|
import java.util.Date
|
|
import java.util.Locale
|
|
|
|
class ApiMangaParser(val client: OkHttpClient, private val lang: String) {
|
|
val db: DatabaseHelper by injectLazy()
|
|
|
|
val metaClass = MangaDexSearchMetadata::class
|
|
|
|
/**
|
|
* Use reflection to create a new instance of metadata
|
|
*/
|
|
private fun newMetaInstance() = metaClass.constructors.find {
|
|
it.parameters.isEmpty()
|
|
}?.call()
|
|
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
|
|
|
suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List<String>, sourceId: Long): MangaInfo {
|
|
return parseToManga(manga, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls, sourceId)
|
|
}
|
|
|
|
suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, coverUrls: List<String>, sourceId: Long): MangaInfo {
|
|
val mangaId = db.getManga(manga.key, sourceId).executeOnIO()?.id
|
|
val metadata = if (mangaId != null) {
|
|
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO()
|
|
flatMetadata?.raise(metaClass) ?: newMetaInstance()
|
|
} else newMetaInstance()
|
|
|
|
parseIntoMetadata(metadata, input, coverUrls)
|
|
if (mangaId != null) {
|
|
metadata.mangaId = mangaId
|
|
db.insertFlatMetadata(metadata.flatten())
|
|
}
|
|
|
|
return metadata.createMangaInfo(manga)
|
|
}
|
|
|
|
/**
|
|
* Parse the manga details json into metadata object
|
|
*/
|
|
suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) {
|
|
parseIntoMetadata(metadata, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls)
|
|
}
|
|
|
|
suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, networkApiManga: MangaResponse, coverUrls: List<String>) {
|
|
with(metadata) {
|
|
try {
|
|
val networkManga = networkApiManga.data.attributes
|
|
mdUuid = networkApiManga.data.id
|
|
title = MdUtil.cleanString(networkManga.title[lang] ?: networkManga.title["en"]!!)
|
|
altTitles = networkManga.altTitles.mapNotNull { it[lang] }
|
|
cover =
|
|
if (coverUrls.isNotEmpty()) {
|
|
coverUrls.last()
|
|
} else {
|
|
null
|
|
// networkManga.mainCover
|
|
}
|
|
|
|
description = MdUtil.cleanDescription(networkManga.description[lang] ?: networkManga.description["en"]!!)
|
|
|
|
// get authors ignore if they error, artists are labelled as authors currently
|
|
val authorIds = networkApiManga.relationships.filter { relationship ->
|
|
relationship.type.equals("author", true)
|
|
}.map { relationship -> relationship.id }
|
|
.distinct()
|
|
val artistIds = networkApiManga.relationships.filter { relationship ->
|
|
relationship.type.equals("artist", true)
|
|
}.map { relationship -> relationship.id }
|
|
.distinct()
|
|
|
|
val authorMap = runCatching {
|
|
val ids = (authorIds + artistIds).distinct().joinToString("&ids[]=", "?ids[]=")
|
|
val response = client.newCall(GET("${MdUtil.authorUrl}$ids")).await()
|
|
.parseAs<AuthorResponseList>()
|
|
response.results.map {
|
|
it.data.id to MdUtil.cleanString(it.data.attributes.name)
|
|
}.toMap()
|
|
}.getOrNull() ?: emptyMap()
|
|
|
|
authors = authorIds.mapNotNull { authorMap[it] }.takeUnless { it.isEmpty() }
|
|
artists = artistIds.mapNotNull { authorMap[it] }.takeUnless { it.isEmpty() }
|
|
|
|
langFlag = networkManga.originalLanguage
|
|
val lastChapter = networkManga.lastChapter.toFloatOrNull()
|
|
lastChapterNumber = lastChapter?.floor()
|
|
|
|
/*networkManga.rating?.let {
|
|
manga.rating = it.bayesian ?: it.mean
|
|
manga.users = it.users
|
|
}*/
|
|
|
|
networkManga.links?.let {
|
|
it["al"]?.let { anilistId = it }
|
|
it["kt"]?.let { kitsuId = it }
|
|
it["mal"]?.let { myAnimeListId = it }
|
|
it["mu"]?.let { mangaUpdatesId = it }
|
|
it["ap"]?.let { animePlanetId = it }
|
|
}
|
|
// val filteredChapters = filterChapterForChecking(networkApiManga)
|
|
|
|
val tempStatus = parseStatus(networkManga.status ?: "")
|
|
val publishedOrCancelled =
|
|
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
|
|
/*if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
|
|
manga.status = SManga.COMPLETED
|
|
manga.missing_chapters = null
|
|
} else {*/
|
|
status = tempStatus
|
|
// }
|
|
|
|
// things that will go with the genre tags but aren't actually genre
|
|
val nonGenres = listOfNotNull(
|
|
networkManga.publicationDemographic?.let { RaisedTag("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
|
|
networkManga.contentRating?.let { RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
|
|
)
|
|
|
|
val genres = nonGenres + networkManga.tags
|
|
.mapNotNull { dexTag ->
|
|
dexTag.attributes.name[lang] ?: dexTag.attributes.name["en"]
|
|
}.map {
|
|
RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
|
|
}
|
|
|
|
if (tags.isNotEmpty()) tags.clear()
|
|
tags += genres
|
|
} catch (e: Exception) {
|
|
xLogE("Parse into metadata error", e)
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If chapter title is oneshot or a chapter exists which matches the last chapter in the required language
|
|
* return manga is complete
|
|
*/
|
|
/*private fun isMangaCompleted(
|
|
serializer: ApiMangaSerializer,
|
|
filteredChapters: List<ChapterSerializer>
|
|
): Boolean {
|
|
if (filteredChapters.isEmpty() || serializer.data.manga.lastChapter.isNullOrEmpty()) {
|
|
return false
|
|
}
|
|
val finalChapterNumber = serializer.data.manga.lastChapter!!
|
|
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
|
|
filteredChapters.firstOrNull()?.let {
|
|
if (isOneShot(it, finalChapterNumber)) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
val removeOneshots = filteredChapters.asSequence()
|
|
.map { it.chapter!!.toDoubleOrNull() }
|
|
.filter { it != null }
|
|
.map { floor(it!!).toInt() }
|
|
.filter { it != 0 }
|
|
.toList().distinctBy { it }
|
|
return removeOneshots.toList().size == floor(finalChapterNumber.toDouble()).toInt()
|
|
}*/
|
|
|
|
/* private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<ChapterSerializer> {
|
|
serializer.data.chapters ?: return emptyList()
|
|
return serializer.data.chapters.asSequence()
|
|
.filter { langs.contains(it.language) }
|
|
.filter {
|
|
it.chapter?.let { chapterNumber ->
|
|
if (chapterNumber.toDoubleOrNull() == null) {
|
|
return@filter false
|
|
}
|
|
return@filter true
|
|
}
|
|
return@filter false
|
|
}.toList()
|
|
}*/
|
|
|
|
/*private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean {
|
|
return chapter.title.equals("oneshot", true) ||
|
|
((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber))
|
|
}*/
|
|
|
|
private fun parseStatus(status: String) = when (status) {
|
|
"ongoing" -> SManga.ONGOING
|
|
"complete" -> SManga.PUBLICATION_COMPLETE
|
|
"abandoned" -> SManga.CANCELLED
|
|
"hiatus" -> SManga.HIATUS
|
|
else -> SManga.UNKNOWN
|
|
}
|
|
|
|
/**
|
|
* Parse for the random manga id from the [MdUtil.randMangaPage] response.
|
|
*/
|
|
fun randomMangaIdParse(response: Response): String {
|
|
return response.parseAs<MangaResponse>(MdUtil.jsonParser).data.id
|
|
}
|
|
|
|
fun chapterListParse(chapterListResponse: List<ChapterResponse>, groupMap: Map<String, String>): List<ChapterInfo> {
|
|
val now = Date().time
|
|
|
|
return chapterListResponse.asSequence()
|
|
.map {
|
|
mapChapter(it, groupMap)
|
|
}.filter {
|
|
it.dateUpload <= now && "MangaPlus" != it.scanlator
|
|
}.toList()
|
|
}
|
|
|
|
fun chapterParseForMangaId(response: Response): String {
|
|
try {
|
|
return response.parseAs<ChapterResponse>(MdUtil.jsonParser)
|
|
.relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
|
|
} catch (e: Exception) {
|
|
XLog.e(e)
|
|
throw e
|
|
}
|
|
}
|
|
|
|
private fun mapChapter(
|
|
networkChapter: ChapterResponse,
|
|
groups: Map<String, String>,
|
|
): ChapterInfo {
|
|
val chapter = SChapter.create()
|
|
val attributes = networkChapter.data.attributes
|
|
val key = MdUtil.chapterSuffix + networkChapter.data.id
|
|
val chapterName = mutableListOf<String>()
|
|
// Build chapter name
|
|
|
|
if (attributes.volume != null) {
|
|
val vol = "Vol." + attributes.volume
|
|
chapterName.add(vol)
|
|
// todo
|
|
// chapter.vol = vol
|
|
}
|
|
|
|
if (attributes.chapter.isNullOrBlank().not()) {
|
|
if (chapterName.isNotEmpty()) {
|
|
chapterName.add("-")
|
|
}
|
|
val chp = "Ch.${attributes.chapter}"
|
|
chapterName.add(chp)
|
|
// chapter.chapter_txt = chp
|
|
}
|
|
|
|
if (attributes.title.isNullOrBlank().not()) {
|
|
if (chapterName.isNotEmpty()) {
|
|
chapterName.add("-")
|
|
}
|
|
chapterName.add(attributes.title!!)
|
|
chapter.name = MdUtil.cleanString(attributes.title)
|
|
}
|
|
|
|
// if volume, chapter and title is empty its a oneshot
|
|
if (chapterName.isEmpty()) {
|
|
chapterName.add("Oneshot")
|
|
}
|
|
/*if ((status == 2 || status == 3)) {
|
|
if (finalChapterNumber != null) {
|
|
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
|
|
networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
|
|
) {
|
|
chapterName.add("[END]")
|
|
}
|
|
}
|
|
}*/
|
|
|
|
val name = MdUtil.cleanString(chapterName.joinToString(" "))
|
|
// Convert from unix time
|
|
val dateUpload = MdUtil.parseDate(attributes.publishAt)
|
|
|
|
val scanlatorName = networkChapter.relationships.filter { it.type == "scanlation_group" }.mapNotNull { groups[it.id] }.toSet()
|
|
|
|
val scanlator = MdUtil.cleanString(MdUtil.getScanlatorString(scanlatorName))
|
|
|
|
// chapter.mangadex_chapter_id = MdUtil.getChapterId(chapter.url)
|
|
|
|
// chapter.language = MdLang.fromIsoCode(attributes.translatedLanguage)?.prettyPrint ?: ""
|
|
|
|
return ChapterInfo(
|
|
key = key,
|
|
name = name,
|
|
scanlator = scanlator,
|
|
dateUpload = dateUpload,
|
|
)
|
|
}
|
|
}
|