290 lines
10 KiB
Kotlin
290 lines
10 KiB
Kotlin
package exh.md.utils
|
|
|
|
import androidx.core.net.toUri
|
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
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.source.getMainSource
|
|
import exh.util.floor
|
|
import exh.util.nullIfZero
|
|
import kotlinx.serialization.json.Json
|
|
import org.jsoup.parser.Parser
|
|
import uy.kohesive.injekt.Injekt
|
|
import uy.kohesive.injekt.api.get
|
|
import java.net.URISyntaxException
|
|
|
|
@Suppress("unused")
|
|
class MdUtil {
|
|
|
|
companion object {
|
|
const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org"
|
|
const val baseUrl = "https://mangadex.org"
|
|
const val randMangaPage = "/manga/"
|
|
const val apiManga = "/api/manga/"
|
|
const val apiChapter = "/api/chapter/"
|
|
const val apiChapterSuffix = "?mark_read=0"
|
|
const val groupSearchUrl = "$baseUrl/groups/0/1/"
|
|
const val followsAllApi = "/api/?type=manga_follows"
|
|
const val followsMangaApi = "/api/?type=manga_follows&manga_id="
|
|
const val coversApi = "/api/index.php?type=covers&id="
|
|
const val reportUrl = "https://api.mangadex.network/report"
|
|
const val imageUrl = "$baseUrl/data"
|
|
|
|
val jsonParser = Json {
|
|
isLenient = true
|
|
ignoreUnknownKeys = true
|
|
allowSpecialFloatingPointValues = true
|
|
useArrayPolymorphism = true
|
|
prettyPrint = true
|
|
}
|
|
|
|
private const val scanlatorSeparator = " & "
|
|
|
|
val validOneShotFinalChapters = listOf("0", "1")
|
|
|
|
val englishDescriptionTags = listOf(
|
|
"[b][u]English:",
|
|
"[b][u]English",
|
|
"[English]:",
|
|
"[B][ENG][/B]"
|
|
)
|
|
|
|
val descriptionLanguages = listOf(
|
|
"Russian / Русский",
|
|
"[u]Russian",
|
|
"[b][u]Russian",
|
|
"[RUS]",
|
|
"Russian / Русский",
|
|
"Russian/Русский:",
|
|
"Russia/Русское",
|
|
"Русский",
|
|
"RUS:",
|
|
"[b][u]German / Deutsch",
|
|
"German/Deutsch:",
|
|
"Español / Spanish",
|
|
"Spanish / Español",
|
|
"Spanish / Espa & ntilde; ol",
|
|
"Spanish / Español",
|
|
"[b][u]Spanish",
|
|
"[Español]:",
|
|
"[b] Spanish: [/ b]",
|
|
"정보",
|
|
"Spanish/Español",
|
|
"Español / Spanish",
|
|
"Italian / Italiano",
|
|
"Italian/Italiano",
|
|
"\r\n\r\nItalian\r\n",
|
|
"Pasta-Pizza-Mandolino/Italiano",
|
|
"Persian /فارسی",
|
|
"Farsi/Persian/",
|
|
"Polish / polski",
|
|
"Polish / Polski",
|
|
"Polish Summary / Polski Opis",
|
|
"Polski",
|
|
"Portuguese (BR) / Português",
|
|
"Portuguese / Português",
|
|
"Português / Portuguese",
|
|
"Portuguese / Portugu",
|
|
"Portuguese / Português",
|
|
"Português",
|
|
"Portuguese (BR) / Portugu & ecirc;",
|
|
"Portuguese (BR) / Portuguê",
|
|
"[PTBR]",
|
|
"Résume Français",
|
|
"Résumé Français",
|
|
"[b][u]French",
|
|
"French / Français",
|
|
"Français",
|
|
"[hr]Fr:",
|
|
"French - Français:",
|
|
"Turkish / Türkçe",
|
|
"Turkish/Türkçe",
|
|
"Türkçe",
|
|
"[b][u]Chinese",
|
|
"Arabic / العربية",
|
|
"العربية",
|
|
"[hr]TH",
|
|
"[b][u]Vietnamese",
|
|
"[b]Links:",
|
|
"[b]Link[/b]",
|
|
"Links:",
|
|
"[b]External Links"
|
|
)
|
|
|
|
// guess the thumbnail url is .jpg this has a ~80% success rate
|
|
fun formThumbUrl(mangaUrl: String, lowQuality: Boolean): String {
|
|
var ext = ".jpg"
|
|
if (lowQuality) {
|
|
ext = ".thumb$ext"
|
|
}
|
|
return cdnUrl + "/images/manga/" + getMangaId(mangaUrl) + ext
|
|
}
|
|
|
|
// Get the ID from the manga url
|
|
fun getMangaId(url: String): String {
|
|
val lastSection = url.trimEnd('/').substringAfterLast("/")
|
|
return if (lastSection.toIntOrNull() != null) {
|
|
lastSection
|
|
} else {
|
|
// this occurs if person has manga from before that had the id/name/
|
|
url.trimEnd('/').substringBeforeLast("/").substringAfterLast("/")
|
|
}
|
|
}
|
|
|
|
fun getChapterId(url: String) = url.substringBeforeLast(apiChapterSuffix).substringAfterLast("/")
|
|
|
|
// creates the manga url from the browse for the api
|
|
fun modifyMangaUrl(url: String): String =
|
|
url.replace("/title/", "/manga/").substringBeforeLast("/") + "/"
|
|
|
|
// Removes the ?timestamp from image urls
|
|
fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?")
|
|
|
|
fun cleanString(string: String): String {
|
|
val bbRegex =
|
|
"""\[(\w+)[^]]*](.*?)\[/\1]""".toRegex()
|
|
var intermediate = string
|
|
.replace("[list]", "", true)
|
|
.replace("[/list]", "", true)
|
|
.replace("[*]", "")
|
|
.replace("[hr]", "", true)
|
|
.replace("[u]", "", true)
|
|
.replace("[/u]", "", true)
|
|
.replace("[b]", "", true)
|
|
.replace("[/b]", "", true)
|
|
|
|
// Recursively remove nested bbcode
|
|
while (bbRegex.containsMatchIn(intermediate)) {
|
|
intermediate = intermediate.replace(bbRegex, "$2")
|
|
}
|
|
return Parser.unescapeEntities(intermediate, 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): List<String> {
|
|
if (scanlators.isBlank()) return emptyList()
|
|
return scanlators.split(scanlatorSeparator).distinct()
|
|
}
|
|
|
|
fun getScanlatorString(scanlators: Set<String>): String {
|
|
return scanlators.toList().sorted().joinToString(scanlatorSeparator)
|
|
}
|
|
|
|
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
|
|
if (mangaStatus == SManga.COMPLETED) return null
|
|
|
|
// TODO
|
|
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.size <= 0) return null
|
|
return allChapters.size.toString()
|
|
}
|
|
return null
|
|
}
|
|
|
|
fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), 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 = Injekt.get(), sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
|
|
val languages = preferences.enabledLanguages().get()
|
|
val disabledSourceIds = preferences.disabledSources().get()
|
|
|
|
return sourceManager.getVisibleOnlineSources()
|
|
.map { it.getMainSource() }
|
|
.filterIsInstance<MangaDex>()
|
|
.filter { it.lang in languages }
|
|
.filterNot { it.id.toString() in disabledSourceIds }
|
|
}
|
|
|
|
fun mapMdIdToMangaUrl(id: Int) = "/manga/$id/"
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
|
* database and the urls could still work after a domain change.
|
|
*
|
|
* @param url the full url to the chapter.
|
|
*/
|
|
fun SChapter.setMDUrlWithoutDomain(url: String) {
|
|
this.url = getMDUrlWithoutDomain(url)
|
|
}
|
|
|
|
/**
|
|
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
|
* database and the urls could still work after a domain change.
|
|
*
|
|
* @param url the full url to the manga.
|
|
*/
|
|
fun SManga.setMDUrlWithoutDomain(url: String) {
|
|
this.url = getMDUrlWithoutDomain(url)
|
|
}
|
|
|
|
/**
|
|
* Returns the url of the given string without the scheme and domain.
|
|
*
|
|
* @param orig the full url.
|
|
*/
|
|
private fun getMDUrlWithoutDomain(orig: String): String {
|
|
return try {
|
|
val uri = orig.toUri()
|
|
var out = uri.path.orEmpty()
|
|
if (uri.query != null) {
|
|
out += "?" + uri.query
|
|
}
|
|
if (uri.fragment != null) {
|
|
out += "#" + uri.fragment
|
|
}
|
|
out
|
|
} catch (e: URISyntaxException) {
|
|
orig
|
|
}
|
|
}
|
|
|
|
fun Chapter.scanlatorList(): List<String> {
|
|
return this.scanlator?.let {
|
|
MdUtil.getScanlators(it)
|
|
} ?: listOf("No scanlator")
|
|
}
|