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.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@ -46,6 +47,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun add(track: Track): Track = update(track) override suspend fun add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException() val mdex = mdex ?: throw MangaDexNotFoundException()
val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url) val remoteTrack = mdex.fetchTrackingInfo(track.tracking_url)
@ -56,7 +58,6 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
if (remoteTrack.status != followStatus.int) { if (remoteTrack.status != followStatus.int) {
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus) mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), followStatus)
remoteTrack.status = followStatus.int remoteTrack.status = followStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
} }
if (track.score.toInt() > 0) { if (track.score.toInt() > 0) {
@ -75,7 +76,6 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
track.status = FollowStatus.READING.int track.status = FollowStatus.READING.int
mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus) mdex.updateFollowStatus(MdUtil.getMangaId(track.tracking_url), newFollowStatus)
remoteTrack.status = newFollowStatus.int remoteTrack.status = newFollowStatus.int
// db.insertFlatMetadataAsync(mangaMetadata.flatten()).await()
} }
mdex.updateReadingProgress(track) mdex.updateReadingProgress(track)
@ -83,21 +83,24 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
// When followStatus has been changed to unfollowed 0 out read chapters since dex does // When followStatus has been changed to unfollowed 0 out read chapters since dex does
track.last_chapter_read = 0 track.last_chapter_read = 0
} }
return track track
}
} }
override fun getCompletionStatus(): Int = FollowStatus.COMPLETED.int 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 { override suspend fun refresh(track: Track): Track {
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException() val mdex = mdex ?: throw MangaDexNotFoundException()
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track) val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack)
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) { if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0 track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
} }
return track track
}
} }
fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track { fun createInitialTracker(dbManga: Manga, mdManga: Manga = dbManga): Track {
@ -110,8 +113,9 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
} }
override suspend fun search(query: String): List<TrackSearch> { override suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException() val mdex = mdex ?: throw MangaDexNotFoundException()
return mdex.fetchSearchManga(0, query, mdex.getFilterList()) mdex.fetchSearchManga(0, query, mdex.getFilterList())
.flatMap { page -> .flatMap { page ->
runAsObservable({ runAsObservable({
page.mangas.map { page.mangas.map {
@ -121,6 +125,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
} }
.awaitSingle() .awaitSingle()
} }
}
private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply { private fun toTrackSearch(mangaInfo: MangaInfo): TrackSearch = TrackSearch.create(TrackManager.MDLIST).apply {
tracking_url = MdUtil.baseUrl + mangaInfo.key tracking_url = MdUtil.baseUrl + mangaInfo.key

View File

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

View File

@ -28,6 +28,7 @@ import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okio.EOFException
class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) { 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 * fetch follow status used when fetching status for 1 manga
*/ */
private fun followStatusParse(response: Response): Track { private fun followStatusParse(response: Response): Track {
val followsPageResult = try { val followsPageResult = try {
response.parseAs<FollowsIndividualSerializer>(MdUtil.jsonParser) response.parseAs<FollowsIndividualSerializer>(MdUtil.jsonParser)
@ -84,15 +84,11 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
throw e throw e
} }
if (followsPageResult.data == null) {
throw Exception("Invalid response ${followsPageResult.code}")
}
val track = Track.create(TrackManager.MDLIST) val track = Track.create(TrackManager.MDLIST)
if (followsPageResult.code == 404) { if (followsPageResult.code == 404) {
track.status = FollowStatus.UNFOLLOWED.int track.status = FollowStatus.UNFOLLOWED.int
} else { } else {
val follow = followsPageResult.data val follow = followsPageResult.data ?: throw Exception("Invalid response ${followsPageResult.code}")
track.status = follow.followType track.status = follow.followType
if (follow.chapter.isNotBlank()) { if (follow.chapter.isNotBlank()) {
track.last_chapter_read = follow.chapter.toFloat().floor() track.last_chapter_read = follow.chapter.toFloat().floor()
@ -153,7 +149,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
.await() .await()
} }
withIOContext { response.body?.string().isNullOrEmpty() } response.succeeded()
} }
} }
@ -172,11 +168,7 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
) )
).await() ).await()
withIOContext { response.succeeded()
response.body?.string()
.also { xLogD(it) }
.let { it != null && it.isEmpty() }
}
} }
} }
@ -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()}", "${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
headers 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.handlers.serializers.ApiMangaSerializer
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import kotlinx.coroutines.async
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -30,7 +31,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val lang: Str
// TODO make use of this // TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<ChapterInfo>> { suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<ChapterInfo>> {
return withIOContext { 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 covers = getCovers(manga, forceLatestCovers)
val parser = ApiMangaParser(lang) 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> { suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean): Pair<Track, MangaDexSearchMetadata> {
return withIOContext {
val metadata = async {
val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt()) val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt())
val manga = MangaInfo(mangaUrl, track.title) val manga = MangaInfo(mangaUrl, track.title)
val response = client.newCall(apiRequest(manga)).await() val response = client.newCall(apiRequest(manga)).await()
val metadata = MangaDexSearchMetadata() val metadata = MangaDexSearchMetadata()
ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList()) ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList())
val remoteTrack = FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url) metadata
return remoteTrack to metadata }
val remoteTrack = async { FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url) }
remoteTrack.await() to metadata.await()
}
} }
private fun randomMangaRequest(): Request { 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 { private fun apiRequest(manga: MangaInfo): Request {

View File

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

View File

@ -31,6 +31,7 @@ class MdUtil {
const val apiChapterSuffix = "?mark_read=0" const val apiChapterSuffix = "?mark_read=0"
const val groupSearchUrl = "$baseUrl/groups/0/1/" const val groupSearchUrl = "$baseUrl/groups/0/1/"
const val followsAllApi = "/v2/user/me/followed-manga" const val followsAllApi = "/v2/user/me/followed-manga"
const val isLoggedInApi = "/v2/user/me"
const val followsMangaApi = "/v2/user/me/manga/" const val followsMangaApi = "/v2/user/me/manga/"
const val apiCovers = "/covers" const val apiCovers = "/covers"
const val reportUrl = "https://api.mangadex.network/report" const val reportUrl = "https://api.mangadex.network/report"
@ -51,10 +52,16 @@ class MdUtil {
val englishDescriptionTags = listOf( val englishDescriptionTags = listOf(
"[b][u]English:", "[b][u]English:",
"[b][u]English", "[b][u]English",
"English:",
"English :",
"[English]:", "[English]:",
"English Translaton:",
"[B][ENG][/B]" "[B][ENG][/B]"
) )
val bbCodeToRemove = listOf(
"list", "*", "hr", "u", "b", "i", "s", "center", "spoiler="
)
val descriptionLanguages = listOf( val descriptionLanguages = listOf(
"=FRANCAIS=", "=FRANCAIS=",
"[b] Spanish: [/ b]", "[b] Spanish: [/ b]",
@ -78,19 +85,23 @@ class MdUtil {
"\r\n\r\nItalian\r\n", "\r\n\r\nItalian\r\n",
"Arabic /", "Arabic /",
"Descriptions in Other Languages", "Descriptions in Other Languages",
"Espa&ntilde;ol /", "Espanol",
"Espa&ntilde;ol:", "[Espa&ntilde;",
"Espa&ntilde;",
"Farsi/", "Farsi/",
"Fran&ccedil;ais", "Fran&ccedil;ais",
"French - ", "French - ",
"Francois", "Francois",
"French:", "French:",
"French/",
"French /", "French /",
"German/", "German/",
"German /", "German /",
"Hindi /", "Hindi /",
"Bahasa Indonesia",
"Indonesia:", "Indonesia:",
"Indonesian:", "Indonesian:",
"Indonesian :",
"Indo:", "Indo:",
"[u]Indonesian", "[u]Indonesian",
"Italian / ", "Italian / ",
@ -98,9 +109,16 @@ class MdUtil {
"Italian/", "Italian/",
"Italiano", "Italiano",
"Italian:", "Italian:",
"Italian summary:",
"Japanese /", "Japanese /",
"Original Japanese",
"Official Japanese Translation",
"Official Chinese Translation",
"Official French Translation",
"Official Indonesian Translation",
"Links:", "Links:",
"Pasta-Pizza-Mandolino/Italiano", "Pasta-Pizza-Mandolino/Italiano",
"Persian/فارسی",
"Persian /فارسی", "Persian /فارسی",
"Polish /", "Polish /",
"Polish Summary /", "Polish Summary /",
@ -108,6 +126,8 @@ class MdUtil {
"Polski", "Polski",
"Portugu&ecirc;s", "Portugu&ecirc;s",
"Portuguese (BR)", "Portuguese (BR)",
"PT/BR:",
"Pt/Br:",
"Pt-Br:", "Pt-Br:",
"Portuguese /", "Portuguese /",
"[right]", "[right]",
@ -115,6 +135,8 @@ class MdUtil {
"R&eacute;sume Fran&ccedil;ais", "R&eacute;sume Fran&ccedil;ais",
"R&Eacute;SUM&Eacute; FRANCAIS :", "R&Eacute;SUM&Eacute; FRANCAIS :",
"RUS:", "RUS:",
"Ru/Pyc",
"\\r\\nRUS\\r\\n",
"Russia/", "Russia/",
"Russian /", "Russian /",
"Spanish:", "Spanish:",
@ -162,23 +184,22 @@ class MdUtil {
fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?") fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?")
fun cleanString(string: String): String { fun cleanString(string: String): String {
var cleanedString = string
bbCodeToRemove.forEach {
cleanedString = cleanedString.replace("[$it]", "", true)
.replace("[/$it]", "", true)
}
val bbRegex = val bbRegex =
"""\[(\w+)[^]]*](.*?)\[/\1]""".toRegex() """\[(\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 // Recursively remove nested bbcode
while (bbRegex.containsMatchIn(intermediate)) { while (bbRegex.containsMatchIn(cleanedString)) {
intermediate = intermediate.replace(bbRegex, "$2") cleanedString = cleanedString.replace(bbRegex, "$2")
} }
return Parser.unescapeEntities(intermediate, false)
return Parser.unescapeEntities(cleanedString, false)
} }
fun cleanDescription(string: String): String { fun cleanDescription(string: String): String {