Fix Mangadex Login, Fix Mangadex tracking, Set Mangadex track status to Reading on tracked

This commit is contained in:
Jobobby04 2021-03-10 17:19:38 -05:00
parent ccdae6bb9a
commit 5dace4fd74
6 changed files with 150 additions and 108 deletions

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo
@ -46,58 +47,60 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track {
val mdex = mdex ?: throw MangaDexNotFoundException()
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
val followStatus = FollowStatus.fromInt(track.status)
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
val followStatus = FollowStatus.fromInt(track.status)
// this updates the follow status in the metadata
// allow follow status to update
if (remoteTrack.status != followStatus.int) {
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
remoteTrack.status = followStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
}
if (track.score.toInt() > 0) {
mdex.updateRating(track)
}
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
if (followStatus != FollowStatus.UNFOLLOWED) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = FollowStatus.COMPLETED.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
}
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
val newFollowStatus = FollowStatus.READING
track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
// this updates the follow status in the metadata
// allow follow status to update
if (remoteTrack.status != followStatus.int) {
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
remoteTrack.status = followStatus.int
}
mdex.updateReadingProgress(track)
} else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0
if (track.score.toInt() > 0) {
mdex.updateRating(track)
}
// mangadex wont update chapters if manga is not follows this prevents unneeded network call
if (followStatus != FollowStatus.UNFOLLOWED) {
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = FollowStatus.COMPLETED.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), FollowStatus.COMPLETED)
}
if (followStatus == FollowStatus.PLAN_TO_READ && track.last_chapter_read > 0) {
val newFollowStatus = FollowStatus.READING
track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int
}
mdex.updateReadingProgress(track)
} else if (track.last_chapter_read != 0) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0
}
track
}
return track
}
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int
override suspend fun bind(track: Track): Track = update(refresh(track))
override suspend fun bind(track: Track): Track = update(refresh(track).also { it.status = FollowStatus.READING.int })
override suspend fun refresh(track: Track): Track {
val mdex = mdex ?: throw MangaDexNotFoundException()
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
track.copyPersonalFrom(remoteTrack)
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
track.copyPersonalFrom(remoteTrack)
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
}
track
}
return track
}
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
@ -110,16 +113,18 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
}
override suspend fun search(query: String): List<TrackSearch> {
val mdex = mdex ?: throw MangaDexNotFoundException()
return mdex.fetchSearchManga(0, query, mdex.getFilterList())
.flatMap { page ->
runAsObservable({
page.mangas.map {
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
}
})
}
.awaitSingle()
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
mdex.fetchSearchManga(0, query, mdex.getFilterList())
.flatMap { page ->
runAsObservable({
page.mangas.map {
toTrackSearch(mdex.getMangaDetails(it.toMangaInfo()))
}
})
}
.awaitSingle()
}
}
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {

View File

@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import androidx.core.text.HtmlCompat
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -48,12 +47,17 @@ import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import exh.util.urlImportFetchSearchManga
import exh.widget.preference.MangadexLoginDialog
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Request
import okhttp3.Response
import okio.EOFException
import rx.Observable
import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo
@ -73,11 +77,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
RandomMangaSource {
override val lang: String = delegate.lang
override val headers: Headers
get() = super.headers.newBuilder().apply {
add("X-Requested-With", "XMLHttpRequest")
add("Referer", MdUtil.baseUrl)
}.build()
override val headers: Headers = super.headers.newBuilder().apply {
add("X-Requested-With", "XMLHttpRequest")
add("Referer", MdUtil.baseUrl)
}.build()
private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
@ -198,13 +201,10 @@ class MangaDex(delegate: HttpSource, val context: Context) :
add("login_password", password)
add("no_js", "1")
add("remember_me", "1")
add("two_factor", twoFactorCode)
}
twoFactorCode.let {
formBody.add("two_factor", it)
}
val response = client.newCall(
client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
@ -212,12 +212,13 @@ class MangaDex(delegate: HttpSource, val context: Context) :
)
).await()
withIOContext { response.body?.string() }.let { result ->
if (result != null && result.isEmpty()) {
true
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
withIOContext { response.body?.string() }.let { jsonData ->
if (jsonData != null) {
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
} else {
val error = result?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() }
throw Exception(error)
throw Exception("Json data was null")
}
}
}
@ -236,8 +237,14 @@ class MangaDex(delegate: HttpSource, val context: Context) :
val result = client.newCall(
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
).await()
val resultStr = withIOContext { result.body?.string() }
if (resultStr?.contains("success", true) == true) {
try {
val resultStr = withIOContext { result.body?.string() }
if (resultStr?.contains("success", true) == true) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
}
} catch (e: EOFException) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
@ -281,7 +288,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
}
override suspend fun fetchRandomMangaUrl(): String {
return MangaHandler(client, headers, mdLang).fetchRandomMangaId()
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
}
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {

View File

@ -28,6 +28,7 @@ import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.EOFException
class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
@ -75,7 +76,6 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
/**
* fetch follow status used when fetching status for 1 manga
*/
private fun followStatusParse(response: Response): Track {
val followsPageResult = try {
response.parseAs<FollowsIndividualSerializer>(MdUtil.jsonParser)
@ -84,15 +84,11 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
throw e
}
if (followsPageResult.data == null) {
throw Exception("Invalid response ${followsPageResult.code}")
}
val track = Track.create(TrackManager.MDLIST)
if (followsPageResult.code == 404) {
track.status = FollowStatus.UNFOLLOWED.int
} else {
val follow = followsPageResult.data
val follow = followsPageResult.data ?: throw Exception("Invalid response ${followsPageResult.code}")
track.status = follow.followType
if (follow.chapter.isNotBlank()) {
track.last_chapter_read = follow.chapter.toFloat().floor()
@ -153,7 +149,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
.await()
}
withIOContext { response.body?.string().isNullOrEmpty() }
response.succeeded()
}
}
@ -172,11 +168,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
)
).await()
withIOContext {
response.body?.string()
.also { xLogD(it) }
.let { it != null && it.isEmpty() }
}
response.succeeded()
}
}
@ -188,10 +180,21 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
headers
)
)
.await()
).await()
withIOContext { response.body?.string().isNullOrEmpty() }
response.succeeded()
}
}
private suspend fun Response.succeeded() = withIOContext {
try {
body?.string().let { body ->
(body != null && body.isEmpty()).also {
if (!it) xLogD(body)
}
}
} catch (e: EOFException) {
true
}
}

View File

@ -15,6 +15,7 @@ import exh.md.handlers.serializers.ApiCovers
import exh.md.handlers.serializers.ApiMangaSerializer
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import kotlinx.coroutines.async
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
@ -30,7 +31,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val lang: Str
// TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<ChapterInfo>> {
return withIOContext {
val apiNetworkManga = client.newCall(apiRequest(manga)).await().parseAs<ApiMangaSerializer>()
val apiNetworkManga = client.newCall(apiRequest(manga)).await().parseAs<ApiMangaSerializer>(MdUtil.jsonParser)
val covers = getCovers(manga, forceLatestCovers)
val parser = ApiMangaParser(lang)
@ -120,17 +121,22 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val lang: Str
}
suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean): Pair<Track, MangaDexSearchMetadata> {
val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt())
val manga = MangaInfo(mangaUrl, track.title)
val response = client.newCall(apiRequest(manga)).await()
val metadata = MangaDexSearchMetadata()
ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList())
val remoteTrack = FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url)
return remoteTrack to metadata
return withIOContext {
val metadata = async {
val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt())
val manga = MangaInfo(mangaUrl, track.title)
val response = client.newCall(apiRequest(manga)).await()
val metadata = MangaDexSearchMetadata()
ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList())
metadata
}
val remoteTrack = async { FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url) }
remoteTrack.await() to metadata.await()
}
}
private fun randomMangaRequest(): Request {
return GET(MdUtil.baseUrl + MdUtil.randMangaPage, cache = CacheControl.Builder().noCache().build())
return GET(MdUtil.baseUrl + MdUtil.randMangaPage, cache = CacheControl.FORCE_NETWORK)
}
private fun apiRequest(manga: MangaInfo): Request {

View File

@ -11,7 +11,7 @@ data class FollowsPageSerializer(
@Serializable
data class FollowsIndividualSerializer(
val code: Int,
val data: FollowPage?
val data: FollowPage? = null
)
@Serializable

View File

@ -31,6 +31,7 @@ class MdUtil {
const val apiChapterSuffix = "?mark_read=0"
const val groupSearchUrl = "$baseUrl/groups/0/1/"
const val followsAllApi = "/v2/user/me/followed-manga"
const val isLoggedInApi = "/v2/user/me"
const val followsMangaApi = "/v2/user/me/manga/"
const val apiCovers = "/covers"
const val reportUrl = "https://api.mangadex.network/report"
@ -51,10 +52,16 @@ class MdUtil {
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]",
@ -78,19 +85,23 @@ class MdUtil {
"\r\n\r\nItalian\r\n",
"Arabic /",
"Descriptions in Other Languages",
"Espa&ntilde;ol /",
"Espa&ntilde;ol:",
"Espanol",
"[Espa&ntilde;",
"Espa&ntilde;",
"Farsi/",
"Fran&ccedil;ais",
"French - ",
"Francois",
"French:",
"French/",
"French /",
"German/",
"German /",
"Hindi /",
"Bahasa Indonesia",
"Indonesia:",
"Indonesian:",
"Indonesian :",
"Indo:",
"[u]Indonesian",
"Italian / ",
@ -98,9 +109,16 @@ class MdUtil {
"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 /",
@ -108,6 +126,8 @@ class MdUtil {
"Polski",
"Portugu&ecirc;s",
"Portuguese (BR)",
"PT/BR:",
"Pt/Br:",
"Pt-Br:",
"Portuguese /",
"[right]",
@ -115,6 +135,8 @@ class MdUtil {
"R&eacute;sume Fran&ccedil;ais",
"R&Eacute;SUM&Eacute; FRANCAIS :",
"RUS:",
"Ru/Pyc",
"\\r\\nRUS\\r\\n",
"Russia/",
"Russian /",
"Spanish:",
@ -162,23 +184,22 @@ class MdUtil {
fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?")
fun cleanString(string: String): String {
var cleanedString = string
bbCodeToRemove.forEach {
cleanedString = cleanedString.replace("[$it]", "", true)
.replace("[/$it]", "", true)
}
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")
while (bbRegex.containsMatchIn(cleanedString)) {
cleanedString = cleanedString.replace(bbRegex, "$2")
}
return Parser.unescapeEntities(intermediate, false)
return Parser.unescapeEntities(cleanedString, false)
}
fun cleanDescription(string: String): String {