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 { return scanlators?.split(scanlatorSeparator)?.toSet().orEmpty() } fun getScanlatorString(scanlators: Set): String { return scanlators.sorted().joinToString(scanlatorSeparator) } fun getMissingChapterCount(chapters: List, 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, 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(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 { val languages = preferences.enabledLanguages().get() val disabledSourceIds = preferences.disabledSources().get() return sourceManager.getVisibleOnlineSources() .asSequence() .mapNotNull { it.getMainSource() } .filter { it.lang in languages } .filterNot { it.id.toString() in disabledSourceIds } .toList() } inline fun encodeToBody(body: T): RequestBody { return jsonParser.encodeToString(body) .toRequestBody("application/json".toMediaType()) } } }