Update Mangadex
This commit is contained in:
parent
efba76380a
commit
20d8cf6c10
@ -21,8 +21,9 @@ import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.md.handlers.ApiChapterParser
|
||||
import exh.md.dto.MangaDto
|
||||
import exh.md.handlers.ApiMangaParser
|
||||
import exh.md.handlers.FollowsHandler
|
||||
import exh.md.handlers.MangaHandler
|
||||
@ -32,13 +33,14 @@ import exh.md.handlers.SimilarHandler
|
||||
import exh.md.network.MangaDexLoginHelper
|
||||
import exh.md.network.NoSessionException
|
||||
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.MdLang
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
@ -51,7 +53,7 @@ import kotlin.reflect.KClass
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
MetadataSource<MangaDexSearchMetadata, Response>,
|
||||
MetadataSource<MangaDexSearchMetadata, MangaDto>,
|
||||
// UrlImportableSource,
|
||||
FollowsSource,
|
||||
LoginSource,
|
||||
@ -73,7 +75,9 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
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()
|
||||
.authenticator(
|
||||
@ -84,26 +88,30 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
|
||||
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
|
||||
|
||||
private val apiMangaParser by lazy {
|
||||
ApiMangaParser(baseHttpClient, mdLang.lang)
|
||||
private val mangadexService by lazy {
|
||||
MangaDexService(client)
|
||||
}
|
||||
private val apiChapterParser by lazy {
|
||||
ApiChapterParser()
|
||||
private val mangadexAuthService by mangadexAuthServiceLazy
|
||||
private val similarService by lazy {
|
||||
SimilarService(client)
|
||||
}
|
||||
private val apiMangaParser by lazy {
|
||||
ApiMangaParser(mdLang.lang)
|
||||
}
|
||||
private val followsHandler by lazy {
|
||||
FollowsHandler(baseHttpClient, headers, preferences, mdLang.lang, mdList)
|
||||
FollowsHandler(mdLang.lang, mangadexAuthService)
|
||||
}
|
||||
private val mangaHandler by lazy {
|
||||
MangaHandler(baseHttpClient, headers, mdLang.lang, apiMangaParser, followsHandler)
|
||||
MangaHandler(mdLang.lang, mangadexService, apiMangaParser, followsHandler)
|
||||
}
|
||||
private val similarHandler by lazy {
|
||||
SimilarHandler(baseHttpClient, mdLang.lang)
|
||||
SimilarHandler(mdLang.lang, mangadexService, similarService)
|
||||
}
|
||||
private val mangaPlusHandler by lazy {
|
||||
MangaPlusHandler(network.client)
|
||||
}
|
||||
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> =
|
||||
@ -152,7 +160,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
|
||||
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> {
|
||||
@ -168,7 +176,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
return MangaDexDescriptionAdapter(controller)
|
||||
}
|
||||
|
||||
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
||||
override suspend fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: MangaDto) {
|
||||
apiMangaParser.parseIntoMetadata(metadata, input)
|
||||
}
|
||||
|
||||
@ -198,8 +206,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
twoFactorCode: String?
|
||||
): Boolean {
|
||||
val result = loginHelper.login(username, password)
|
||||
return if (result is MangaDexLoginHelper.LoginResult.Success) {
|
||||
MdUtil.updateLoginToken(result.token, preferences, mdList)
|
||||
return if (result) {
|
||||
mdList.saveCredentials(username, password)
|
||||
true
|
||||
} else false
|
||||
@ -207,7 +214,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
|
||||
override suspend fun logout(): Boolean {
|
||||
val result = try {
|
||||
loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList))
|
||||
loginHelper.logout()
|
||||
} catch (e: NoSessionException) {
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
|
17
app/src/main/java/exh/md/dto/AtHomeDto.kt
Normal file
17
app/src/main/java/exh/md/dto/AtHomeDto.kt
Normal 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,
|
||||
)
|
39
app/src/main/java/exh/md/dto/AuthDto.kt
Normal file
39
app/src/main/java/exh/md/dto/AuthDto.kt
Normal 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)
|
62
app/src/main/java/exh/md/dto/ChapterDto.kt
Normal file
62
app/src/main/java/exh/md/dto/ChapterDto.kt
Normal 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,
|
||||
)
|
8
app/src/main/java/exh/md/dto/ListCallDto.kt
Normal file
8
app/src/main/java/exh/md/dto/ListCallDto.kt
Normal 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>
|
||||
}
|
119
app/src/main/java/exh/md/dto/MangaDto.kt
Normal file
119
app/src/main/java/exh/md/dto/MangaDto.kt
Normal 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,
|
||||
)
|
@ -1,4 +1,4 @@
|
||||
package exh.md.handlers.serializers
|
||||
package exh.md.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Serializer
|
8
app/src/main/java/exh/md/dto/ResultDto.kt
Normal file
8
app/src/main/java/exh/md/dto/ResultDto.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package exh.md.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ResultDto(
|
||||
val result: String,
|
||||
)
|
@ -1,20 +1,20 @@
|
||||
package exh.md.handlers.serializers
|
||||
package exh.md.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SimilarMangaResponse(
|
||||
data class SimilarMangaDto(
|
||||
val id: String,
|
||||
val title: Map<String, String>,
|
||||
val contentRating: String,
|
||||
val matches: List<Matches>,
|
||||
val updatedAt: String
|
||||
val matches: List<SimilarMangaMatchListDto>,
|
||||
val updatedAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Matches(
|
||||
data class SimilarMangaMatchListDto(
|
||||
val id: String,
|
||||
val title: Map<String, String>,
|
||||
val contentRating: String,
|
||||
val score: Double
|
||||
val score: Double,
|
||||
)
|
@ -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("/")
|
||||
}
|
||||
}
|
@ -1,25 +1,20 @@
|
||||
package exh.md.handlers
|
||||
|
||||
import com.elvishew.xlog.XLog
|
||||
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.source.model.SManga
|
||||
import exh.log.xLogE
|
||||
import exh.md.handlers.serializers.AuthorResponseList
|
||||
import exh.md.handlers.serializers.ChapterResponse
|
||||
import exh.md.handlers.serializers.MangaResponse
|
||||
import exh.md.dto.ChapterDto
|
||||
import exh.md.dto.MangaDto
|
||||
import exh.md.utils.MdConstants
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.util.capitalize
|
||||
import exh.util.dropEmpty
|
||||
import exh.util.floor
|
||||
import exh.util.nullIfEmpty
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
@ -27,7 +22,6 @@ import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
class ApiMangaParser(
|
||||
private val client: OkHttpClient,
|
||||
private val lang: String
|
||||
) {
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
@ -42,11 +36,7 @@ class ApiMangaParser(
|
||||
}?.call()
|
||||
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||
|
||||
suspend fun parseToManga(manga: MangaInfo, input: Response, sourceId: Long): MangaInfo {
|
||||
return parseToManga(manga, input.parseAs<MangaResponse>(MdUtil.jsonParser), sourceId)
|
||||
}
|
||||
|
||||
suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, sourceId: Long): MangaInfo {
|
||||
fun parseToManga(manga: MangaInfo, input: MangaDto, sourceId: Long): MangaInfo {
|
||||
val mangaId = db.getManga(manga.key, sourceId).executeAsBlocking()?.id
|
||||
val metadata = if (mangaId != null) {
|
||||
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
||||
@ -62,59 +52,34 @@ class ApiMangaParser(
|
||||
return metadata.createMangaInfo(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, mangaDto: MangaDto) {
|
||||
with(metadata) {
|
||||
try {
|
||||
val networkManga = networkApiManga.data.attributes
|
||||
mdUuid = networkApiManga.data.id
|
||||
title = MdUtil.cleanString(networkManga.title[lang] ?: networkManga.title["en"]!!)
|
||||
altTitles = networkManga.altTitles.mapNotNull { it[lang] }.nullIfEmpty()
|
||||
val mangaAttributesDto = mangaDto.data.attributes
|
||||
mdUuid = mangaDto.data.id
|
||||
title = MdUtil.cleanString(mangaAttributesDto.title[lang] ?: mangaAttributesDto.title["en"]!!)
|
||||
altTitles = mangaAttributesDto.altTitles.mapNotNull { it[lang] }.nullIfEmpty()
|
||||
|
||||
val coverId = networkApiManga.relationships.firstOrNull { it.type.equals("cover_art", true) }?.id
|
||||
cover = MdUtil.getCoverUrl(networkApiManga.data.id, coverId, client)
|
||||
mangaDto.relationships
|
||||
.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
|
||||
.filter { it.type.equals("author", true) }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
val artistIds = networkApiManga.relationships
|
||||
.filter { it.type.equals("artist", true) }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
authors = mangaDto.relationships.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MdConstants.Types.author, true)
|
||||
}.mapNotNull { it.attributes!!.name }.distinct()
|
||||
|
||||
// get author/artist map ignore if they error
|
||||
val authorMap = runCatching {
|
||||
(authorIds + artistIds).chunked(10)
|
||||
.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()
|
||||
artists = mangaDto.relationships.filter { relationshipDto ->
|
||||
relationshipDto.type.equals(MdConstants.Types.artist, true)
|
||||
}.mapNotNull { it.attributes!!.name }.distinct()
|
||||
|
||||
authors = authorIds.mapNotNull { authorMap[it] }.dropEmpty()
|
||||
artists = artistIds.mapNotNull { authorMap[it] }.dropEmpty()
|
||||
|
||||
langFlag = networkManga.originalLanguage
|
||||
val lastChapter = networkManga.lastChapter?.toFloatOrNull()
|
||||
langFlag = mangaAttributesDto.originalLanguage
|
||||
val lastChapter = mangaAttributesDto.lastChapter?.toFloatOrNull()
|
||||
lastChapterNumber = lastChapter?.floor()
|
||||
|
||||
/*networkManga.rating?.let {
|
||||
@ -122,7 +87,7 @@ class ApiMangaParser(
|
||||
manga.users = it.users
|
||||
}*/
|
||||
|
||||
networkManga.links?.let { links ->
|
||||
mangaAttributesDto.links?.let { links ->
|
||||
links["al"]?.let { anilistId = it }
|
||||
links["kt"]?.let { kitsuId = it }
|
||||
links["mal"]?.let { myAnimeListId = it }
|
||||
@ -132,7 +97,7 @@ class ApiMangaParser(
|
||||
|
||||
// val filteredChapters = filterChapterForChecking(networkApiManga)
|
||||
|
||||
val tempStatus = parseStatus(networkManga.status ?: "")
|
||||
val tempStatus = parseStatus(mangaAttributesDto.status ?: "")
|
||||
/*val publishedOrCancelled =
|
||||
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
|
||||
if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
|
||||
@ -144,17 +109,18 @@ class ApiMangaParser(
|
||||
|
||||
// things that will go with the genre tags but aren't actually genre
|
||||
val nonGenres = listOfNotNull(
|
||||
networkManga.publicationDemographic
|
||||
mangaAttributesDto.publicationDemographic
|
||||
?.let { RaisedTag("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
|
||||
networkManga.contentRating
|
||||
mangaAttributesDto.contentRating
|
||||
?.takeUnless { it == "safe" }
|
||||
?.let { RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
|
||||
)
|
||||
|
||||
val genres = nonGenres + networkManga.tags
|
||||
.mapNotNull { dexTag ->
|
||||
dexTag.attributes.name[lang] ?: dexTag.attributes.name["en"]
|
||||
}.map {
|
||||
val genres = nonGenres + mangaAttributesDto.tags
|
||||
.mapNotNull {
|
||||
it.attributes.name[lang] ?: it.attributes.name["en"]
|
||||
}
|
||||
.map {
|
||||
RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
|
||||
}
|
||||
|
||||
@ -223,14 +189,7 @@ class ApiMangaParser(
|
||||
else -> SManga.UNKNOWN
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
fun chapterListParse(chapterListResponse: List<ChapterDto>, groupMap: Map<String, String>): List<ChapterInfo> {
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
return chapterListResponse.asSequence()
|
||||
@ -242,19 +201,14 @@ class ApiMangaParser(
|
||||
}
|
||||
|
||||
fun chapterParseForMangaId(response: Response): String {
|
||||
try {
|
||||
return response.parseAs<ChapterResponse>(MdUtil.jsonParser)
|
||||
.relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
|
||||
} catch (e: Exception) {
|
||||
XLog.e(e)
|
||||
throw e
|
||||
}
|
||||
return response.parseAs<ChapterDto>(MdUtil.jsonParser)
|
||||
.relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
|
||||
}
|
||||
|
||||
fun StringBuilder.appends(string: String) = append("$string ")
|
||||
fun StringBuilder.appends(string: String): StringBuilder = append("$string ")
|
||||
|
||||
private fun mapChapter(
|
||||
networkChapter: ChapterResponse,
|
||||
networkChapter: ChapterDto,
|
||||
groups: Map<String, String>,
|
||||
): ChapterInfo {
|
||||
val attributes = networkChapter.data.attributes
|
||||
|
@ -1,13 +1,10 @@
|
||||
package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.Locale
|
||||
|
||||
class FilterHandler(private val preferencesHelper: PreferencesHelper) {
|
||||
|
||||
class FilterHandler {
|
||||
internal fun getMDFilterList(): FilterList {
|
||||
val filters = mutableListOf(
|
||||
OriginalLanguageList(getOriginalLanguage()),
|
||||
@ -85,7 +82,7 @@ class FilterHandler(private val preferencesHelper: PreferencesHelper) {
|
||||
Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"),
|
||||
Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"),
|
||||
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("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"),
|
||||
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)
|
||||
|
||||
val sortableList = listOf(
|
||||
Pair("Default (Asc/Desc doesn't matter)", ""),
|
||||
Pair("Number of follows", ""),
|
||||
Pair("Created at", "createdAt"),
|
||||
Pair("Updated at", "updatedAt"),
|
||||
)
|
||||
|
||||
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(0, false))
|
||||
|
||||
fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList): String {
|
||||
url.apply {
|
||||
// add filters
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OriginalLanguageList -> {
|
||||
filter.state.forEach { lang ->
|
||||
if (lang.state) {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
lang.isoCode
|
||||
)
|
||||
}
|
||||
fun getQueryMap(filters: FilterList): Map<String, Any> {
|
||||
val queryMap = mutableMapOf<String, Any>()
|
||||
|
||||
val originalLanguageList = mutableListOf<String>() // originalLanguage[]
|
||||
val contentRatingList = mutableListOf<String>() // contentRating[]
|
||||
val demographicList = mutableListOf<String>() // publicationDemographic[]
|
||||
val statusList = mutableListOf<String>() // status[]
|
||||
val includeTagList = mutableListOf<String>() // includedTags[]
|
||||
val excludeTagList = mutableListOf<String>() // excludedTags[]
|
||||
|
||||
// add filters
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OriginalLanguageList -> {
|
||||
filter.state.filter { lang -> lang.state }
|
||||
.forEach { lang ->
|
||||
if (lang.isoCode == "zh") {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
"zh-hk"
|
||||
)
|
||||
originalLanguageList.add("zh-hk")
|
||||
}
|
||||
originalLanguageList.add(lang.isoCode)
|
||||
}
|
||||
}
|
||||
is ContentRatingList -> {
|
||||
filter.state.forEach { rating ->
|
||||
if (rating.state) {
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
rating.name.lowercase(Locale.US)
|
||||
)
|
||||
}
|
||||
}
|
||||
is ContentRatingList -> {
|
||||
filter.state.filter { rating -> rating.state }
|
||||
.forEach { rating ->
|
||||
contentRatingList.add(rating.name.lowercase(Locale.US))
|
||||
}
|
||||
}
|
||||
is DemographicList -> {
|
||||
filter.state.forEach { demographic ->
|
||||
if (demographic.state) {
|
||||
addQueryParameter(
|
||||
"publicationDemographic[]",
|
||||
demographic.name.lowercase(
|
||||
Locale.US
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is DemographicList -> {
|
||||
filter.state.filter { demographic -> demographic.state }
|
||||
.forEach { demographic ->
|
||||
demographicList.add(demographic.name.lowercase(Locale.US))
|
||||
}
|
||||
}
|
||||
is StatusList -> {
|
||||
filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
addQueryParameter(
|
||||
"status[]",
|
||||
status.name.lowercase(
|
||||
Locale.US
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is StatusList -> {
|
||||
filter.state.filter { status -> status.state }
|
||||
.forEach { status ->
|
||||
statusList.add(status.name.lowercase(Locale.US))
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
if (filter.state != null) {
|
||||
if (filter.state!!.index != 0) {
|
||||
val query = sortableList[filter.state!!.index].second
|
||||
val value = when (filter.state!!.ascending) {
|
||||
true -> "asc"
|
||||
false -> "desc"
|
||||
}
|
||||
addQueryParameter("order[$query]", value)
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
if (filter.state != null && filter.state!!.index != 0) {
|
||||
val query = sortableList[filter.state!!.index].second
|
||||
val value = when (filter.state!!.ascending) {
|
||||
true -> "asc"
|
||||
false -> "desc"
|
||||
}
|
||||
}
|
||||
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)
|
||||
)
|
||||
queryMap["order[$query]"] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if (false) { // preferencesHelper.showR18Filter().not()) {
|
||||
addQueryParameter("contentRating[]", "safe")
|
||||
is TagList -> {
|
||||
filter.state.forEach { tag ->
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -1,45 +1,24 @@
|
||||
package exh.md.handlers
|
||||
|
||||
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.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.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.md.handlers.serializers.MangaListResponse
|
||||
import exh.md.handlers.serializers.MangaResponse
|
||||
import exh.md.handlers.serializers.MangaStatusListResponse
|
||||
import exh.md.handlers.serializers.MangaStatusResponse
|
||||
import exh.md.handlers.serializers.ResultResponse
|
||||
import exh.md.handlers.serializers.UpdateReadingStatus
|
||||
import exh.md.dto.MangaDto
|
||||
import exh.md.dto.ReadingStatusDto
|
||||
import exh.md.service.MangaDexAuthService
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.md.utils.mdListCall
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.util.under
|
||||
import kotlinx.serialization.encodeToString
|
||||
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
|
||||
import kotlinx.coroutines.async
|
||||
|
||||
class FollowsHandler(
|
||||
private val client: OkHttpClient,
|
||||
private val headers: Headers,
|
||||
private val preferences: PreferencesHelper,
|
||||
private val lang: String,
|
||||
private val mdList: MdList
|
||||
private val service: MangaDexAuthService
|
||||
) {
|
||||
|
||||
/**
|
||||
@ -47,21 +26,15 @@ class FollowsHandler(
|
||||
*/
|
||||
suspend fun fetchFollows(page: Int): MetadataMangasPage {
|
||||
return withIOContext {
|
||||
val response = client.newCall(followsListRequest(MdUtil.mangaLimit * page - 1)).await()
|
||||
if (response.code == 204) {
|
||||
val follows = service.userFollowList(MdUtil.mangaLimit * page)
|
||||
|
||||
if (follows.results.isEmpty()) {
|
||||
return@withIOContext MetadataMangasPage(emptyList(), false, emptyList())
|
||||
}
|
||||
|
||||
val mangaListResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
|
||||
|
||||
if (mangaListResponse.results.isEmpty()) {
|
||||
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)
|
||||
val hasMoreResults = follows.limit + follows.offset under follows.total
|
||||
val statusListResponse = service.readingStatusAllManga()
|
||||
val results = followsParseMangaPage(follows.results, statusListResponse.statuses)
|
||||
|
||||
MetadataMangasPage(results.map { it.first }, hasMoreResults, results.map { it.second })
|
||||
}
|
||||
@ -71,53 +44,23 @@ class FollowsHandler(
|
||||
* Parse follows api to manga page
|
||||
* 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 }
|
||||
.thenBy { it.first.title }
|
||||
|
||||
val coverMap = MdUtil.getCoversFromMangaList(response, client)
|
||||
|
||||
return response.map {
|
||||
MdUtil.createMangaEntry(
|
||||
it,
|
||||
lang,
|
||||
coverMap[it.data.id]
|
||||
lang
|
||||
).toSManga() to MangaDexSearchMetadata().apply {
|
||||
followStatus = FollowStatus.fromDex(statuses[it.data.id]).int
|
||||
}
|
||||
}.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
|
||||
*/
|
||||
@ -125,21 +68,17 @@ class FollowsHandler(
|
||||
return withIOContext {
|
||||
val status = when (followStatus == FollowStatus.UNFOLLOWED) {
|
||||
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))
|
||||
|
||||
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"
|
||||
service.updateReadingStatusForManga(mangaId, readingStatusDto).result == "ok"
|
||||
}
|
||||
}
|
||||
|
||||
@ -203,43 +142,26 @@ class FollowsHandler(
|
||||
*/
|
||||
suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
|
||||
return withIOContext {
|
||||
val results = client.mdListCall<MangaResponse> {
|
||||
followsListRequest(it)
|
||||
val results = async {
|
||||
mdListCall {
|
||||
service.userFollowList(it)
|
||||
}
|
||||
}
|
||||
|
||||
val statuses = client.newCall(mangaStatusListRequest()).await()
|
||||
.parseAs<MangaStatusListResponse>().statuses
|
||||
val readingStatusResponse = async { service.readingStatusAllManga().statuses }
|
||||
|
||||
followsParseMangaPage(results, statuses)
|
||||
followsParseMangaPage(results.await(), readingStatusResponse.await())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun fetchTrackingInfo(url: String): Track {
|
||||
return withIOContext {
|
||||
val mangaId = MdUtil.getMangaId(url)
|
||||
val request = GET(
|
||||
MdUtil.mangaUrl + "/" + mangaId,
|
||||
MdUtil.getAuthHeaders(headers, preferences, mdList),
|
||||
CacheControl.FORCE_NETWORK
|
||||
)
|
||||
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)
|
||||
val followStatus = FollowStatus.fromDex(service.readingStatusForManga(mangaId).status)
|
||||
Track.create(TrackManager.MDLIST).apply {
|
||||
status = followStatus.int
|
||||
tracking_url = "${MdUtil.baseUrl}/title/$mangaId"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
package exh.md.handlers
|
||||
|
||||
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.SManga
|
||||
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.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.md.handlers.serializers.ChapterResponse
|
||||
import exh.md.handlers.serializers.GroupListResponse
|
||||
import exh.md.dto.ChapterDto
|
||||
import exh.md.service.MangaDexService
|
||||
import exh.md.utils.MdConstants
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.md.utils.mdListCall
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaHandler(
|
||||
private val client: OkHttpClient,
|
||||
private val headers: Headers,
|
||||
private val lang: String,
|
||||
private val service: MangaDexService,
|
||||
private val apiMangaParser: ApiMangaParser,
|
||||
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 {
|
||||
val response = withIOContext { client.newCall(mangaRequest(manga)).await() }
|
||||
val response = withIOContext { service.viewManga(MdUtil.getMangaId(manga.key)) }
|
||||
return apiMangaParser.parseToManga(manga, response, sourceId)
|
||||
}
|
||||
|
||||
@ -75,8 +42,8 @@ class MangaHandler(
|
||||
|
||||
suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||
return withIOContext {
|
||||
val results = client.mdListCall<ChapterResponse> {
|
||||
mangaFeedRequest(manga, it, lang)
|
||||
val results = mdListCall {
|
||||
service.viewChapters(MdUtil.getMangaId(manga.key), lang, it)
|
||||
}
|
||||
|
||||
val groupMap = getGroupMap(results)
|
||||
@ -85,31 +52,17 @@ class MangaHandler(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGroupMap(results: List<ChapterResponse>): Map<String, String> {
|
||||
val groupIds = results.asSequence()
|
||||
.flatMap { chapter -> chapter.relationships }
|
||||
.filter { it.type == "scanlation_group" }
|
||||
.map { it.id }
|
||||
.toSet()
|
||||
|
||||
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()
|
||||
private fun getGroupMap(results: List<ChapterDto>): Map<String, String> {
|
||||
return results.map { chapter -> chapter.relationships }
|
||||
.flatten()
|
||||
.filter { it.type == MdConstants.Types.scanlator }
|
||||
.map { it.id to it.attributes!!.name!! }
|
||||
.toMap()
|
||||
}
|
||||
|
||||
suspend fun fetchRandomMangaId(): String {
|
||||
return withIOContext {
|
||||
val response = client.newCall(randomMangaRequest()).await()
|
||||
apiMangaParser.randomMangaIdParse(response)
|
||||
service.randomManga().data.id
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,28 +82,4 @@ class MangaHandler(
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import exh.md.handlers.serializers.MangaPlusSerializer
|
||||
import exh.md.dto.MangaPlusSerializer
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
|
@ -2,57 +2,65 @@ package exh.md.handlers
|
||||
|
||||
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.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
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 okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
|
||||
class PageHandler(
|
||||
private val client: OkHttpClient,
|
||||
private val headers: Headers,
|
||||
private val apiChapterParser: ApiChapterParser,
|
||||
private val service: MangaDexService,
|
||||
private val mangaPlusHandler: MangaPlusHandler,
|
||||
private val preferences: PreferencesHelper,
|
||||
private val mdList: MdList,
|
||||
) {
|
||||
|
||||
fun fetchPageList(chapter: SChapter, isLogged: Boolean, usePort443Only: Boolean, dataSaver: Boolean): Observable<List<Page>> {
|
||||
if (chapter.scanlator.equals("MangaPlus")) {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val chapterId = apiChapterParser.externalParse(response)
|
||||
mangaPlusHandler.fetchPageList(chapterId)
|
||||
suspend fun fetchPageList(chapter: SChapter, isLogged: Boolean, usePort443Only: Boolean, dataSaver: Boolean): List<Page> {
|
||||
return withIOContext {
|
||||
val chapterResponse = service.viewChapter(MdUtil.getChapterId(chapter.url))
|
||||
|
||||
if (chapter.scanlator.equals("mangaplus", true)) {
|
||||
mangaPlusHandler.fetchPageList(
|
||||
chapterResponse.data.attributes.data
|
||||
.first()
|
||||
.substringAfterLast("/")
|
||||
)
|
||||
} else {
|
||||
val headers = if (isLogged) {
|
||||
MdUtil.getAuthHeaders(headers, preferences, mdList)
|
||||
} else {
|
||||
headers
|
||||
}
|
||||
}
|
||||
|
||||
val atHomeRequestUrl = if (usePort443Only) {
|
||||
"${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}?forcePort443=true"
|
||||
} else {
|
||||
"${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}"
|
||||
}
|
||||
val (atHomeRequestUrl, atHomeResponse) = service.getAtHomeServer(headers, MdUtil.getChapterId(chapter.url), usePort443Only)
|
||||
|
||||
val headers = if (isLogged) {
|
||||
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)
|
||||
pageListParse(chapterResponse, atHomeRequestUrl, atHomeResponse, dataSaver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pageListRequest(chapter: SChapter): Request {
|
||||
return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK)
|
||||
fun pageListParse(
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -1,65 +1,35 @@
|
||||
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.SManga
|
||||
import exh.md.handlers.serializers.CoverListResponse
|
||||
import exh.md.handlers.serializers.SimilarMangaResponse
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import exh.md.dto.SimilarMangaDto
|
||||
import exh.md.service.MangaDexService
|
||||
import exh.md.service.SimilarService
|
||||
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
|
||||
|
||||
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 {
|
||||
val response = client.newCall(similarMangaRequest(manga)).await()
|
||||
.parseAs<SimilarMangaResponse>()
|
||||
|
||||
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)
|
||||
val similarDto = similarService.getSimilarManga(MdUtil.getMangaId(manga.key))
|
||||
return similarDtoToMangaListPage(similarDto)
|
||||
}
|
||||
|
||||
private fun similarMangaRequest(manga: MangaInfo): Request {
|
||||
val tempUrl = MdUtil.similarBaseApi + MdUtil.getMangaId(manga.key) + ".json"
|
||||
return GET(tempUrl, Headers.Builder().build(), CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
private suspend fun similarDtoToMangaListPage(
|
||||
similarMangaDto: SimilarMangaDto,
|
||||
): MangasPage {
|
||||
val ids = similarMangaDto.matches.map {
|
||||
it.id
|
||||
}
|
||||
|
||||
val mangaList = service.viewMangas(ids).results.map {
|
||||
MdUtil.createMangaEntry(it, lang).toSManga()
|
||||
}
|
||||
|
||||
return MangasPage(mangaList, false)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
@ -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>,
|
||||
)
|
@ -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,
|
||||
)
|
@ -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?
|
||||
)
|
@ -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>
|
||||
)
|
@ -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,
|
||||
)
|
@ -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)
|
@ -2,50 +2,32 @@ package exh.md.network
|
||||
|
||||
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 eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.log.xLogE
|
||||
import exh.log.xLogI
|
||||
import exh.md.handlers.serializers.CheckTokenResponse
|
||||
import exh.md.handlers.serializers.LoginBodyToken
|
||||
import exh.md.handlers.serializers.LoginRequest
|
||||
import exh.md.handlers.serializers.LoginResponse
|
||||
import exh.md.handlers.serializers.RefreshTokenRequest
|
||||
import exh.md.handlers.serializers.ResultResponse
|
||||
import exh.md.dto.LoginRequestDto
|
||||
import exh.md.dto.RefreshTokenDto
|
||||
import exh.md.service.MangaDexAuthService
|
||||
import exh.md.utils.MdUtil
|
||||
import kotlinx.serialization.SerializationException
|
||||
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) {
|
||||
suspend fun isAuthenticated(authHeaders: Headers): Boolean {
|
||||
val response = client.newCall(GET(MdUtil.checkTokenUrl, authHeaders, CacheControl.FORCE_NETWORK)).await()
|
||||
val body = response.parseAs<CheckTokenResponse>(MdUtil.jsonParser)
|
||||
return body.isAuthenticated
|
||||
class MangaDexLoginHelper(val authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
|
||||
val authService by authServiceLazy
|
||||
suspend fun isAuthenticated(): Boolean {
|
||||
return runCatching { authService.checkToken().isAuthenticated }
|
||||
.getOrElse { e ->
|
||||
xLogE("error authenticating", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun refreshToken(authHeaders: Headers): Boolean {
|
||||
suspend fun refreshToken(): Boolean {
|
||||
val refreshToken = MdUtil.refreshToken(preferences, mdList)
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
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 jsonResponse = postResult.parseAs<LoginResponse>(MdUtil.jsonParser)
|
||||
val jsonResponse = authService.refreshToken(RefreshTokenDto(refreshToken))
|
||||
preferences.trackToken(mdList).set(MdUtil.jsonParser.encodeToString(jsonResponse.token))
|
||||
}
|
||||
return refresh.isSuccess
|
||||
@ -54,56 +36,32 @@ class MangaDexLoginHelper(val client: OkHttpClient, val preferences: Preferences
|
||||
suspend fun login(
|
||||
username: String,
|
||||
password: String,
|
||||
): LoginResult {
|
||||
): Boolean {
|
||||
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 postResult = runCatching {
|
||||
client.newCall(
|
||||
POST(
|
||||
MdUtil.loginUrl,
|
||||
Headers.Builder().build(),
|
||||
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()
|
||||
}
|
||||
val loginResponseDto = loginResult.getOrNull()
|
||||
MdUtil.updateLoginToken(
|
||||
loginResponseDto?.token,
|
||||
preferences,
|
||||
mdList
|
||||
)
|
||||
loginResponseDto != null
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LoginResult {
|
||||
data class Failure(val e: Throwable? = null) : LoginResult()
|
||||
data class Success(val token: LoginBodyToken) : LoginResult()
|
||||
}
|
||||
|
||||
suspend fun login(): LoginResult {
|
||||
suspend fun login(): Boolean {
|
||||
val username = preferences.trackUsername(mdList)
|
||||
val password = preferences.trackPassword(mdList)
|
||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
||||
xLogI("No username or password stored, can't login")
|
||||
return LoginResult.Failure()
|
||||
return false
|
||||
}
|
||||
return login(username, password)
|
||||
}
|
||||
|
||||
suspend fun logout(authHeaders: Headers): Boolean {
|
||||
val response = client.newCall(GET(MdUtil.logoutUrl, authHeaders, CacheControl.FORCE_NETWORK)).await()
|
||||
val body = response.parseAs<ResultResponse>(MdUtil.jsonParser)
|
||||
return body.result == "ok"
|
||||
suspend fun logout(): Boolean {
|
||||
return authService.logout().result == "ok"
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,9 @@
|
||||
package exh.md.network
|
||||
|
||||
import exh.log.xLogD
|
||||
import exh.log.xLogI
|
||||
import exh.log.xLogW
|
||||
import exh.md.utils.MdUtil
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
@ -14,6 +11,7 @@ import okhttp3.Route
|
||||
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
xLogI("Detected Auth error ${response.code} on ${response.request.url}")
|
||||
|
||||
val token = refreshToken(loginHelper)
|
||||
return if (token != null) {
|
||||
response.request.newBuilder().header("Authorization", token).build()
|
||||
@ -27,24 +25,7 @@ class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authent
|
||||
var validated = false
|
||||
|
||||
runBlocking {
|
||||
val checkTokenResult = runCatching {
|
||||
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
|
||||
}
|
||||
val checkToken = loginHelper.isAuthenticated()
|
||||
|
||||
if (checkToken) {
|
||||
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()) {
|
||||
this@TokenAuthenticator.xLogI("Token is invalid trying to refresh")
|
||||
val result = runCatching {
|
||||
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)
|
||||
}
|
||||
}
|
||||
validated = loginHelper.refreshToken()
|
||||
}
|
||||
|
||||
if (validated.not()) {
|
||||
this@TokenAuthenticator.xLogI("Did not refresh token, trying to login")
|
||||
val loginResult = loginHelper.login()
|
||||
validated = if (loginResult is MangaDexLoginHelper.LoginResult.Success) {
|
||||
MdUtil.updateLoginToken(
|
||||
loginResult.token,
|
||||
loginHelper.preferences,
|
||||
loginHelper.mdList
|
||||
)
|
||||
true
|
||||
} else false
|
||||
validated = loginHelper.login()
|
||||
}
|
||||
}
|
||||
return when {
|
||||
validated -> "bearer: ${MdUtil.sessionToken(loginHelper.preferences, loginHelper.mdList)!!}"
|
||||
validated -> "Bearer ${MdUtil.sessionToken(loginHelper.preferences, loginHelper.mdList)!!}"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
185
app/src/main/java/exh/md/service/MangaDexAuthService.kt
Normal file
185
app/src/main/java/exh/md/service/MangaDexAuthService.kt
Normal 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)
|
||||
}
|
||||
}
|
107
app/src/main/java/exh/md/service/MangaDexService.kt
Normal file
107
app/src/main/java/exh/md/service/MangaDexService.kt
Normal 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) }
|
||||
}
|
||||
}
|
20
app/src/main/java/exh/md/service/SimilarService.kt
Normal file
20
app/src/main/java/exh/md/service/SimilarService.kt
Normal 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()
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ enum class FollowStatus(val int: Int) {
|
||||
DROPPED(5),
|
||||
RE_READING(6);
|
||||
|
||||
fun toDex(): String = this.name.lowercase(Locale.US)
|
||||
|
||||
companion object {
|
||||
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
|
||||
|
19
app/src/main/java/exh/md/utils/MdApi.kt
Normal file
19
app/src/main/java/exh/md/utils/MdApi.kt
Normal 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"
|
||||
}
|
19
app/src/main/java/exh/md/utils/MdConstants.kt
Normal file
19
app/src/main/java/exh/md/utils/MdConstants.kt
Normal 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
|
||||
}
|
17
app/src/main/java/exh/md/utils/MdExtensions.kt
Normal file
17
app/src/main/java/exh/md/utils/MdExtensions.kt
Normal 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
|
||||
}
|
@ -2,36 +2,26 @@ package exh.md.utils
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import exh.log.xLogD
|
||||
import exh.log.xLogE
|
||||
import exh.md.handlers.serializers.AtHomeResponse
|
||||
import exh.md.handlers.serializers.CoverListResponse
|
||||
import exh.md.handlers.serializers.CoverResponse
|
||||
import exh.md.handlers.serializers.ListCallResponse
|
||||
import exh.md.handlers.serializers.LoginBodyToken
|
||||
import exh.md.handlers.serializers.MangaResponse
|
||||
import exh.md.dto.LoginBodyTokenDto
|
||||
import exh.md.dto.MangaDto
|
||||
import exh.md.network.NoSessionException
|
||||
import exh.source.getMainSource
|
||||
import exh.util.floor
|
||||
import exh.util.nullIfBlank
|
||||
import exh.util.nullIfZero
|
||||
import exh.util.under
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.jsoup.parser.Parser
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@ -45,35 +35,7 @@ class MdUtil {
|
||||
companion object {
|
||||
const val cdnUrl = "https://uploads.mangadex.org"
|
||||
const val baseUrl = "https://mangadex.org"
|
||||
const val apiUrl = "https://api.mangadex.org"
|
||||
const val imageUrlCacheNotFound = "https://cdn.statically.io/img/raw.githubusercontent.com/CarlosEsco/Neko/master/.github/manga_cover_not_found.png"
|
||||
const val atHomeUrl = "$apiUrl/at-home/server"
|
||||
const val coverUrl = "$apiUrl/cover"
|
||||
const val chapterUrl = "$apiUrl/chapter/"
|
||||
const val chapterSuffix = "/chapter/"
|
||||
const val checkTokenUrl = "$apiUrl/auth/check"
|
||||
const val refreshTokenUrl = "$apiUrl/auth/refresh"
|
||||
const val loginUrl = "$apiUrl/auth/login"
|
||||
const val logoutUrl = "$apiUrl/auth/logout"
|
||||
const val groupUrl = "$apiUrl/group"
|
||||
const val authorUrl = "$apiUrl/author"
|
||||
const val randomMangaUrl = "$apiUrl/manga/random"
|
||||
const val mangaUrl = "$apiUrl/manga"
|
||||
const val mangaStatus = "$apiUrl/manga/status"
|
||||
const val userFollows = "$apiUrl/user/follows/manga"
|
||||
fun updateReadingStatusUrl(id: String) = "$apiUrl/manga/$id/status"
|
||||
|
||||
fun mangaFeedUrl(id: String, offset: Int, language: String): String {
|
||||
return "$mangaUrl/$id/feed".toHttpUrl().newBuilder().apply {
|
||||
addQueryParameter("limit", "500")
|
||||
addQueryParameter("offset", offset.toString())
|
||||
addQueryParameter("translatedLanguage[]", language)
|
||||
addQueryParameter("order[volume]", "desc")
|
||||
addQueryParameter("order[chapter]", "desc")
|
||||
}.build().toString()
|
||||
}
|
||||
|
||||
fun coverUrl(mangaId: String, coverId: String) = "$apiUrl/cover?manga[]=$mangaId&ids[]=$coverId"
|
||||
|
||||
const val similarCacheMapping = "https://api.similarmanga.com/mapping/mdex2search.csv"
|
||||
const val similarCacheMangas = "https://api.similarmanga.com/manga/"
|
||||
@ -83,7 +45,7 @@ class MdUtil {
|
||||
const val reportUrl = "https://api.mangadex.network/report"
|
||||
|
||||
const val mdAtHomeTokenLifespan = 10 * 60 * 1000
|
||||
const val mangaLimit = 25
|
||||
const val mangaLimit = 20
|
||||
|
||||
/**
|
||||
* Get the manga offset pages are 1 based, so subtract 1
|
||||
@ -217,10 +179,6 @@ class MdUtil {
|
||||
return "/manga/$mangaUuid"
|
||||
}
|
||||
|
||||
fun formThumbUrl(mangaUrl: String): String {
|
||||
return "https://coverapi.orell.dev/api/v1/mdaltimage/manga/${getMangaId(mangaUrl)}/cover"
|
||||
}
|
||||
|
||||
// Get the ID from the manga url
|
||||
fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/")
|
||||
|
||||
@ -298,76 +256,33 @@ class MdUtil {
|
||||
return null
|
||||
}
|
||||
|
||||
fun atHomeUrlHostUrl(requestUrl: String, client: OkHttpClient, headers: Headers, cacheControl: CacheControl): String {
|
||||
val atHomeRequest = GET(requestUrl, headers, cache = cacheControl)
|
||||
val atHomeResponse = client.newCall(atHomeRequest).execute()
|
||||
return jsonParser.decodeFromString<AtHomeResponse>(atHomeResponse.body!!.string()).baseUrl
|
||||
}
|
||||
|
||||
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
fun parseDate(dateAsString: String): Long =
|
||||
dateFormatter.parse(dateAsString)?.time ?: 0
|
||||
|
||||
fun createMangaEntry(json: MangaResponse, lang: String, coverUrl: String?): MangaInfo {
|
||||
fun createMangaEntry(json: MangaDto, lang: String): MangaInfo {
|
||||
return MangaInfo(
|
||||
key = buildMangaUrl(json.data.id),
|
||||
title = cleanString(json.data.attributes.title[lang] ?: json.data.attributes.title["en"]!!),
|
||||
cover = coverUrl.orEmpty()
|
||||
cover = json.relationships
|
||||
.firstOrNull { relationshipDto -> relationshipDto.type == MdConstants.Types.coverArt }
|
||||
?.attributes
|
||||
?.fileName
|
||||
?.let { coverFileName ->
|
||||
cdnCoverUrl(json.data.id, coverFileName)
|
||||
}.orEmpty()
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getCoverUrl(dexId: String, coverId: String?, client: OkHttpClient): String {
|
||||
coverId ?: return ""
|
||||
val coverResponse = client.newCall(GET("$coverUrl/$coverId"))
|
||||
.await().parseAs<CoverResponse>()
|
||||
val fileName = coverResponse.data.attributes.fileName
|
||||
fun cdnCoverUrl(dexId: String, fileName: String): String {
|
||||
return "$cdnUrl/covers/$dexId/$fileName"
|
||||
}
|
||||
|
||||
suspend fun getCoversFromMangaList(mangaResponseList: List<MangaResponse>, client: OkHttpClient): Map<String, String> {
|
||||
val idsAndCoverIds = mangaResponseList.mapNotNull { mangaResponse ->
|
||||
val mangaId = mangaResponse.data.id
|
||||
val coverId = mangaResponse.relationships.firstOrNull { relationship ->
|
||||
relationship.type.equals("cover_art", true)
|
||||
}?.id
|
||||
if (coverId == null) {
|
||||
null
|
||||
} else {
|
||||
Pair(mangaId, coverId)
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
return runCatching {
|
||||
getBatchCoverUrls(idsAndCoverIds, client)
|
||||
}.getOrNull()!!
|
||||
}
|
||||
|
||||
private suspend fun getBatchCoverUrls(ids: Map<String, String>, client: OkHttpClient): Map<String, String> {
|
||||
try {
|
||||
val url = coverUrl.toHttpUrl().newBuilder().apply {
|
||||
ids.values.forEach { coverArtId ->
|
||||
addQueryParameter("ids[]", coverArtId)
|
||||
}
|
||||
addQueryParameter("limit", ids.size.toString())
|
||||
}.build().toString()
|
||||
val coverList = client.newCall(GET(url)).await().parseAs<CoverListResponse>(jsonParser)
|
||||
return coverList.results.map { coverResponse ->
|
||||
val fileName = coverResponse.data.attributes.fileName
|
||||
val mangaId = coverResponse.relationships.first { it.type.equals("manga", true) }.id
|
||||
val thumbnailUrl = "$cdnUrl/covers/$mangaId/$fileName"
|
||||
Pair(mangaId, thumbnailUrl)
|
||||
}.toMap()
|
||||
} catch (e: Exception) {
|
||||
xLogE("Error getting covers", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
fun getLoginBody(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
|
||||
try {
|
||||
jsonParser.decodeFromString<LoginBodyToken>(it)
|
||||
jsonParser.decodeFromString<LoginBodyTokenDto>(it)
|
||||
} catch (e: SerializationException) {
|
||||
xLogD("Unable to load login body")
|
||||
null
|
||||
@ -378,8 +293,10 @@ class MdUtil {
|
||||
|
||||
fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = getLoginBody(preferences, mdList)?.refresh
|
||||
|
||||
fun updateLoginToken(token: LoginBodyToken, preferences: PreferencesHelper, mdList: MdList) {
|
||||
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
|
||||
fun updateLoginToken(token: LoginBodyTokenDto?, preferences: PreferencesHelper, mdList: MdList) {
|
||||
if (token != null) {
|
||||
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
|
||||
} else preferences.trackToken(mdList).delete()
|
||||
}
|
||||
|
||||
fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) =
|
||||
@ -400,27 +317,17 @@ class MdUtil {
|
||||
val disabledSourceIds = preferences.disabledSources().get()
|
||||
|
||||
return sourceManager.getVisibleOnlineSources()
|
||||
.asSequence()
|
||||
.map { it.getMainSource() }
|
||||
.filterIsInstance<MangaDex>()
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in disabledSourceIds }
|
||||
.toList()
|
||||
}
|
||||
|
||||
inline fun <reified T> encodeToBody(body: T): RequestBody {
|
||||
return jsonParser.encodeToString(body)
|
||||
.toRequestBody("application/json".toMediaType())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <reified T> OkHttpClient.mdListCall(request: (offset: Int) -> Request): List<T> {
|
||||
val results = mutableListOf<T>()
|
||||
var offset = 0
|
||||
|
||||
do {
|
||||
val response = newCall(request(offset)).await()
|
||||
if (response.code == 204) {
|
||||
break
|
||||
}
|
||||
val mangaListResponse = response.parseAs<ListCallResponse<T>>(MdUtil.jsonParser)
|
||||
results += mangaListResponse.results
|
||||
offset += mangaListResponse.limit
|
||||
} while (offset under mangaListResponse.total)
|
||||
|
||||
return results
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user