2021-11-13 22:52:50 -05:00

347 lines
12 KiB
Kotlin

package exh.md.utils
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex
import exh.log.xLogD
import exh.md.dto.LoginBodyTokenDto
import exh.md.dto.MangaDataDto
import exh.md.network.NoSessionException
import exh.source.getMainSource
import exh.util.floor
import exh.util.nullIfBlank
import exh.util.nullIfZero
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.parser.Parser
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
class MdUtil {
companion object {
const val cdnUrl = "https://uploads.mangadex.org"
const val baseUrl = "https://mangadex.org"
const val chapterSuffix = "/chapter/"
const val similarCacheMapping = "https://api.similarmanga.com/mapping/mdex2search.csv"
const val similarCacheMangas = "https://api.similarmanga.com/manga/"
const val similarBaseApi = "https://api.similarmanga.com/similar/"
const val groupSearchUrl = "$baseUrl/groups/0/1/"
const val reportUrl = "https://api.mangadex.network/report"
const val mdAtHomeTokenLifespan = 10 * 60 * 1000
const val mangaLimit = 20
/**
* Get the manga offset pages are 1 based, so subtract 1
*/
fun getMangaListOffset(page: Int): String = (mangaLimit * (page - 1)).toString()
val jsonParser =
Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
prettyPrint = true
}
private const val scanlatorSeparator = " & "
const val contentRatingSafe = "safe"
const val contentRatingSuggestive = "suggestive"
const val contentRatingErotica = "erotica"
const val contentRatingPornographic = "pornographic"
val validOneShotFinalChapters = listOf("0", "1")
val englishDescriptionTags = listOf(
"[b][u]English:",
"[b][u]English",
"English:",
"English :",
"[English]:",
"English Translaton:",
"[B][ENG][/B]"
)
val bbCodeToRemove = listOf(
"list", "*", "hr", "u", "b", "i", "s", "center", "spoiler="
)
val descriptionLanguages = listOf(
"=FRANCAIS=",
"[b] Spanish: [/ b]",
"[b][u]Chinese",
"[b][u]French",
"[b][u]German / Deutsch",
"[b][u]Russian",
"[b][u]Spanish",
"[b][u]Vietnamese",
"[b]External Links",
"[b]Link[/b]",
"[b]Links:",
"[Español]:",
"[hr]Fr:",
"[hr]TH",
"[INDO]",
"[PTBR]",
"[right][b][u]Persian",
"[RUS]",
"[u]Russian",
"\r\n\r\nItalian\r\n",
"Arabic /",
"Descriptions in Other Languages",
"Espanol",
"[Españ",
"Españ",
"Farsi/",
"Français",
"French - ",
"Francois",
"French:",
"French/",
"French /",
"German/",
"German /",
"Hindi /",
"Bahasa Indonesia",
"Indonesia:",
"Indonesian:",
"Indonesian :",
"Indo:",
"[u]Indonesian",
"Italian / ",
"Italian Summary:",
"Italian/",
"Italiano",
"Italian:",
"Italian summary:",
"Japanese /",
"Original Japanese",
"Official Japanese Translation",
"Official Chinese Translation",
"Official French Translation",
"Official Indonesian Translation",
"Links:",
"Pasta-Pizza-Mandolino/Italiano",
"Persian/فارسی",
"Persian /فارسی",
"Polish /",
"Polish Summary /",
"Polish/",
"Polski",
"Português",
"Portuguese (BR)",
"PT/BR:",
"Pt/Br:",
"Pt-Br:",
"Portuguese /",
"[right]",
"Résumé Français",
"Résume Français",
"RÉSUMÉ FRANCAIS :",
"RUS:",
"Ru/Pyc",
"\\r\\nRUS\\r\\n",
"Russia/",
"Russian /",
"Spanish:",
"Spanish /",
"Spanish Summary:",
"Spanish/",
"Türkçe",
"Thai:",
"Turkish /",
"Turkish/",
"Turkish:",
"Русский",
"العربية",
"정보",
"(zh-Hant)",
)
fun buildMangaUrl(mangaUuid: String): String {
return "/manga/$mangaUuid"
}
// Get the ID from the manga url
fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/")
fun getChapterId(url: String) = url.substringAfterLast("/")
fun cleanString(string: String): String {
var cleanedString = string
bbCodeToRemove.forEach {
cleanedString = cleanedString.replace("[$it]", "", true)
.replace("[/$it]", "", true)
}
val bbRegex =
"""\[(\w+)[^]]*](.*?)\[/\1]""".toRegex()
// Recursively remove nested bbcode
while (bbRegex.containsMatchIn(cleanedString)) {
cleanedString = cleanedString.replace(bbRegex, "$2")
}
return Parser.unescapeEntities(cleanedString, false)
}
fun cleanDescription(string: String): String {
var newDescription = string
descriptionLanguages.forEach {
newDescription = newDescription.substringBefore(it)
}
englishDescriptionTags.forEach {
newDescription = newDescription.replace(it, "")
}
return cleanString(newDescription).trim()
}
fun getImageUrl(attr: String): String {
// Some images are hosted elsewhere
if (attr.startsWith("http")) {
return attr
}
return baseUrl + attr
}
fun getScanlators(scanlators: String?): Set<String> {
return scanlators?.split(scanlatorSeparator)?.toSet().orEmpty()
}
fun getScanlatorString(scanlators: Set<String>): String {
return scanlators.sorted().joinToString(scanlatorSeparator)
}
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
if (mangaStatus == SManga.COMPLETED) return null
val remove0ChaptersFromCount = chapters.distinctBy {
/*if (it.chapter_txt.isNotEmpty()) {
it.vol + it.chapter_txt
} else {*/
it.name
/*}*/
}.sortedByDescending { it.chapter_number }
remove0ChaptersFromCount.firstOrNull()?.let { chapter ->
val chpNumber = chapter.chapter_number.floor()
val allChapters = (1..chpNumber).toMutableSet()
remove0ChaptersFromCount.forEach {
allChapters.remove(it.chapter_number.floor())
}
if (allChapters.isEmpty()) return null
return allChapters.size.toString()
}
return null
}
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") }
fun parseDate(dateAsString: String): Long =
dateFormatter.parse(dateAsString)?.time ?: 0
fun createMangaEntry(json: MangaDataDto, lang: String): MangaInfo {
return MangaInfo(
key = buildMangaUrl(json.id),
title = cleanString(getTitle(json.attributes.title.asMdMap(), lang, json.attributes.originalLanguage)),
cover = json.relationships
.firstOrNull { relationshipDto -> relationshipDto.type == MdConstants.Types.coverArt }
?.attributes
?.fileName
?.let { coverFileName ->
cdnCoverUrl(json.id, coverFileName)
}.orEmpty()
)
}
fun getTitle(titleMap: Map<String, String?>, currentLang: String, originalLanguage: String): String {
return titleMap[currentLang] ?: titleMap["en"] ?: titleMap[originalLanguage].let {
if (it == null && originalLanguage == "ja") {
titleMap["jp"]
} else {
it
}.orEmpty()
}
}
fun cdnCoverUrl(dexId: String, fileName: String): String {
return "$cdnUrl/covers/$dexId/$fileName"
}
fun getLoginBody(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList)
.get()
.nullIfBlank()
?.let {
try {
jsonParser.decodeFromString<LoginBodyTokenDto>(it)
} catch (e: SerializationException) {
xLogD("Unable to load login body")
null
}
}
fun sessionToken(preferences: PreferencesHelper, mdList: MdList) = getLoginBody(preferences, mdList)?.session
fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = getLoginBody(preferences, mdList)?.refresh
fun updateLoginToken(token: LoginBodyTokenDto, preferences: PreferencesHelper, mdList: MdList) {
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
}
fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) =
headers.newBuilder().add(
"Authorization",
"Bearer " + (sessionToken(preferences, mdList) ?: throw NoSessionException())
).build()
fun getEnabledMangaDex(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): MangaDex? {
return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs ->
preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()
?.let { preferredMangaDexId ->
mangadexs.firstOrNull { it.id == preferredMangaDexId }
}
?: mangadexs.firstOrNull()
}
}
fun getEnabledMangaDexs(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
val languages = preferences.enabledLanguages().get()
val disabledSourceIds = preferences.disabledSources().get()
return sourceManager.getVisibleOnlineSources()
.asSequence()
.mapNotNull { it.getMainSource<MangaDex>() }
.filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds }
.toList()
}
inline fun <reified T> encodeToBody(body: T): RequestBody {
return jsonParser.encodeToString(body)
.toRequestBody("application/json".toMediaType())
}
}
}