|
|
|
@@ -2,36 +2,26 @@ package exh.md.utils
|
|
|
|
|
|
|
|
|
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
|
|
|
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
|
|
|
|
import eu.kanade.tachiyomi.network.GET
|
|
|
|
|
import eu.kanade.tachiyomi.network.await
|
|
|
|
|
import eu.kanade.tachiyomi.network.parseAs
|
|
|
|
|
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.log.xLogE
|
|
|
|
|
import exh.md.handlers.serializers.AtHomeResponse
|
|
|
|
|
import exh.md.handlers.serializers.CoverListResponse
|
|
|
|
|
import exh.md.handlers.serializers.CoverResponse
|
|
|
|
|
import exh.md.handlers.serializers.ListCallResponse
|
|
|
|
|
import exh.md.handlers.serializers.LoginBodyToken
|
|
|
|
|
import exh.md.handlers.serializers.MangaResponse
|
|
|
|
|
import exh.md.dto.LoginBodyTokenDto
|
|
|
|
|
import exh.md.dto.MangaDto
|
|
|
|
|
import exh.md.network.NoSessionException
|
|
|
|
|
import exh.source.getMainSource
|
|
|
|
|
import exh.util.floor
|
|
|
|
|
import exh.util.nullIfBlank
|
|
|
|
|
import exh.util.nullIfZero
|
|
|
|
|
import exh.util.under
|
|
|
|
|
import kotlinx.serialization.SerializationException
|
|
|
|
|
import kotlinx.serialization.decodeFromString
|
|
|
|
|
import kotlinx.serialization.encodeToString
|
|
|
|
|
import kotlinx.serialization.json.Json
|
|
|
|
|
import okhttp3.CacheControl
|
|
|
|
|
import okhttp3.Headers
|
|
|
|
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
|
|
|
import okhttp3.OkHttpClient
|
|
|
|
|
import okhttp3.Request
|
|
|
|
|
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
|
|
|
|
@@ -45,35 +35,7 @@ class MdUtil {
|
|
|
|
|
companion object {
|
|
|
|
|
const val cdnUrl = "https://uploads.mangadex.org"
|
|
|
|
|
const val baseUrl = "https://mangadex.org"
|
|
|
|
|
const val apiUrl = "https://api.mangadex.org"
|
|
|
|
|
const val imageUrlCacheNotFound = "https://cdn.statically.io/img/raw.githubusercontent.com/CarlosEsco/Neko/master/.github/manga_cover_not_found.png"
|
|
|
|
|
const val atHomeUrl = "$apiUrl/at-home/server"
|
|
|
|
|
const val coverUrl = "$apiUrl/cover"
|
|
|
|
|
const val chapterUrl = "$apiUrl/chapter/"
|
|
|
|
|
const val chapterSuffix = "/chapter/"
|
|
|
|
|
const val checkTokenUrl = "$apiUrl/auth/check"
|
|
|
|
|
const val refreshTokenUrl = "$apiUrl/auth/refresh"
|
|
|
|
|
const val loginUrl = "$apiUrl/auth/login"
|
|
|
|
|
const val logoutUrl = "$apiUrl/auth/logout"
|
|
|
|
|
const val groupUrl = "$apiUrl/group"
|
|
|
|
|
const val authorUrl = "$apiUrl/author"
|
|
|
|
|
const val randomMangaUrl = "$apiUrl/manga/random"
|
|
|
|
|
const val mangaUrl = "$apiUrl/manga"
|
|
|
|
|
const val mangaStatus = "$apiUrl/manga/status"
|
|
|
|
|
const val userFollows = "$apiUrl/user/follows/manga"
|
|
|
|
|
fun updateReadingStatusUrl(id: String) = "$apiUrl/manga/$id/status"
|
|
|
|
|
|
|
|
|
|
fun mangaFeedUrl(id: String, offset: Int, language: String): String {
|
|
|
|
|
return "$mangaUrl/$id/feed".toHttpUrl().newBuilder().apply {
|
|
|
|
|
addQueryParameter("limit", "500")
|
|
|
|
|
addQueryParameter("offset", offset.toString())
|
|
|
|
|
addQueryParameter("translatedLanguage[]", language)
|
|
|
|
|
addQueryParameter("order[volume]", "desc")
|
|
|
|
|
addQueryParameter("order[chapter]", "desc")
|
|
|
|
|
}.build().toString()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun coverUrl(mangaId: String, coverId: String) = "$apiUrl/cover?manga[]=$mangaId&ids[]=$coverId"
|
|
|
|
|
|
|
|
|
|
const val similarCacheMapping = "https://api.similarmanga.com/mapping/mdex2search.csv"
|
|
|
|
|
const val similarCacheMangas = "https://api.similarmanga.com/manga/"
|
|
|
|
@@ -83,7 +45,7 @@ class MdUtil {
|
|
|
|
|
const val reportUrl = "https://api.mangadex.network/report"
|
|
|
|
|
|
|
|
|
|
const val mdAtHomeTokenLifespan = 10 * 60 * 1000
|
|
|
|
|
const val mangaLimit = 25
|
|
|
|
|
const val mangaLimit = 20
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the manga offset pages are 1 based, so subtract 1
|
|
|
|
@@ -217,10 +179,6 @@ class MdUtil {
|
|
|
|
|
return "/manga/$mangaUuid"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun formThumbUrl(mangaUrl: String): String {
|
|
|
|
|
return "https://coverapi.orell.dev/api/v1/mdaltimage/manga/${getMangaId(mangaUrl)}/cover"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get the ID from the manga url
|
|
|
|
|
fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/")
|
|
|
|
|
|
|
|
|
@@ -298,76 +256,33 @@ class MdUtil {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun atHomeUrlHostUrl(requestUrl: String, client: OkHttpClient, headers: Headers, cacheControl: CacheControl): String {
|
|
|
|
|
val atHomeRequest = GET(requestUrl, headers, cache = cacheControl)
|
|
|
|
|
val atHomeResponse = client.newCall(atHomeRequest).execute()
|
|
|
|
|
return jsonParser.decodeFromString<AtHomeResponse>(atHomeResponse.body!!.string()).baseUrl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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: MangaResponse, lang: String, coverUrl: String?): MangaInfo {
|
|
|
|
|
fun createMangaEntry(json: MangaDto, lang: String): MangaInfo {
|
|
|
|
|
return MangaInfo(
|
|
|
|
|
key = buildMangaUrl(json.data.id),
|
|
|
|
|
title = cleanString(json.data.attributes.title[lang] ?: json.data.attributes.title["en"]!!),
|
|
|
|
|
cover = coverUrl.orEmpty()
|
|
|
|
|
cover = json.relationships
|
|
|
|
|
.firstOrNull { relationshipDto -> relationshipDto.type == MdConstants.Types.coverArt }
|
|
|
|
|
?.attributes
|
|
|
|
|
?.fileName
|
|
|
|
|
?.let { coverFileName ->
|
|
|
|
|
cdnCoverUrl(json.data.id, coverFileName)
|
|
|
|
|
}.orEmpty()
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun getCoverUrl(dexId: String, coverId: String?, client: OkHttpClient): String {
|
|
|
|
|
coverId ?: return ""
|
|
|
|
|
val coverResponse = client.newCall(GET("$coverUrl/$coverId"))
|
|
|
|
|
.await().parseAs<CoverResponse>()
|
|
|
|
|
val fileName = coverResponse.data.attributes.fileName
|
|
|
|
|
fun cdnCoverUrl(dexId: String, fileName: String): String {
|
|
|
|
|
return "$cdnUrl/covers/$dexId/$fileName"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun getCoversFromMangaList(mangaResponseList: List<MangaResponse>, client: OkHttpClient): Map<String, String> {
|
|
|
|
|
val idsAndCoverIds = mangaResponseList.mapNotNull { mangaResponse ->
|
|
|
|
|
val mangaId = mangaResponse.data.id
|
|
|
|
|
val coverId = mangaResponse.relationships.firstOrNull { relationship ->
|
|
|
|
|
relationship.type.equals("cover_art", true)
|
|
|
|
|
}?.id
|
|
|
|
|
if (coverId == null) {
|
|
|
|
|
null
|
|
|
|
|
} else {
|
|
|
|
|
Pair(mangaId, coverId)
|
|
|
|
|
}
|
|
|
|
|
}.toMap()
|
|
|
|
|
|
|
|
|
|
return runCatching {
|
|
|
|
|
getBatchCoverUrls(idsAndCoverIds, client)
|
|
|
|
|
}.getOrNull()!!
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun getBatchCoverUrls(ids: Map<String, String>, client: OkHttpClient): Map<String, String> {
|
|
|
|
|
try {
|
|
|
|
|
val url = coverUrl.toHttpUrl().newBuilder().apply {
|
|
|
|
|
ids.values.forEach { coverArtId ->
|
|
|
|
|
addQueryParameter("ids[]", coverArtId)
|
|
|
|
|
}
|
|
|
|
|
addQueryParameter("limit", ids.size.toString())
|
|
|
|
|
}.build().toString()
|
|
|
|
|
val coverList = client.newCall(GET(url)).await().parseAs<CoverListResponse>(jsonParser)
|
|
|
|
|
return coverList.results.map { coverResponse ->
|
|
|
|
|
val fileName = coverResponse.data.attributes.fileName
|
|
|
|
|
val mangaId = coverResponse.relationships.first { it.type.equals("manga", true) }.id
|
|
|
|
|
val thumbnailUrl = "$cdnUrl/covers/$mangaId/$fileName"
|
|
|
|
|
Pair(mangaId, thumbnailUrl)
|
|
|
|
|
}.toMap()
|
|
|
|
|
} catch (e: Exception) {
|
|
|
|
|
xLogE("Error getting covers", e)
|
|
|
|
|
throw e
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getLoginBody(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
|
|
|
|
|
try {
|
|
|
|
|
jsonParser.decodeFromString<LoginBodyToken>(it)
|
|
|
|
|
jsonParser.decodeFromString<LoginBodyTokenDto>(it)
|
|
|
|
|
} catch (e: SerializationException) {
|
|
|
|
|
xLogD("Unable to load login body")
|
|
|
|
|
null
|
|
|
|
@@ -378,8 +293,10 @@ class MdUtil {
|
|
|
|
|
|
|
|
|
|
fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = getLoginBody(preferences, mdList)?.refresh
|
|
|
|
|
|
|
|
|
|
fun updateLoginToken(token: LoginBodyToken, preferences: PreferencesHelper, mdList: MdList) {
|
|
|
|
|
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
|
|
|
|
|
fun updateLoginToken(token: LoginBodyTokenDto?, preferences: PreferencesHelper, mdList: MdList) {
|
|
|
|
|
if (token != null) {
|
|
|
|
|
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
|
|
|
|
|
} else preferences.trackToken(mdList).delete()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) =
|
|
|
|
@@ -400,27 +317,17 @@ class MdUtil {
|
|
|
|
|
val disabledSourceIds = preferences.disabledSources().get()
|
|
|
|
|
|
|
|
|
|
return sourceManager.getVisibleOnlineSources()
|
|
|
|
|
.asSequence()
|
|
|
|
|
.map { it.getMainSource() }
|
|
|
|
|
.filterIsInstance<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())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend inline fun <reified T> OkHttpClient.mdListCall(request: (offset: Int) -> Request): List<T> {
|
|
|
|
|
val results = mutableListOf<T>()
|
|
|
|
|
var offset = 0
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
val response = newCall(request(offset)).await()
|
|
|
|
|
if (response.code == 204) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
val mangaListResponse = response.parseAs<ListCallResponse<T>>(MdUtil.jsonParser)
|
|
|
|
|
results += mangaListResponse.results
|
|
|
|
|
offset += mangaListResponse.limit
|
|
|
|
|
} while (offset under mangaListResponse.total)
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
}
|
|
|
|
|