Update Mangadex

This commit is contained in:
Jobobby04 2021-07-05 18:31:30 -04:00
parent efba76380a
commit 20d8cf6c10
36 changed files with 943 additions and 1200 deletions

View File

@ -21,8 +21,9 @@ import eu.kanade.tachiyomi.source.online.NamespaceSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.runAsObservable
import exh.md.MangaDexFabHeaderAdapter import exh.md.MangaDexFabHeaderAdapter
import exh.md.handlers.ApiChapterParser import exh.md.dto.MangaDto
import exh.md.handlers.ApiMangaParser import exh.md.handlers.ApiMangaParser
import exh.md.handlers.FollowsHandler import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler import exh.md.handlers.MangaHandler
@ -32,13 +33,14 @@ import exh.md.handlers.SimilarHandler
import exh.md.network.MangaDexLoginHelper import exh.md.network.MangaDexLoginHelper
import exh.md.network.NoSessionException import exh.md.network.NoSessionException
import exh.md.network.TokenAuthenticator import exh.md.network.TokenAuthenticator
import exh.md.service.MangaDexAuthService
import exh.md.service.MangaDexService
import exh.md.service.SimilarService
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdLang import exh.md.utils.MdLang
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@ -51,7 +53,7 @@ import kotlin.reflect.KClass
@Suppress("OverridingDeprecatedMember") @Suppress("OverridingDeprecatedMember")
class MangaDex(delegate: HttpSource, val context: Context) : class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
MetadataSource<MangaDexSearchMetadata, Response>, MetadataSource<MangaDexSearchMetadata, MangaDto>,
// UrlImportableSource, // UrlImportableSource,
FollowsSource, FollowsSource,
LoginSource, LoginSource,
@ -73,7 +75,9 @@ class MangaDex(delegate: HttpSource, val context: Context) :
context.getSharedPreferences("source_$id", 0x0000) context.getSharedPreferences("source_$id", 0x0000)
} }
private val loginHelper = MangaDexLoginHelper(networkHttpClient, preferences, mdList) val mangadexAuthServiceLazy = lazy { MangaDexAuthService(baseHttpClient, headers, preferences, mdList) }
private val loginHelper = MangaDexLoginHelper(mangadexAuthServiceLazy, preferences, mdList)
override val baseHttpClient: OkHttpClient = super.client.newBuilder() override val baseHttpClient: OkHttpClient = super.client.newBuilder()
.authenticator( .authenticator(
@ -84,26 +88,30 @@ class MangaDex(delegate: HttpSource, val context: Context) :
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false) private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false) private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
private val apiMangaParser by lazy { private val mangadexService by lazy {
ApiMangaParser(baseHttpClient, mdLang.lang) MangaDexService(client)
} }
private val apiChapterParser by lazy { private val mangadexAuthService by mangadexAuthServiceLazy
ApiChapterParser() private val similarService by lazy {
SimilarService(client)
}
private val apiMangaParser by lazy {
ApiMangaParser(mdLang.lang)
} }
private val followsHandler by lazy { private val followsHandler by lazy {
FollowsHandler(baseHttpClient, headers, preferences, mdLang.lang, mdList) FollowsHandler(mdLang.lang, mangadexAuthService)
} }
private val mangaHandler by lazy { private val mangaHandler by lazy {
MangaHandler(baseHttpClient, headers, mdLang.lang, apiMangaParser, followsHandler) MangaHandler(mdLang.lang, mangadexService, apiMangaParser, followsHandler)
} }
private val similarHandler by lazy { private val similarHandler by lazy {
SimilarHandler(baseHttpClient, mdLang.lang) SimilarHandler(mdLang.lang, mangadexService, similarService)
} }
private val mangaPlusHandler by lazy { private val mangaPlusHandler by lazy {
MangaPlusHandler(network.client) MangaPlusHandler(network.client)
} }
private val pageHandler by lazy { private val pageHandler by lazy {
PageHandler(network.client, headers, apiChapterParser, mangaPlusHandler, preferences, mdList) PageHandler(headers, mangadexService, mangaPlusHandler, preferences, mdList)
} }
/*override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> = /*override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
@ -152,7 +160,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return pageHandler.fetchPageList(chapter, isLogged(), usePort443Only(), dataSaver()) return runAsObservable({ pageHandler.fetchPageList(chapter, isLogged(), usePort443Only(), dataSaver()) })
} }
override fun fetchImage(page: Page): Observable<Response> { override fun fetchImage(page: Page): Observable<Response> {
@ -168,7 +176,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
return MangaDexDescriptionAdapter(controller) return MangaDexDescriptionAdapter(controller)
} }
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: MangaDto) {
apiMangaParser.parseIntoMetadata(metadata, input) apiMangaParser.parseIntoMetadata(metadata, input)
} }
@ -198,8 +206,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
twoFactorCode: String? twoFactorCode: String?
): Boolean { ): Boolean {
val result = loginHelper.login(username, password) val result = loginHelper.login(username, password)
return if (result is MangaDexLoginHelper.LoginResult.Success) { return if (result) {
MdUtil.updateLoginToken(result.token, preferences, mdList)
mdList.saveCredentials(username, password) mdList.saveCredentials(username, password)
true true
} else false } else false
@ -207,7 +214,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override suspend fun logout(): Boolean { override suspend fun logout(): Boolean {
val result = try { val result = try {
loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList)) loginHelper.logout()
} catch (e: NoSessionException) { } catch (e: NoSessionException) {
true true
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -0,0 +1,17 @@
package exh.md.dto
import kotlinx.serialization.Serializable
@Serializable
data class AtHomeDto(
val baseUrl: String,
)
@Serializable
data class AtHomeImageReportDto(
val url: String,
val success: Boolean,
val bytes: Int? = null,
val cached: Boolean? = null,
val duration: Long,
)

View File

@ -0,0 +1,39 @@
package exh.md.dto
import kotlinx.serialization.Serializable
/**
* Login Request object for Dex Api
*/
@Serializable
data class LoginRequestDto(val username: String, val password: String)
/**
* Response after login
*/
@Serializable
data class LoginResponseDto(val result: String, val token: LoginBodyTokenDto)
/**
* Tokens for the logins
*/
@Serializable
data class LoginBodyTokenDto(val session: String, val refresh: String)
/**
* Response after logout
*/
@Serializable
data class LogoutDto(val result: String)
/**
* Check if session token is valid
*/
@Serializable
data class CheckTokenDto(val isAuthenticated: Boolean)
/**
* Request to refresh token
*/
@Serializable
data class RefreshTokenDto(val token: String)

View File

@ -0,0 +1,62 @@
package exh.md.dto
import kotlinx.serialization.Serializable
@Serializable
data class ChapterListDto(
override val limit: Int,
override val offset: Int,
override val total: Int,
override val results: List<ChapterDto>,
) : ListCallDto<ChapterDto>
@Serializable
data class ChapterDto(
val result: String,
val data: ChapterDataDto,
val relationships: List<RelationshipDto>,
)
@Serializable
data class ChapterDataDto(
val id: String,
val type: String,
val attributes: ChapterAttributesDto,
)
@Serializable
data class ChapterAttributesDto(
val title: String?,
val volume: String?,
val chapter: String?,
val translatedLanguage: String,
val publishAt: String,
val data: List<String>,
val dataSaver: List<String>,
val hash: String,
)
@Serializable
data class GroupListDto(
val limit: Int,
val offset: Int,
val total: Int,
val results: List<GroupDto>,
)
@Serializable
data class GroupDto(
val result: String,
val data: GroupDataDto,
)
@Serializable
data class GroupDataDto(
val id: String,
val attributes: GroupAttributesDto,
)
@Serializable
data class GroupAttributesDto(
val name: String,
)

View File

@ -0,0 +1,8 @@
package exh.md.dto
interface ListCallDto<T> {
val limit: Int
val offset: Int
val total: Int
val results: List<T>
}

View File

@ -0,0 +1,119 @@
package exh.md.dto
import kotlinx.serialization.Serializable
@Serializable
data class MangaListDto(
override val limit: Int,
override val offset: Int,
override val total: Int,
override val results: List<MangaDto>,
) : ListCallDto<MangaDto>
@Serializable
data class MangaDto(
val result: String,
val data: MangaDataDto,
val relationships: List<RelationshipDto>,
)
@Serializable
data class MangaDataDto(val id: String, val type: String, val attributes: MangaAttributesDto)
@Serializable
data class MangaAttributesDto(
val title: Map<String, String>,
val altTitles: List<Map<String, String>>,
val description: Map<String, String>,
val links: Map<String, String>?,
val originalLanguage: String,
val lastVolume: String?,
val lastChapter: String?,
val contentRating: String?,
val publicationDemographic: String?,
val status: String?,
val year: Int?,
val tags: List<TagDto>,
)
@Serializable
data class TagDto(
val id: String,
val attributes: TagAttributesDto
)
@Serializable
data class TagAttributesDto(
val name: Map<String, String>
)
@Serializable
data class RelationshipDto(
val id: String,
val type: String,
val attributes: IncludesAttributesDto? = null,
)
@Serializable
data class IncludesAttributesDto(
val name: String? = null,
val fileName: String? = null,
)
@Serializable
data class AuthorListDto(
val results: List<AuthorDto>,
)
@Serializable
data class AuthorDto(
val result: String,
val data: AuthorDataDto,
)
@Serializable
data class AuthorDataDto(
val id: String,
val attributes: AuthorAttributesDto,
)
@Serializable
data class AuthorAttributesDto(
val name: String,
)
@Serializable
data class ReadingStatusDto(
val status: String?,
)
@Serializable
data class ReadingStatusMapDto(
val statuses: Map<String, String?>,
)
@Serializable
data class ReadChapterDto(
val data: List<String>,
)
@Serializable
data class CoverListDto(
val results: List<CoverDto>,
)
@Serializable
data class CoverDto(
val data: CoverDataDto,
val relationships: List<RelationshipDto>,
)
@Serializable
data class CoverDataDto(
val attributes: CoverAttributesDto,
)
@Serializable
data class CoverAttributesDto(
val fileName: String,
)

View File

@ -1,4 +1,4 @@
package exh.md.handlers.serializers package exh.md.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer import kotlinx.serialization.Serializer

View File

@ -0,0 +1,8 @@
package exh.md.dto
import kotlinx.serialization.Serializable
@Serializable
data class ResultDto(
val result: String,
)

View File

@ -1,20 +1,20 @@
package exh.md.handlers.serializers package exh.md.dto
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class SimilarMangaResponse( data class SimilarMangaDto(
val id: String, val id: String,
val title: Map<String, String>, val title: Map<String, String>,
val contentRating: String, val contentRating: String,
val matches: List<Matches>, val matches: List<SimilarMangaMatchListDto>,
val updatedAt: String val updatedAt: String,
) )
@Serializable @Serializable
data class Matches( data class SimilarMangaMatchListDto(
val id: String, val id: String,
val title: Map<String, String>, val title: Map<String, String>,
val contentRating: String, val contentRating: String,
val score: Double val score: Double,
) )

View File

@ -1,37 +0,0 @@
package exh.md.handlers
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.Page
import exh.md.handlers.serializers.ChapterResponse
import exh.md.utils.MdUtil
import okhttp3.Response
class ApiChapterParser {
fun pageListParse(response: Response, host: String, dataSaver: Boolean): List<Page> {
val networkApiChapter = response.parseAs<ChapterResponse>(MdUtil.jsonParser)
val pages = mutableListOf<Page>()
val atHomeRequestUrl = response.request.url.toUrl().toString()
val hash = networkApiChapter.data.attributes.hash
val pageArray = if (dataSaver) {
networkApiChapter.data.attributes.dataSaver.map { "/data-saver/$hash/$it" }
} else {
networkApiChapter.data.attributes.data.map { "/data/$hash/$it" }
}
val now = System.currentTimeMillis()
pageArray.forEach { imgUrl ->
val mdAtHomeUrl = "$host,$atHomeRequestUrl,$now"
pages += Page(pages.size, mdAtHomeUrl, imgUrl)
}
return pages
}
fun externalParse(response: Response): String {
val chapterResponse = response.parseAs<ChapterResponse>()
val external = chapterResponse.data.attributes.data.first()
return external.substringAfterLast("/")
}
}

View File

@ -1,25 +1,20 @@
package exh.md.handlers package exh.md.handlers
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.log.xLogE import exh.log.xLogE
import exh.md.handlers.serializers.AuthorResponseList import exh.md.dto.ChapterDto
import exh.md.handlers.serializers.ChapterResponse import exh.md.dto.MangaDto
import exh.md.handlers.serializers.MangaResponse import exh.md.utils.MdConstants
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.util.capitalize import exh.util.capitalize
import exh.util.dropEmpty
import exh.util.floor import exh.util.floor
import exh.util.nullIfEmpty import exh.util.nullIfEmpty
import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@ -27,7 +22,6 @@ import uy.kohesive.injekt.injectLazy
import java.util.Locale import java.util.Locale
class ApiMangaParser( class ApiMangaParser(
private val client: OkHttpClient,
private val lang: String private val lang: String
) { ) {
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
@ -42,11 +36,7 @@ class ApiMangaParser(
}?.call() }?.call()
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!") ?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
suspend fun parseToManga(manga: MangaInfo, input: Response, sourceId: Long): MangaInfo { fun parseToManga(manga: MangaInfo, input: MangaDto, sourceId: Long): MangaInfo {
return parseToManga(manga, input.parseAs<MangaResponse>(MdUtil.jsonParser), sourceId)
}
suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, sourceId: Long): MangaInfo {
val mangaId = db.getManga(manga.key, sourceId).executeAsBlocking()?.id val mangaId = db.getManga(manga.key, sourceId).executeAsBlocking()?.id
val metadata = if (mangaId != null) { val metadata = if (mangaId != null) {
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeAsBlocking() val flatMetadata = db.getFlatMetadataForManga(mangaId).executeAsBlocking()
@ -62,59 +52,34 @@ class ApiMangaParser(
return metadata.createMangaInfo(manga) return metadata.createMangaInfo(manga)
} }
/** fun parseIntoMetadata(metadata: MangaDexSearchMetadata, mangaDto: MangaDto) {
* Parse the manga details json into metadata object
*/
suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
parseIntoMetadata(metadata, input.parseAs<MangaResponse>(MdUtil.jsonParser))
}
suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, networkApiManga: MangaResponse) {
with(metadata) { with(metadata) {
try { try {
val networkManga = networkApiManga.data.attributes val mangaAttributesDto = mangaDto.data.attributes
mdUuid = networkApiManga.data.id mdUuid = mangaDto.data.id
title = MdUtil.cleanString(networkManga.title[lang] ?: networkManga.title["en"]!!) title = MdUtil.cleanString(mangaAttributesDto.title[lang] ?: mangaAttributesDto.title["en"]!!)
altTitles = networkManga.altTitles.mapNotNull { it[lang] }.nullIfEmpty() altTitles = mangaAttributesDto.altTitles.mapNotNull { it[lang] }.nullIfEmpty()
val coverId = networkApiManga.relationships.firstOrNull { it.type.equals("cover_art", true) }?.id mangaDto.relationships
cover = MdUtil.getCoverUrl(networkApiManga.data.id, coverId, client) .firstOrNull { relationshipDto -> relationshipDto.type == MdConstants.Types.coverArt }
?.attributes
?.fileName
?.let { coverFileName ->
cover = MdUtil.cdnCoverUrl(mangaDto.data.id, coverFileName)
}
description = MdUtil.cleanDescription(networkManga.description[lang] ?: networkManga.description["en"]!!) description = MdUtil.cleanDescription(mangaAttributesDto.description[lang] ?: mangaAttributesDto.description["en"]!!)
val authorIds = networkApiManga.relationships authors = mangaDto.relationships.filter { relationshipDto ->
.filter { it.type.equals("author", true) } relationshipDto.type.equals(MdConstants.Types.author, true)
.map { it.id } }.mapNotNull { it.attributes!!.name }.distinct()
.toSet()
val artistIds = networkApiManga.relationships
.filter { it.type.equals("artist", true) }
.map { it.id }
.toSet()
// get author/artist map ignore if they error artists = mangaDto.relationships.filter { relationshipDto ->
val authorMap = runCatching { relationshipDto.type.equals(MdConstants.Types.artist, true)
(authorIds + artistIds).chunked(10) }.mapNotNull { it.attributes!!.name }.distinct()
.flatMap { idList ->
val ids = idList.joinToString("&ids[]=", "?ids[]=")
val response = client.newCall(GET("${MdUtil.authorUrl}$ids")).await()
if (response.code != 204) {
response
.parseAs<AuthorResponseList>()
.results.map {
it.data.id to MdUtil.cleanString(it.data.attributes.name)
}
} else {
emptyList()
}
}
.toMap()
}.getOrNull() ?: emptyMap()
authors = authorIds.mapNotNull { authorMap[it] }.dropEmpty() langFlag = mangaAttributesDto.originalLanguage
artists = artistIds.mapNotNull { authorMap[it] }.dropEmpty() val lastChapter = mangaAttributesDto.lastChapter?.toFloatOrNull()
langFlag = networkManga.originalLanguage
val lastChapter = networkManga.lastChapter?.toFloatOrNull()
lastChapterNumber = lastChapter?.floor() lastChapterNumber = lastChapter?.floor()
/*networkManga.rating?.let { /*networkManga.rating?.let {
@ -122,7 +87,7 @@ class ApiMangaParser(
manga.users = it.users manga.users = it.users
}*/ }*/
networkManga.links?.let { links -> mangaAttributesDto.links?.let { links ->
links["al"]?.let { anilistId = it } links["al"]?.let { anilistId = it }
links["kt"]?.let { kitsuId = it } links["kt"]?.let { kitsuId = it }
links["mal"]?.let { myAnimeListId = it } links["mal"]?.let { myAnimeListId = it }
@ -132,7 +97,7 @@ class ApiMangaParser(
// val filteredChapters = filterChapterForChecking(networkApiManga) // val filteredChapters = filterChapterForChecking(networkApiManga)
val tempStatus = parseStatus(networkManga.status ?: "") val tempStatus = parseStatus(mangaAttributesDto.status ?: "")
/*val publishedOrCancelled = /*val publishedOrCancelled =
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) { if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
@ -144,17 +109,18 @@ class ApiMangaParser(
// things that will go with the genre tags but aren't actually genre // things that will go with the genre tags but aren't actually genre
val nonGenres = listOfNotNull( val nonGenres = listOfNotNull(
networkManga.publicationDemographic mangaAttributesDto.publicationDemographic
?.let { RaisedTag("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) }, ?.let { RaisedTag("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
networkManga.contentRating mangaAttributesDto.contentRating
?.takeUnless { it == "safe" } ?.takeUnless { it == "safe" }
?.let { RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) }, ?.let { RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
) )
val genres = nonGenres + networkManga.tags val genres = nonGenres + mangaAttributesDto.tags
.mapNotNull { dexTag -> .mapNotNull {
dexTag.attributes.name[lang] ?: dexTag.attributes.name["en"] it.attributes.name[lang] ?: it.attributes.name["en"]
}.map { }
.map {
RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT) RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
} }
@ -223,14 +189,7 @@ class ApiMangaParser(
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
/** fun chapterListParse(chapterListResponse: List<ChapterDto>, groupMap: Map<String, String>): List<ChapterInfo> {
* Parse for the random manga id from the [MdUtil.randMangaPage] response.
*/
fun randomMangaIdParse(response: Response): String {
return response.parseAs<MangaResponse>(MdUtil.jsonParser).data.id
}
fun chapterListParse(chapterListResponse: List<ChapterResponse>, groupMap: Map<String, String>): List<ChapterInfo> {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
return chapterListResponse.asSequence() return chapterListResponse.asSequence()
@ -242,19 +201,14 @@ class ApiMangaParser(
} }
fun chapterParseForMangaId(response: Response): String { fun chapterParseForMangaId(response: Response): String {
try { return response.parseAs<ChapterDto>(MdUtil.jsonParser)
return response.parseAs<ChapterResponse>(MdUtil.jsonParser) .relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
.relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
} catch (e: Exception) {
XLog.e(e)
throw e
}
} }
fun StringBuilder.appends(string: String) = append("$string ") fun StringBuilder.appends(string: String): StringBuilder = append("$string ")
private fun mapChapter( private fun mapChapter(
networkChapter: ChapterResponse, networkChapter: ChapterDto,
groups: Map<String, String>, groups: Map<String, String>,
): ChapterInfo { ): ChapterInfo {
val attributes = networkChapter.data.attributes val attributes = networkChapter.data.attributes

View File

@ -1,13 +1,10 @@
package exh.md.handlers package exh.md.handlers
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
import okhttp3.HttpUrl
import java.util.Locale import java.util.Locale
class FilterHandler(private val preferencesHelper: PreferencesHelper) { class FilterHandler {
internal fun getMDFilterList(): FilterList { internal fun getMDFilterList(): FilterList {
val filters = mutableListOf( val filters = mutableListOf(
OriginalLanguageList(getOriginalLanguage()), OriginalLanguageList(getOriginalLanguage()),
@ -85,7 +82,7 @@ class FilterHandler(private val preferencesHelper: PreferencesHelper) {
Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"), Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"),
Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"), Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"),
Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"), Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"),
Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Crossdressing"), Tag("9ab53f92-3eed-4e9b-903a-917c86035ee3", "Crossdressing"),
Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"), Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"),
Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"), Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"),
Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"), Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"),
@ -160,108 +157,99 @@ class FilterHandler(private val preferencesHelper: PreferencesHelper) {
Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1) Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1)
val sortableList = listOf( val sortableList = listOf(
Pair("Default (Asc/Desc doesn't matter)", ""), Pair("Number of follows", ""),
Pair("Created at", "createdAt"), Pair("Created at", "createdAt"),
Pair("Updated at", "updatedAt"), Pair("Updated at", "updatedAt"),
) )
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(0, false)) class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(0, false))
fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList): String { fun getQueryMap(filters: FilterList): Map<String, Any> {
url.apply { val queryMap = mutableMapOf<String, Any>()
// add filters
filters.forEach { filter -> val originalLanguageList = mutableListOf<String>() // originalLanguage[]
when (filter) { val contentRatingList = mutableListOf<String>() // contentRating[]
is OriginalLanguageList -> { val demographicList = mutableListOf<String>() // publicationDemographic[]
filter.state.forEach { lang -> val statusList = mutableListOf<String>() // status[]
if (lang.state) { val includeTagList = mutableListOf<String>() // includedTags[]
addQueryParameter( val excludeTagList = mutableListOf<String>() // excludedTags[]
"originalLanguage[]",
lang.isoCode // add filters
) filters.forEach { filter ->
} when (filter) {
is OriginalLanguageList -> {
filter.state.filter { lang -> lang.state }
.forEach { lang ->
if (lang.isoCode == "zh") { if (lang.isoCode == "zh") {
addQueryParameter( originalLanguageList.add("zh-hk")
"originalLanguage[]",
"zh-hk"
)
} }
originalLanguageList.add(lang.isoCode)
} }
} }
is ContentRatingList -> { is ContentRatingList -> {
filter.state.forEach { rating -> filter.state.filter { rating -> rating.state }
if (rating.state) { .forEach { rating ->
addQueryParameter( contentRatingList.add(rating.name.lowercase(Locale.US))
"contentRating[]",
rating.name.lowercase(Locale.US)
)
}
} }
} }
is DemographicList -> { is DemographicList -> {
filter.state.forEach { demographic -> filter.state.filter { demographic -> demographic.state }
if (demographic.state) { .forEach { demographic ->
addQueryParameter( demographicList.add(demographic.name.lowercase(Locale.US))
"publicationDemographic[]",
demographic.name.lowercase(
Locale.US
)
)
}
} }
} }
is StatusList -> { is StatusList -> {
filter.state.forEach { status -> filter.state.filter { status -> status.state }
if (status.state) { .forEach { status ->
addQueryParameter( statusList.add(status.name.lowercase(Locale.US))
"status[]",
status.name.lowercase(
Locale.US
)
)
}
} }
} }
is SortFilter -> { is SortFilter -> {
if (filter.state != null) { if (filter.state != null && filter.state!!.index != 0) {
if (filter.state!!.index != 0) { val query = sortableList[filter.state!!.index].second
val query = sortableList[filter.state!!.index].second val value = when (filter.state!!.ascending) {
val value = when (filter.state!!.ascending) { true -> "asc"
true -> "asc" false -> "desc"
false -> "desc"
}
addQueryParameter("order[$query]", value)
}
} }
} queryMap["order[$query]"] = value
is TagList -> {
filter.state.forEach { tag ->
if (tag.isIncluded()) {
addQueryParameter("includedTags[]", tag.id)
} else if (tag.isExcluded()) {
addQueryParameter("excludedTags[]", tag.id)
}
}
}
is TagInclusionMode -> {
addQueryParameter(
"includedTagsMode",
filter.values[filter.state].uppercase(Locale.US)
)
}
is TagExclusionMode -> {
addQueryParameter(
"excludedTagsMode",
filter.values[filter.state].uppercase(Locale.US)
)
} }
} }
} is TagList -> {
if (false) { // preferencesHelper.showR18Filter().not()) { filter.state.forEach { tag ->
addQueryParameter("contentRating[]", "safe") if (tag.isIncluded()) {
includeTagList.add(tag.id)
} else if (tag.isExcluded()) {
excludeTagList.add(tag.id)
}
}
}
is TagInclusionMode -> {
queryMap["includedTagsMode"] = filter.values[filter.state].uppercase(Locale.US)
}
is TagExclusionMode -> {
queryMap["excludedTagsMode"] = filter.values[filter.state].uppercase(Locale.US)
}
} }
} }
if (originalLanguageList.isNotEmpty()) {
queryMap["originalLanguage[]"] = originalLanguageList
}
if (contentRatingList.isNotEmpty()) {
queryMap["contentRating[]"] = contentRatingList
}
if (demographicList.isNotEmpty()) {
queryMap["publicationDemographic[]"] = demographicList
}
if (statusList.isNotEmpty()) {
queryMap["status[]"] = statusList
}
if (includeTagList.isNotEmpty()) {
queryMap["includedTags[]"] = includeTagList
}
if (excludeTagList.isNotEmpty()) {
queryMap["excludedTags[]"] = excludeTagList
}
return url.toString() return queryMap
} }
} }

View File

@ -1,45 +1,24 @@
package exh.md.handlers package exh.md.handlers
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.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.handlers.serializers.MangaListResponse import exh.md.dto.MangaDto
import exh.md.handlers.serializers.MangaResponse import exh.md.dto.ReadingStatusDto
import exh.md.handlers.serializers.MangaStatusListResponse import exh.md.service.MangaDexAuthService
import exh.md.handlers.serializers.MangaStatusResponse
import exh.md.handlers.serializers.ResultResponse
import exh.md.handlers.serializers.UpdateReadingStatus
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.md.utils.mdListCall import exh.md.utils.mdListCall
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.util.under import exh.util.under
import kotlinx.serialization.encodeToString import kotlinx.coroutines.async
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import java.util.Locale
class FollowsHandler( class FollowsHandler(
private val client: OkHttpClient,
private val headers: Headers,
private val preferences: PreferencesHelper,
private val lang: String, private val lang: String,
private val mdList: MdList private val service: MangaDexAuthService
) { ) {
/** /**
@ -47,21 +26,15 @@ class FollowsHandler(
*/ */
suspend fun fetchFollows(page: Int): MetadataMangasPage { suspend fun fetchFollows(page: Int): MetadataMangasPage {
return withIOContext { return withIOContext {
val response = client.newCall(followsListRequest(MdUtil.mangaLimit * page - 1)).await() val follows = service.userFollowList(MdUtil.mangaLimit * page)
if (response.code == 204) {
if (follows.results.isEmpty()) {
return@withIOContext MetadataMangasPage(emptyList(), false, emptyList()) return@withIOContext MetadataMangasPage(emptyList(), false, emptyList())
} }
val mangaListResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser) val hasMoreResults = follows.limit + follows.offset under follows.total
val statusListResponse = service.readingStatusAllManga()
if (mangaListResponse.results.isEmpty()) { val results = followsParseMangaPage(follows.results, statusListResponse.statuses)
return@withIOContext MetadataMangasPage(emptyList(), false, emptyList())
}
val hasMoreResults = mangaListResponse.limit + mangaListResponse.offset under mangaListResponse.total
val statusListResponse = client.newCall(mangaStatusListRequest()).await()
.parseAs<MangaStatusListResponse>()
val results = followsParseMangaPage(mangaListResponse.results, statusListResponse.statuses)
MetadataMangasPage(results.map { it.first }, hasMoreResults, results.map { it.second }) MetadataMangasPage(results.map { it.first }, hasMoreResults, results.map { it.second })
} }
@ -71,53 +44,23 @@ class FollowsHandler(
* Parse follows api to manga page * Parse follows api to manga page
* used when multiple follows * used when multiple follows
*/ */
private suspend fun followsParseMangaPage(response: List<MangaResponse>, statuses: Map<String, String?>): List<Pair<SManga, MangaDexSearchMetadata>> { private fun followsParseMangaPage(
response: List<MangaDto>,
statuses: Map<String, String?>
): List<Pair<SManga, MangaDexSearchMetadata>> {
val comparator = compareBy<Pair<SManga, MangaDexSearchMetadata>> { it.second.followStatus } val comparator = compareBy<Pair<SManga, MangaDexSearchMetadata>> { it.second.followStatus }
.thenBy { it.first.title } .thenBy { it.first.title }
val coverMap = MdUtil.getCoversFromMangaList(response, client)
return response.map { return response.map {
MdUtil.createMangaEntry( MdUtil.createMangaEntry(
it, it,
lang, lang
coverMap[it.data.id]
).toSManga() to MangaDexSearchMetadata().apply { ).toSManga() to MangaDexSearchMetadata().apply {
followStatus = FollowStatus.fromDex(statuses[it.data.id]).int followStatus = FollowStatus.fromDex(statuses[it.data.id]).int
} }
}.sortedWith(comparator) }.sortedWith(comparator)
} }
/**
* fetch follow status used when fetching status for 1 manga
*/
private fun followStatusParse(response: Response, sResponse: Response): Track {
val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
val statusResponse = sResponse.parseAs<MangaStatusResponse>()
val track = Track.create(TrackManager.MDLIST)
track.status = FollowStatus.fromDex(statusResponse.status).int
track.tracking_url = MdUtil.baseUrl + "/manga/" + mangaResponse.data.id
track.title = mangaResponse.data.attributes.title[lang] ?: mangaResponse.data.attributes.title["en"]!!
/* if (follow.chapter.isNotBlank()) {
track.last_chapter_read = follow.chapter.toFloat().floor()
}*/
return track
}
/**
* build Request for follows page
*/
private fun followsListRequest(offset: Int): Request {
val tempUrl = MdUtil.userFollows.toHttpUrl().newBuilder()
tempUrl.apply {
addQueryParameter("limit", MdUtil.mangaLimit.toString())
addQueryParameter("offset", offset.toString())
}
return GET(tempUrl.build().toString(), MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK)
}
/** /**
* Change the status of a manga * Change the status of a manga
*/ */
@ -125,21 +68,17 @@ class FollowsHandler(
return withIOContext { return withIOContext {
val status = when (followStatus == FollowStatus.UNFOLLOWED) { val status = when (followStatus == FollowStatus.UNFOLLOWED) {
true -> null true -> null
false -> followStatus.name.lowercase(Locale.US) false -> followStatus.toDex()
}
val readingStatusDto = ReadingStatusDto(status)
if (followStatus == FollowStatus.UNFOLLOWED) {
service.unfollowManga(mangaId)
} else {
service.followManga(mangaId)
} }
val jsonString = MdUtil.jsonParser.encodeToString(UpdateReadingStatus(status)) service.updateReadingStatusForManga(mangaId, readingStatusDto).result == "ok"
val postResult = client.newCall(
POST(
MdUtil.updateReadingStatusUrl(mangaId),
MdUtil.getAuthHeaders(headers, preferences, mdList),
jsonString.toRequestBody("application/json".toMediaType())
)
).await()
val body = postResult.parseAs<ResultResponse>(MdUtil.jsonParser)
body.result == "ok"
} }
} }
@ -203,43 +142,26 @@ class FollowsHandler(
*/ */
suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> { suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
return withIOContext { return withIOContext {
val results = client.mdListCall<MangaResponse> { val results = async {
followsListRequest(it) mdListCall {
service.userFollowList(it)
}
} }
val statuses = client.newCall(mangaStatusListRequest()).await() val readingStatusResponse = async { service.readingStatusAllManga().statuses }
.parseAs<MangaStatusListResponse>().statuses
followsParseMangaPage(results, statuses) followsParseMangaPage(results.await(), readingStatusResponse.await())
} }
} }
suspend fun fetchTrackingInfo(url: String): Track { suspend fun fetchTrackingInfo(url: String): Track {
return withIOContext { return withIOContext {
val mangaId = MdUtil.getMangaId(url) val mangaId = MdUtil.getMangaId(url)
val request = GET( val followStatus = FollowStatus.fromDex(service.readingStatusForManga(mangaId).status)
MdUtil.mangaUrl + "/" + mangaId, Track.create(TrackManager.MDLIST).apply {
MdUtil.getAuthHeaders(headers, preferences, mdList), status = followStatus.int
CacheControl.FORCE_NETWORK tracking_url = "${MdUtil.baseUrl}/title/$mangaId"
) }
val statusRequest = GET(
MdUtil.mangaUrl + "/" + mangaId + "/status",
MdUtil.getAuthHeaders(headers, preferences, mdList),
CacheControl.FORCE_NETWORK
)
val response = client.newCall(request).await()
val statusResponse = client.newCall(statusRequest).await()
followStatusParse(response, statusResponse)
} }
} }
private fun mangaStatusListRequest(status: FollowStatus? = null): Request {
val mangaStatusUrl = MdUtil.mangaStatus.toHttpUrl().newBuilder()
if (status != null) {
mangaStatusUrl.addQueryParameter("status", status.name.lowercase(Locale.US))
}
return GET(mangaStatusUrl.build().toString(), MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK)
}
} }

View File

@ -1,9 +1,6 @@
package exh.md.handlers package exh.md.handlers
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toMangaInfo
@ -11,55 +8,25 @@ import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.handlers.serializers.ChapterResponse import exh.md.dto.ChapterDto
import exh.md.handlers.serializers.GroupListResponse import exh.md.service.MangaDexService
import exh.md.utils.MdConstants
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.md.utils.mdListCall import exh.md.utils.mdListCall
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import kotlinx.coroutines.async import kotlinx.coroutines.async
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
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
import uy.kohesive.injekt.api.get
class MangaHandler( class MangaHandler(
private val client: OkHttpClient,
private val headers: Headers,
private val lang: String, private val lang: String,
private val service: MangaDexService,
private val apiMangaParser: ApiMangaParser, private val apiMangaParser: ApiMangaParser,
private val followsHandler: FollowsHandler private val followsHandler: FollowsHandler
) { ) {
suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long, forceLatestCovers: Boolean): Pair<MangaInfo, List<ChapterInfo>> {
return withIOContext {
val response = client.newCall(mangaRequest(manga)).await()
apiMangaParser.parseToManga(manga, response, sourceId) to getChapterList(manga)
}
}
/*suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> {
*//* if (forceLatestCovers) {
val covers = client.newCall(coverRequest(manga)).await().parseAs<ApiCovers>(MdUtil.jsonParser)
return covers.data.map { it.url }
} else {*//*
return emptyList()
// }
}*/
suspend fun getMangaIdFromChapterId(urlChapterId: String): String {
return withIOContext {
val request = GET(MdUtil.chapterUrl + urlChapterId)
val response = client.newCall(request).await()
apiMangaParser.chapterParseForMangaId(response)
}
}
suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long, forceLatestCovers: Boolean): MangaInfo { suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long, forceLatestCovers: Boolean): MangaInfo {
val response = withIOContext { client.newCall(mangaRequest(manga)).await() } val response = withIOContext { service.viewManga(MdUtil.getMangaId(manga.key)) }
return apiMangaParser.parseToManga(manga, response, sourceId) return apiMangaParser.parseToManga(manga, response, sourceId)
} }
@ -75,8 +42,8 @@ class MangaHandler(
suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> { suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return withIOContext { return withIOContext {
val results = client.mdListCall<ChapterResponse> { val results = mdListCall {
mangaFeedRequest(manga, it, lang) service.viewChapters(MdUtil.getMangaId(manga.key), lang, it)
} }
val groupMap = getGroupMap(results) val groupMap = getGroupMap(results)
@ -85,31 +52,17 @@ class MangaHandler(
} }
} }
private suspend fun getGroupMap(results: List<ChapterResponse>): Map<String, String> { private fun getGroupMap(results: List<ChapterDto>): Map<String, String> {
val groupIds = results.asSequence() return results.map { chapter -> chapter.relationships }
.flatMap { chapter -> chapter.relationships } .flatten()
.filter { it.type == "scanlation_group" } .filter { it.type == MdConstants.Types.scanlator }
.map { it.id } .map { it.id to it.attributes!!.name!! }
.toSet() .toMap()
return runCatching {
groupIds.chunked(100).flatMapIndexed { index, ids ->
val response = client.newCall(groupIdRequest(ids, 100 * index)).await()
if (response.code != 204) {
response
.parseAs<GroupListResponse>(MdUtil.jsonParser)
.results.map { group -> group.data.id to group.data.attributes.name }
} else {
emptyList()
}
}.toMap()
}.getOrNull().orEmpty()
} }
suspend fun fetchRandomMangaId(): String { suspend fun fetchRandomMangaId(): String {
return withIOContext { return withIOContext {
val response = client.newCall(randomMangaRequest()).await() service.randomManga().data.id
apiMangaParser.randomMangaIdParse(response)
} }
} }
@ -129,28 +82,4 @@ class MangaHandler(
remoteTrack.await() to null remoteTrack.await() to null
} }
} }
private fun randomMangaRequest(): Request {
return GET(MdUtil.randomMangaUrl, cache = CacheControl.FORCE_NETWORK)
}
private fun mangaRequest(manga: MangaInfo): Request {
return GET(MdUtil.mangaUrl + "/" + MdUtil.getMangaId(manga.key), headers, CacheControl.FORCE_NETWORK)
}
private fun mangaFeedRequest(manga: MangaInfo, offset: Int, lang: String): Request {
return GET(MdUtil.mangaFeedUrl(MdUtil.getMangaId(manga.key), offset, lang), headers, CacheControl.FORCE_NETWORK)
}
private fun groupIdRequest(id: List<String>, offset: Int): Request {
val urlSuffix = id.joinToString("&ids[]=", "?limit=100&offset=$offset&ids[]=")
return GET(MdUtil.groupUrl + urlSuffix, headers)
}
/* private fun coverRequest(manga: SManga): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK)
}*/
companion object {
}
} }

View File

@ -2,7 +2,7 @@ package exh.md.handlers
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import exh.md.handlers.serializers.MangaPlusSerializer import exh.md.dto.MangaPlusSerializer
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor

View File

@ -2,57 +2,65 @@ package exh.md.handlers
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.dto.AtHomeDto
import exh.md.dto.ChapterDto
import exh.md.service.MangaDexService
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
class PageHandler( class PageHandler(
private val client: OkHttpClient,
private val headers: Headers, private val headers: Headers,
private val apiChapterParser: ApiChapterParser, private val service: MangaDexService,
private val mangaPlusHandler: MangaPlusHandler, private val mangaPlusHandler: MangaPlusHandler,
private val preferences: PreferencesHelper, private val preferences: PreferencesHelper,
private val mdList: MdList, private val mdList: MdList,
) { ) {
fun fetchPageList(chapter: SChapter, isLogged: Boolean, usePort443Only: Boolean, dataSaver: Boolean): Observable<List<Page>> { suspend fun fetchPageList(chapter: SChapter, isLogged: Boolean, usePort443Only: Boolean, dataSaver: Boolean): List<Page> {
if (chapter.scanlator.equals("MangaPlus")) { return withIOContext {
return client.newCall(pageListRequest(chapter)) val chapterResponse = service.viewChapter(MdUtil.getChapterId(chapter.url))
.asObservableSuccess()
.map { response -> if (chapter.scanlator.equals("mangaplus", true)) {
val chapterId = apiChapterParser.externalParse(response) mangaPlusHandler.fetchPageList(
mangaPlusHandler.fetchPageList(chapterId) chapterResponse.data.attributes.data
.first()
.substringAfterLast("/")
)
} else {
val headers = if (isLogged) {
MdUtil.getAuthHeaders(headers, preferences, mdList)
} else {
headers
} }
}
val atHomeRequestUrl = if (usePort443Only) { val (atHomeRequestUrl, atHomeResponse) = service.getAtHomeServer(headers, MdUtil.getChapterId(chapter.url), usePort443Only)
"${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}?forcePort443=true"
} else {
"${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}"
}
val headers = if (isLogged) { pageListParse(chapterResponse, atHomeRequestUrl, atHomeResponse, dataSaver)
MdUtil.getAuthHeaders(headers, preferences, mdList)
} else {
headers
}
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
val host = MdUtil.atHomeUrlHostUrl(atHomeRequestUrl, client, headers, CacheControl.FORCE_NETWORK)
apiChapterParser.pageListParse(response, host, dataSaver)
} }
}
} }
private fun pageListRequest(chapter: SChapter): Request { fun pageListParse(
return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK) chapterDto: ChapterDto,
atHomeRequestUrl: String,
atHomeDto: AtHomeDto,
dataSaver: Boolean,
): List<Page> {
val hash = chapterDto.data.attributes.hash
val pageArray = if (dataSaver) {
chapterDto.data.attributes.dataSaver.map { "/data-saver/$hash/$it" }
} else {
chapterDto.data.attributes.data.map { "/data/$hash/$it" }
}
val now = System.currentTimeMillis()
val pages = pageArray.mapIndexed { pos, imgUrl ->
Page(pos + 1, "${atHomeDto.baseUrl},$atHomeRequestUrl,$now", imgUrl)
}
return pages
} }
} }

View File

@ -1,60 +0,0 @@
package exh.md.handlers
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.lang.runAsObservable
import exh.md.handlers.serializers.MangaListResponse
import exh.md.utils.MdUtil
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
/**
* Returns the latest manga from the updates url since it actually respects the users settings
*/
class PopularHandler(
private val client: OkHttpClient,
private val headers: Headers,
private val lang: String
) {
fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
.asObservableSuccess()
.flatMap { response ->
runAsObservable({
popularMangaParse(response)
})
}
}
private fun popularMangaRequest(page: Int): Request {
val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder()
tempUrl.apply {
addQueryParameter("limit", MdUtil.mangaLimit.toString())
addQueryParameter("offset", (MdUtil.getMangaListOffset(page)))
}
return GET(tempUrl.build().toString(), headers, CacheControl.FORCE_NETWORK)
}
private suspend fun popularMangaParse(response: Response): MangasPage {
val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
val coverMap = MdUtil.getCoversFromMangaList(mlResponse.results, client)
val mangaList = mlResponse.results.map {
MdUtil.createMangaEntry(it, lang, coverMap[it.data.id]).toSManga()
}
return MangasPage(mangaList, hasMoreResults)
}
}

View File

@ -1,93 +0,0 @@
package exh.md.handlers
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.lang.runAsObservable
import exh.md.handlers.serializers.MangaListResponse
import exh.md.handlers.serializers.MangaResponse
import exh.md.utils.MdUtil
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
class SearchHandler(
private val client: OkHttpClient,
private val headers: Headers,
private val lang: String,
private val filterHandler: FilterHandler,
private val apiMangaParser: ApiMangaParser
) {
fun fetchSearchManga(page: Int, query: String, filters: FilterList, sourceId: Long): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(realQuery))
.asObservableSuccess()
.flatMap { response ->
runAsObservable({
val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
val details = apiMangaParser
.parseToManga(MdUtil.createMangaEntry(mangaResponse, lang, null), response, sourceId).toSManga()
MangasPage(listOf(details), false)
})
}
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.flatMap { response ->
runAsObservable({
searchMangaParse(response)
})
}
}
}
private suspend fun searchMangaParse(response: Response): MangasPage {
val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val coverMap = MdUtil.getCoversFromMangaList(mlResponse.results, client)
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
val mangaList = mlResponse.results.map {
MdUtil.createMangaEntry(it, lang, coverMap[it.data.id]).toSManga()
}
return MangasPage(mangaList, hasMoreResults)
}
private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder()
tempUrl.apply {
addQueryParameter("limit", MdUtil.mangaLimit.toString())
addQueryParameter("offset", (MdUtil.getMangaListOffset(page)))
val actualQuery = query.replace(WHITESPACE_REGEX, " ")
if (actualQuery.isNotBlank()) {
addQueryParameter("title", actualQuery)
}
}
val finalUrl = filterHandler.addFiltersToUrl(tempUrl, filters)
return GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
}
private fun searchMangaByIdRequest(id: String): Request {
return GET(MdUtil.mangaUrl + "/" + id, headers, CacheControl.FORCE_NETWORK)
}
private fun searchMangaByGroupRequest(group: String): Request {
return GET(MdUtil.groupSearchUrl + group, headers, CacheControl.FORCE_NETWORK)
}
companion object {
const val PREFIX_ID_SEARCH = "id:"
const val PREFIX_GROUP_SEARCH = "group:"
val WHITESPACE_REGEX = "\\s".toRegex()
}
}

View File

@ -1,65 +1,35 @@
package exh.md.handlers package exh.md.handlers
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.toSManga
import exh.md.handlers.serializers.CoverListResponse import exh.md.dto.SimilarMangaDto
import exh.md.handlers.serializers.SimilarMangaResponse import exh.md.service.MangaDexService
import exh.md.service.SimilarService
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
class SimilarHandler( class SimilarHandler(
private val client: OkHttpClient, private val lang: String,
private val lang: String private val service: MangaDexService,
private val similarService: SimilarService
) { ) {
suspend fun getSimilar(manga: MangaInfo): MangasPage { suspend fun getSimilar(manga: MangaInfo): MangasPage {
val response = client.newCall(similarMangaRequest(manga)).await() val similarDto = similarService.getSimilarManga(MdUtil.getMangaId(manga.key))
.parseAs<SimilarMangaResponse>() return similarDtoToMangaListPage(similarDto)
val ids = response.matches.map { it.id }
val coverUrl = MdUtil.coverUrl.toHttpUrl().newBuilder().apply {
ids.forEach { mangaId ->
addQueryParameter("manga[]", mangaId)
}
addQueryParameter("limit", ids.size.toString())
}.build().toString()
val coverListResponse = client.newCall(GET(coverUrl)).await()
.parseAs<CoverListResponse>()
val unique = coverListResponse.results.distinctBy { it.relationships[0].id }
val coverMap = unique.map { coverResponse ->
val fileName = coverResponse.data.attributes.fileName
val mangaId = coverResponse.relationships.first { it.type.equals("manga", true) }.id
val thumbnailUrl = "${MdUtil.cdnUrl}/covers/$mangaId/$fileName"
mangaId to thumbnailUrl
}.toMap()
return similarMangaParse(response, coverMap)
} }
private fun similarMangaRequest(manga: MangaInfo): Request { private suspend fun similarDtoToMangaListPage(
val tempUrl = MdUtil.similarBaseApi + MdUtil.getMangaId(manga.key) + ".json" similarMangaDto: SimilarMangaDto,
return GET(tempUrl, Headers.Builder().build(), CacheControl.FORCE_NETWORK) ): MangasPage {
} val ids = similarMangaDto.matches.map {
it.id
private fun similarMangaParse(response: SimilarMangaResponse, coverMap: Map<String, String>): MangasPage {
val mangaList = response.matches.map {
SManga.create().apply {
url = MdUtil.buildMangaUrl(it.id)
title = MdUtil.cleanString(it.title[lang] ?: it.title["en"]!!)
thumbnail_url = coverMap[it.id]
}
} }
val mangaList = service.viewMangas(ids).results.map {
MdUtil.createMangaEntry(it, lang).toSManga()
}
return MangasPage(mangaList, false) return MangasPage(mangaList, false)
} }
} }

View File

@ -1,39 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
/**
* Login Request object for Dex Api
*/
@Serializable
data class LoginRequest(val username: String, val password: String)
/**
* Response after login
*/
@Serializable
data class LoginResponse(val result: String, val token: LoginBodyToken)
/**
* Tokens for the logins
*/
@Serializable
data class LoginBodyToken(val session: String, val refresh: String)
/**
* Response after logout
*/
@Serializable
data class ResultResponse(val result: String)
/**
* Check if session token is valid
*/
@Serializable
data class CheckTokenResponse(val isAuthenticated: Boolean)
/**
* Request to refresh token
*/
@Serializable
data class RefreshTokenRequest(val token: String)

View File

@ -1,40 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class CacheApiMangaSerializer(
val id: Long,
val title: String,
val url: String,
val description: String,
val is_r18: Boolean,
val rating: Float,
val demographic: List<String>,
val content: List<String>,
val format: List<String>,
val genre: List<String>,
val theme: List<String>,
val languages: List<String>,
val related: List<CacheRelatedSerializer>,
val external: MutableMap<String, String>,
val last_updated: String,
val matches: List<CacheSimilarMatchesSerializer>,
)
@Serializable
data class CacheRelatedSerializer(
val id: Long,
val title: String,
val type: String,
val r18: Boolean,
)
@Serializable
data class CacheSimilarMatchesSerializer(
val id: Long,
val title: String,
val score: Float,
val r18: Boolean,
val languages: List<String>,
)

View File

@ -1,67 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class ChapterListResponse(
val limit: Int,
val offset: Int,
val total: Int,
val results: List<ChapterResponse>
)
@Serializable
data class ChapterResponse(
val result: String,
val data: NetworkChapter,
val relationships: List<Relationships>
)
@Serializable
data class NetworkChapter(
val id: String,
val type: String,
val attributes: ChapterAttributes,
)
@Serializable
data class ChapterAttributes(
val title: String?,
val volume: String?,
val chapter: String?,
val translatedLanguage: String,
val publishAt: String,
val data: List<String>,
val dataSaver: List<String>,
val hash: String,
)
@Serializable
data class AtHomeResponse(
val baseUrl: String
)
@Serializable
data class GroupListResponse(
val limit: Int,
val offset: Int,
val total: Int,
val results: List<GroupResponse>
)
@Serializable
data class GroupResponse(
val result: String,
val data: GroupData,
)
@Serializable
data class GroupData(
val id: String,
val attributes: GroupAttributes,
)
@Serializable
data class GroupAttributes(
val name: String,
)

View File

@ -1,10 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class ImageReportResult(
val url: String,
val success: Boolean,
val bytes: Int?
)

View File

@ -1,11 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class ListCallResponse<T>(
val limit: Int,
val offset: Int,
val total: Int,
val results: List<T>
)

View File

@ -1,112 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class MangaListResponse(
val limit: Int,
val offset: Int,
val total: Int,
val results: List<MangaResponse>
)
@Serializable
data class MangaResponse(
val result: String,
val data: NetworkManga,
val relationships: List<Relationships>
)
@Serializable
data class NetworkManga(val id: String, val type: String, val attributes: NetworkMangaAttributes)
@Serializable
data class NetworkMangaAttributes(
val title: Map<String, String>,
val altTitles: List<Map<String, String>>,
val description: Map<String, String>,
val links: Map<String, String>?,
val originalLanguage: String,
val lastVolume: String?,
val lastChapter: String?,
val contentRating: String?,
val publicationDemographic: String?,
val status: String?,
val year: Int?,
val tags: List<TagsSerializer>
)
@Serializable
data class TagsSerializer(
val id: String,
val attributes: TagAttributes
)
@Serializable
data class TagAttributes(
val name: Map<String, String>
)
@Serializable
data class Relationships(
val id: String,
val type: String,
)
@Serializable
data class AuthorResponseList(
val results: List<AuthorResponse>,
)
@Serializable
data class AuthorResponse(
val result: String,
val data: NetworkAuthor,
)
@Serializable
data class NetworkAuthor(
val id: String,
val attributes: AuthorAttributes,
)
@Serializable
data class AuthorAttributes(
val name: String,
)
@Serializable
data class UpdateReadingStatus(
val status: String?
)
@Serializable
data class MangaStatusResponse(
val status: String?
)
@Serializable
data class MangaStatusListResponse(
val statuses: Map<String, String?>
)
@Serializable
data class CoverListResponse(
val results: List<CoverResponse>,
)
@Serializable
data class CoverResponse(
val data: Cover,
val relationships: List<Relationships>
)
@Serializable
data class Cover(
val attributes: CoverAttributes,
)
@Serializable
data class CoverAttributes(
val fileName: String,
)

View File

@ -1,13 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class NetworkFollowed(
val code: Int,
val message: String = "",
val data: List<FollowedSerializer>? = null
)
@Serializable
data class FollowedSerializer(val mangaId: String, val mangaTitle: String, val followType: Int)

View File

@ -2,50 +2,32 @@ package exh.md.network
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.log.xLogE
import exh.log.xLogI import exh.log.xLogI
import exh.md.handlers.serializers.CheckTokenResponse import exh.md.dto.LoginRequestDto
import exh.md.handlers.serializers.LoginBodyToken import exh.md.dto.RefreshTokenDto
import exh.md.handlers.serializers.LoginRequest import exh.md.service.MangaDexAuthService
import exh.md.handlers.serializers.LoginResponse
import exh.md.handlers.serializers.RefreshTokenRequest
import exh.md.handlers.serializers.ResultResponse
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
class MangaDexLoginHelper(val client: OkHttpClient, val preferences: PreferencesHelper, val mdList: MdList) { class MangaDexLoginHelper(val authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
suspend fun isAuthenticated(authHeaders: Headers): Boolean { val authService by authServiceLazy
val response = client.newCall(GET(MdUtil.checkTokenUrl, authHeaders, CacheControl.FORCE_NETWORK)).await() suspend fun isAuthenticated(): Boolean {
val body = response.parseAs<CheckTokenResponse>(MdUtil.jsonParser) return runCatching { authService.checkToken().isAuthenticated }
return body.isAuthenticated .getOrElse { e ->
xLogE("error authenticating", e)
false
}
} }
suspend fun refreshToken(authHeaders: Headers): Boolean { suspend fun refreshToken(): Boolean {
val refreshToken = MdUtil.refreshToken(preferences, mdList) val refreshToken = MdUtil.refreshToken(preferences, mdList)
if (refreshToken.isNullOrEmpty()) { if (refreshToken.isNullOrEmpty()) {
return false return false
} }
val result = RefreshTokenRequest(refreshToken)
val jsonString = MdUtil.jsonParser.encodeToString(result)
val postResult = client.newCall(
POST(
MdUtil.refreshTokenUrl,
authHeaders,
jsonString.toRequestBody("application/json".toMediaType())
)
).await()
val refresh = runCatching { val refresh = runCatching {
val jsonResponse = postResult.parseAs<LoginResponse>(MdUtil.jsonParser) val jsonResponse = authService.refreshToken(RefreshTokenDto(refreshToken))
preferences.trackToken(mdList).set(MdUtil.jsonParser.encodeToString(jsonResponse.token)) preferences.trackToken(mdList).set(MdUtil.jsonParser.encodeToString(jsonResponse.token))
} }
return refresh.isSuccess return refresh.isSuccess
@ -54,56 +36,32 @@ class MangaDexLoginHelper(val client: OkHttpClient, val preferences: Preferences
suspend fun login( suspend fun login(
username: String, username: String,
password: String, password: String,
): LoginResult { ): Boolean {
return withIOContext { return withIOContext {
val loginRequest = LoginRequest(username, password) val loginRequest = LoginRequestDto(username, password)
val loginResult = runCatching { authService.login(loginRequest) }
val jsonString = MdUtil.jsonParser.encodeToString(loginRequest) val loginResponseDto = loginResult.getOrNull()
MdUtil.updateLoginToken(
val postResult = runCatching { loginResponseDto?.token,
client.newCall( preferences,
POST( mdList
MdUtil.loginUrl, )
Headers.Builder().build(), loginResponseDto != null
jsonString.toRequestBody("application/json".toMediaType())
)
).await()
}
val response = postResult.getOrNull() ?: return@withIOContext LoginResult.Failure(postResult.exceptionOrNull())
// if it fails to parse then login failed
val loginResponse = try {
response.parseAs<LoginResponse>(MdUtil.jsonParser)
} catch (e: SerializationException) {
null
}
if (response.code == 200 && loginResponse != null && loginResponse.result == "ok") {
LoginResult.Success(loginResponse.token)
} else {
LoginResult.Failure()
}
} }
} }
sealed class LoginResult { suspend fun login(): Boolean {
data class Failure(val e: Throwable? = null) : LoginResult()
data class Success(val token: LoginBodyToken) : LoginResult()
}
suspend fun login(): LoginResult {
val username = preferences.trackUsername(mdList) val username = preferences.trackUsername(mdList)
val password = preferences.trackPassword(mdList) val password = preferences.trackPassword(mdList)
if (username.isNullOrBlank() || password.isNullOrBlank()) { if (username.isNullOrBlank() || password.isNullOrBlank()) {
xLogI("No username or password stored, can't login") xLogI("No username or password stored, can't login")
return LoginResult.Failure() return false
} }
return login(username, password) return login(username, password)
} }
suspend fun logout(authHeaders: Headers): Boolean { suspend fun logout(): Boolean {
val response = client.newCall(GET(MdUtil.logoutUrl, authHeaders, CacheControl.FORCE_NETWORK)).await() return authService.logout().result == "ok"
val body = response.parseAs<ResultResponse>(MdUtil.jsonParser)
return body.result == "ok"
} }
} }

View File

@ -1,12 +1,9 @@
package exh.md.network package exh.md.network
import exh.log.xLogD
import exh.log.xLogI import exh.log.xLogI
import exh.log.xLogW
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator import okhttp3.Authenticator
import okhttp3.Headers
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.Route import okhttp3.Route
@ -14,6 +11,7 @@ import okhttp3.Route
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator { class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? { override fun authenticate(route: Route?, response: Response): Request? {
xLogI("Detected Auth error ${response.code} on ${response.request.url}") xLogI("Detected Auth error ${response.code} on ${response.request.url}")
val token = refreshToken(loginHelper) val token = refreshToken(loginHelper)
return if (token != null) { return if (token != null) {
response.request.newBuilder().header("Authorization", token).build() response.request.newBuilder().header("Authorization", token).build()
@ -27,24 +25,7 @@ class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authent
var validated = false var validated = false
runBlocking { runBlocking {
val checkTokenResult = runCatching { val checkToken = loginHelper.isAuthenticated()
loginHelper.isAuthenticated(
MdUtil.getAuthHeaders(
Headers.Builder().build(),
loginHelper.preferences,
loginHelper.mdList
)
)
}
val checkToken = if (checkTokenResult.isSuccess) {
checkTokenResult.getOrNull() ?: false
} else {
val e = checkTokenResult.exceptionOrNull()
if (e is NoSessionException) {
this@TokenAuthenticator.xLogD("Session token does not exist")
}
false
}
if (checkToken) { if (checkToken) {
this@TokenAuthenticator.xLogI("Token is valid, other thread must have refreshed it") this@TokenAuthenticator.xLogI("Token is valid, other thread must have refreshed it")
@ -52,37 +33,16 @@ class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authent
} }
if (validated.not()) { if (validated.not()) {
this@TokenAuthenticator.xLogI("Token is invalid trying to refresh") this@TokenAuthenticator.xLogI("Token is invalid trying to refresh")
val result = runCatching { validated = loginHelper.refreshToken()
validated = loginHelper.refreshToken(
MdUtil.getAuthHeaders(
Headers.Builder().build(),
loginHelper.preferences,
loginHelper.mdList
)
)
}
if (result.isFailure) {
result.exceptionOrNull()?.let {
this@TokenAuthenticator.xLogW("Error refreshing token", it)
}
}
} }
if (validated.not()) { if (validated.not()) {
this@TokenAuthenticator.xLogI("Did not refresh token, trying to login") this@TokenAuthenticator.xLogI("Did not refresh token, trying to login")
val loginResult = loginHelper.login() validated = loginHelper.login()
validated = if (loginResult is MangaDexLoginHelper.LoginResult.Success) {
MdUtil.updateLoginToken(
loginResult.token,
loginHelper.preferences,
loginHelper.mdList
)
true
} else false
} }
} }
return when { return when {
validated -> "bearer: ${MdUtil.sessionToken(loginHelper.preferences, loginHelper.mdList)!!}" validated -> "Bearer ${MdUtil.sessionToken(loginHelper.preferences, loginHelper.mdList)!!}"
else -> null else -> null
} }
} }

View File

@ -0,0 +1,185 @@
package exh.md.service
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.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import exh.md.dto.CheckTokenDto
import exh.md.dto.LoginRequestDto
import exh.md.dto.LoginResponseDto
import exh.md.dto.LogoutDto
import exh.md.dto.MangaListDto
import exh.md.dto.ReadChapterDto
import exh.md.dto.ReadingStatusDto
import exh.md.dto.ReadingStatusMapDto
import exh.md.dto.RefreshTokenDto
import exh.md.dto.ResultDto
import exh.md.utils.MdApi
import exh.md.utils.MdUtil
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
class MangaDexAuthService(
private val client: OkHttpClient,
private val headers: Headers,
private val preferences: PreferencesHelper,
private val mdList: MdList
) {
fun getHeaders() = MdUtil.getAuthHeaders(
headers,
preferences,
mdList
)
suspend fun login(request: LoginRequestDto): LoginResponseDto {
return client.newCall(
POST(
MdApi.login,
body = MdUtil.encodeToBody(request),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun logout(): LogoutDto {
return client.newCall(
POST(
MdApi.logout,
getHeaders(),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun checkToken(): CheckTokenDto {
return client.newCall(
GET(
MdApi.checkToken,
getHeaders(),
CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun refreshToken(request: RefreshTokenDto): LoginResponseDto {
return client.newCall(
POST(
MdApi.refreshToken,
getHeaders(),
body = MdUtil.encodeToBody(request),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
// &includes[]=${MdConstants.Type.coverArt}
suspend fun userFollowList(offset: Int): MangaListDto {
return client.newCall(
GET(
"${MdApi.userFollows}?limit=100&offset=$offset",
getHeaders(),
CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun readingStatusForManga(mangaId: String): ReadingStatusDto {
return client.newCall(
GET(
"${MdApi.manga}/$mangaId/status",
getHeaders(),
CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun readChaptersForManga(mangaId: String): ReadChapterDto {
return client.newCall(
GET(
"${MdApi.manga}/$mangaId/read",
getHeaders(),
CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun updateReadingStatusForManga(
mangaId: String,
readingStatusDto: ReadingStatusDto,
): ResultDto {
return client.newCall(
POST(
"${MdApi.manga}/$mangaId/status",
getHeaders(),
body = MdUtil.encodeToBody(readingStatusDto),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun readingStatusAllManga(): ReadingStatusMapDto {
return client.newCall(
GET(
MdApi.readingStatusForAllManga,
getHeaders(),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun readingStatusByType(status: String): ReadingStatusMapDto {
return client.newCall(
GET(
"${MdApi.readingStatusForAllManga}?status=$status",
getHeaders(),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun markChapterRead(chapterId: String): ResultDto {
return client.newCall(
POST(
"${MdApi.chapter}/$chapterId/read",
getHeaders(),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun markChapterUnRead(chapterId: String): ResultDto {
return client.newCall(
Request.Builder()
.url("${MdApi.chapter}/$chapterId/read")
.delete()
.headers(getHeaders())
.cacheControl(CacheControl.FORCE_NETWORK)
.build()
).await().parseAs(MdUtil.jsonParser)
}
suspend fun followManga(mangaId: String): ResultDto {
return client.newCall(
POST(
"${MdApi.manga}/$mangaId/follow",
getHeaders(),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun unfollowManga(mangaId: String): ResultDto {
return client.newCall(
Request.Builder()
.url("${MdApi.manga}/$mangaId/follow")
.delete()
.headers(getHeaders())
.cacheControl(CacheControl.FORCE_NETWORK)
.build()
).await().parseAs(MdUtil.jsonParser)
}
}

View File

@ -0,0 +1,107 @@
package exh.md.service
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import exh.md.dto.AtHomeDto
import exh.md.dto.AtHomeImageReportDto
import exh.md.dto.ChapterDto
import exh.md.dto.ChapterListDto
import exh.md.dto.MangaDto
import exh.md.dto.MangaListDto
import exh.md.dto.ResultDto
import exh.md.utils.MdApi
import exh.md.utils.MdConstants
import exh.md.utils.MdUtil
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
class MangaDexService(
private val client: OkHttpClient
) {
suspend fun viewMangas(
ids: List<String>
): MangaListDto {
return client.newCall(
GET(
MdApi.manga.toHttpUrl().newBuilder().apply {
addQueryParameter("includes[]", MdConstants.Types.coverArt)
addQueryParameter("limit", ids.size.toString())
ids.forEach {
addQueryParameter("ids[]", it)
}
}.build().toString(),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun viewManga(
id: String
): MangaDto {
return client.newCall(
GET(
"${MdApi.manga}/$id?includes[]=${MdConstants.Types.coverArt}&includes[]=${MdConstants.Types.author}&includes[]=${MdConstants.Types.artist}",
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun viewChapters(
id: String,
translatedLanguage: String,
offset: Int,
): ChapterListDto {
val url = "${MdApi.manga}/$id/feed?limit=500&includes[]=${MdConstants.Types.scanlator}&order[volume]=desc&order[chapter]=desc".toHttpUrl()
.newBuilder()
.apply {
addQueryParameter("translatedLanguage[]", translatedLanguage)
addQueryParameter("offset", offset.toString())
}.build()
.toString()
return client.newCall(
GET(
url,
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun viewChapter(id: String): ChapterDto {
return client.newCall(GET("${MdApi.chapter}/$id", cache = CacheControl.FORCE_NETWORK))
.await()
.parseAs(MdUtil.jsonParser)
}
suspend fun randomManga(): MangaDto {
return client.newCall(GET("${MdApi.manga}/random", cache = CacheControl.FORCE_NETWORK))
.await()
.parseAs(MdUtil.jsonParser)
}
suspend fun atHomeImageReport(atHomeImageReportDto: AtHomeImageReportDto): ResultDto {
return client.newCall(
POST(
MdConstants.atHomeReportUrl,
body = MdUtil.encodeToBody(atHomeImageReportDto),
cache = CacheControl.FORCE_NETWORK
)
).await().parseAs(MdUtil.jsonParser)
}
suspend fun getAtHomeServer(
headers: Headers,
chapterId: String,
forcePort443: Boolean,
): Pair<String, AtHomeDto> {
val url = "${MdApi.atHomeServer}/$chapterId?forcePort443=$forcePort443"
return client.newCall(GET(url, headers, CacheControl.FORCE_NETWORK))
.await()
.let { it.request.url.toUrl().toString() to it.parseAs(MdUtil.jsonParser) }
}
}

View File

@ -0,0 +1,20 @@
package exh.md.service
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import exh.md.dto.SimilarMangaDto
import exh.md.utils.MdUtil
import okhttp3.OkHttpClient
class SimilarService(
private val client: OkHttpClient
) {
suspend fun getSimilarManga(mangaId: String): SimilarMangaDto {
return client.newCall(
GET(
"${MdUtil.similarBaseApi}$mangaId.json"
)
).await().parseAs()
}
}

View File

@ -11,6 +11,8 @@ enum class FollowStatus(val int: Int) {
DROPPED(5), DROPPED(5),
RE_READING(6); RE_READING(6);
fun toDex(): String = this.name.lowercase(Locale.US)
companion object { companion object {
fun fromDex(value: String?): FollowStatus = values().firstOrNull { it.name.lowercase(Locale.US) == value } ?: UNFOLLOWED fun fromDex(value: String?): FollowStatus = values().firstOrNull { it.name.lowercase(Locale.US) == value } ?: UNFOLLOWED
fun fromInt(value: Int): FollowStatus = values().firstOrNull { it.int == value } ?: UNFOLLOWED fun fromInt(value: Int): FollowStatus = values().firstOrNull { it.int == value } ?: UNFOLLOWED

View File

@ -0,0 +1,19 @@
package exh.md.utils
object MdApi {
const val baseUrl = "https://api.mangadex.org"
const val login = "$baseUrl/auth/login"
const val checkToken = "$baseUrl/auth/check"
const val refreshToken = "$baseUrl/auth/refresh"
const val logout = "$baseUrl/auth/logout"
const val manga = "$baseUrl/manga"
const val chapter = "$baseUrl/chapter"
const val group = "$baseUrl/group"
const val author = "$baseUrl/author"
const val chapterImageServer = "$baseUrl/at-home/server"
const val userFollows = "$baseUrl/user/follows/manga"
const val readingStatusForAllManga = "$baseUrl/manga/status"
const val atHomeServer = "$baseUrl/at-home/server"
const val legacyMapping = "$baseUrl/legacy/mapping"
}

View File

@ -0,0 +1,19 @@
package exh.md.utils
import exh.util.minutes
object MdConstants {
const val baseUrl = "https://mangadex.org"
const val cdnUrl = "https://uploads.mangadex.org"
const val atHomeReportUrl = "https://api.mangadex.network/report"
object Types {
const val author = "author"
const val artist = "artist"
const val coverArt = "cover_art"
const val manga = "manga"
const val scanlator = "scanlation_group"
}
val mdAtHomeTokenLifespan = 5.minutes.inWholeMilliseconds
}

View File

@ -0,0 +1,17 @@
package exh.md.utils
import exh.md.dto.ListCallDto
import exh.util.under
suspend fun <T> mdListCall(request: suspend (offset: Int) -> ListCallDto<T>): List<T> {
val results = mutableListOf<T>()
var offset = 0
do {
val list = request(offset)
results += list.results
offset += list.limit
} while (offset under list.total)
return results
}

View File

@ -2,36 +2,26 @@ package exh.md.utils
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.mdlist.MdList 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.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MangaDex
import exh.log.xLogD import exh.log.xLogD
import exh.log.xLogE import exh.md.dto.LoginBodyTokenDto
import exh.md.handlers.serializers.AtHomeResponse import exh.md.dto.MangaDto
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.network.NoSessionException import exh.md.network.NoSessionException
import exh.source.getMainSource import exh.source.getMainSource
import exh.util.floor import exh.util.floor
import exh.util.nullIfBlank import exh.util.nullIfBlank
import exh.util.nullIfZero import exh.util.nullIfZero
import exh.util.under
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.RequestBody
import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -45,35 +35,7 @@ class MdUtil {
companion object { companion object {
const val cdnUrl = "https://uploads.mangadex.org" const val cdnUrl = "https://uploads.mangadex.org"
const val baseUrl = "https://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 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 similarCacheMapping = "https://api.similarmanga.com/mapping/mdex2search.csv"
const val similarCacheMangas = "https://api.similarmanga.com/manga/" const val similarCacheMangas = "https://api.similarmanga.com/manga/"
@ -83,7 +45,7 @@ class MdUtil {
const val reportUrl = "https://api.mangadex.network/report" const val reportUrl = "https://api.mangadex.network/report"
const val mdAtHomeTokenLifespan = 10 * 60 * 1000 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 * Get the manga offset pages are 1 based, so subtract 1
@ -217,10 +179,6 @@ class MdUtil {
return "/manga/$mangaUuid" 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 // Get the ID from the manga url
fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/") fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/")
@ -298,76 +256,33 @@ class MdUtil {
return null 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) val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
.apply { timeZone = TimeZone.getTimeZone("UTC") } .apply { timeZone = TimeZone.getTimeZone("UTC") }
fun parseDate(dateAsString: String): Long = fun parseDate(dateAsString: String): Long =
dateFormatter.parse(dateAsString)?.time ?: 0 dateFormatter.parse(dateAsString)?.time ?: 0
fun createMangaEntry(json: MangaResponse, lang: String, coverUrl: String?): MangaInfo { fun createMangaEntry(json: MangaDto, lang: String): MangaInfo {
return MangaInfo( return MangaInfo(
key = buildMangaUrl(json.data.id), key = buildMangaUrl(json.data.id),
title = cleanString(json.data.attributes.title[lang] ?: json.data.attributes.title["en"]!!), 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 { fun cdnCoverUrl(dexId: String, fileName: String): String {
coverId ?: return ""
val coverResponse = client.newCall(GET("$coverUrl/$coverId"))
.await().parseAs<CoverResponse>()
val fileName = coverResponse.data.attributes.fileName
return "$cdnUrl/covers/$dexId/$fileName" 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 { fun getLoginBody(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
try { try {
jsonParser.decodeFromString<LoginBodyToken>(it) jsonParser.decodeFromString<LoginBodyTokenDto>(it)
} catch (e: SerializationException) { } catch (e: SerializationException) {
xLogD("Unable to load login body") xLogD("Unable to load login body")
null null
@ -378,8 +293,10 @@ class MdUtil {
fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = getLoginBody(preferences, mdList)?.refresh fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = getLoginBody(preferences, mdList)?.refresh
fun updateLoginToken(token: LoginBodyToken, preferences: PreferencesHelper, mdList: MdList) { fun updateLoginToken(token: LoginBodyTokenDto?, preferences: PreferencesHelper, mdList: MdList) {
preferences.trackToken(mdList).set(jsonParser.encodeToString(token)) if (token != null) {
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
} else preferences.trackToken(mdList).delete()
} }
fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) = fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) =
@ -400,27 +317,17 @@ class MdUtil {
val disabledSourceIds = preferences.disabledSources().get() val disabledSourceIds = preferences.disabledSources().get()
return sourceManager.getVisibleOnlineSources() return sourceManager.getVisibleOnlineSources()
.asSequence()
.map { it.getMainSource() } .map { it.getMainSource() }
.filterIsInstance<MangaDex>() .filterIsInstance<MangaDex>()
.filter { it.lang in languages } .filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds } .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
}