diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 05782fdd1..bf0081ea2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -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, + MetadataSource, // 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 = @@ -152,7 +160,7 @@ class MangaDex(delegate: HttpSource, val context: Context) : } override fun fetchPageList(chapter: SChapter): Observable> { - return pageHandler.fetchPageList(chapter, isLogged(), usePort443Only(), dataSaver()) + return runAsObservable({ pageHandler.fetchPageList(chapter, isLogged(), usePort443Only(), dataSaver()) }) } override fun fetchImage(page: Page): Observable { @@ -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) { diff --git a/app/src/main/java/exh/md/dto/AtHomeDto.kt b/app/src/main/java/exh/md/dto/AtHomeDto.kt new file mode 100644 index 000000000..a215e2b12 --- /dev/null +++ b/app/src/main/java/exh/md/dto/AtHomeDto.kt @@ -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, +) diff --git a/app/src/main/java/exh/md/dto/AuthDto.kt b/app/src/main/java/exh/md/dto/AuthDto.kt new file mode 100644 index 000000000..85789b371 --- /dev/null +++ b/app/src/main/java/exh/md/dto/AuthDto.kt @@ -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) diff --git a/app/src/main/java/exh/md/dto/ChapterDto.kt b/app/src/main/java/exh/md/dto/ChapterDto.kt new file mode 100644 index 000000000..5f40b2885 --- /dev/null +++ b/app/src/main/java/exh/md/dto/ChapterDto.kt @@ -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, +) : ListCallDto + +@Serializable +data class ChapterDto( + val result: String, + val data: ChapterDataDto, + val relationships: List, +) + +@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, + val dataSaver: List, + val hash: String, +) + +@Serializable +data class GroupListDto( + val limit: Int, + val offset: Int, + val total: Int, + val results: List, +) + +@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, +) diff --git a/app/src/main/java/exh/md/dto/ListCallDto.kt b/app/src/main/java/exh/md/dto/ListCallDto.kt new file mode 100644 index 000000000..073a6d814 --- /dev/null +++ b/app/src/main/java/exh/md/dto/ListCallDto.kt @@ -0,0 +1,8 @@ +package exh.md.dto + +interface ListCallDto { + val limit: Int + val offset: Int + val total: Int + val results: List +} diff --git a/app/src/main/java/exh/md/dto/MangaDto.kt b/app/src/main/java/exh/md/dto/MangaDto.kt new file mode 100644 index 000000000..165a8b412 --- /dev/null +++ b/app/src/main/java/exh/md/dto/MangaDto.kt @@ -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, +) : ListCallDto + +@Serializable +data class MangaDto( + val result: String, + val data: MangaDataDto, + val relationships: List, +) + +@Serializable +data class MangaDataDto(val id: String, val type: String, val attributes: MangaAttributesDto) + +@Serializable +data class MangaAttributesDto( + val title: Map, + val altTitles: List>, + val description: Map, + val links: Map?, + val originalLanguage: String, + val lastVolume: String?, + val lastChapter: String?, + val contentRating: String?, + val publicationDemographic: String?, + val status: String?, + val year: Int?, + val tags: List, +) + +@Serializable +data class TagDto( + val id: String, + val attributes: TagAttributesDto +) + +@Serializable +data class TagAttributesDto( + val name: Map +) + +@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, +) + +@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, +) + +@Serializable +data class ReadChapterDto( + val data: List, +) + +@Serializable +data class CoverListDto( + val results: List, +) + +@Serializable +data class CoverDto( + val data: CoverDataDto, + val relationships: List, +) + +@Serializable +data class CoverDataDto( + val attributes: CoverAttributesDto, +) + +@Serializable +data class CoverAttributesDto( + val fileName: String, +) diff --git a/app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt b/app/src/main/java/exh/md/dto/MangaPlusDto.kt similarity index 99% rename from app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt rename to app/src/main/java/exh/md/dto/MangaPlusDto.kt index ca5d8533e..c19131a41 100644 --- a/app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt +++ b/app/src/main/java/exh/md/dto/MangaPlusDto.kt @@ -1,4 +1,4 @@ -package exh.md.handlers.serializers +package exh.md.dto import kotlinx.serialization.Serializable import kotlinx.serialization.Serializer diff --git a/app/src/main/java/exh/md/dto/ResultDto.kt b/app/src/main/java/exh/md/dto/ResultDto.kt new file mode 100644 index 000000000..984fc4e69 --- /dev/null +++ b/app/src/main/java/exh/md/dto/ResultDto.kt @@ -0,0 +1,8 @@ +package exh.md.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ResultDto( + val result: String, +) diff --git a/app/src/main/java/exh/md/handlers/serializers/SimilarSerializer.kt b/app/src/main/java/exh/md/dto/SimilarDto.kt similarity index 57% rename from app/src/main/java/exh/md/handlers/serializers/SimilarSerializer.kt rename to app/src/main/java/exh/md/dto/SimilarDto.kt index 0ff07c07b..4914d0639 100644 --- a/app/src/main/java/exh/md/handlers/serializers/SimilarSerializer.kt +++ b/app/src/main/java/exh/md/dto/SimilarDto.kt @@ -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, val contentRating: String, - val matches: List, - val updatedAt: String + val matches: List, + val updatedAt: String, ) @Serializable -data class Matches( +data class SimilarMangaMatchListDto( val id: String, val title: Map, val contentRating: String, - val score: Double + val score: Double, ) diff --git a/app/src/main/java/exh/md/handlers/ApiChapterParser.kt b/app/src/main/java/exh/md/handlers/ApiChapterParser.kt deleted file mode 100644 index 92b427305..000000000 --- a/app/src/main/java/exh/md/handlers/ApiChapterParser.kt +++ /dev/null @@ -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 { - val networkApiChapter = response.parseAs(MdUtil.jsonParser) - - val pages = mutableListOf() - - 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() - val external = chapterResponse.data.attributes.data.first() - return external.substringAfterLast("/") - } -} diff --git a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt index aefaea233..d149b0b84 100644 --- a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt +++ b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt @@ -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(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(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() - .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(MdUtil.jsonParser).data.id - } - - fun chapterListParse(chapterListResponse: List, groupMap: Map): List { + fun chapterListParse(chapterListResponse: List, groupMap: Map): List { val now = System.currentTimeMillis() return chapterListResponse.asSequence() @@ -242,19 +201,14 @@ class ApiMangaParser( } fun chapterParseForMangaId(response: Response): String { - try { - return response.parseAs(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(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, ): ChapterInfo { val attributes = networkChapter.data.attributes diff --git a/app/src/main/java/exh/md/handlers/FilterHandler.kt b/app/src/main/java/exh/md/handlers/FilterHandler.kt index bcc89d3dc..cd378c7e0 100644 --- a/app/src/main/java/exh/md/handlers/FilterHandler.kt +++ b/app/src/main/java/exh/md/handlers/FilterHandler.kt @@ -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("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) : 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 { + val queryMap = mutableMapOf() + + val originalLanguageList = mutableListOf() // originalLanguage[] + val contentRatingList = mutableListOf() // contentRating[] + val demographicList = mutableListOf() // publicationDemographic[] + val statusList = mutableListOf() // status[] + val includeTagList = mutableListOf() // includedTags[] + val excludeTagList = mutableListOf() // 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 } } diff --git a/app/src/main/java/exh/md/handlers/FollowsHandler.kt b/app/src/main/java/exh/md/handlers/FollowsHandler.kt index 42b2dc08b..3a6d3f047 100644 --- a/app/src/main/java/exh/md/handlers/FollowsHandler.kt +++ b/app/src/main/java/exh/md/handlers/FollowsHandler.kt @@ -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(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() - 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, statuses: Map): List> { + private fun followsParseMangaPage( + response: List, + statuses: Map + ): List> { val comparator = compareBy> { 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(MdUtil.jsonParser) - val statusResponse = sResponse.parseAs() - 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(MdUtil.jsonParser) - body.result == "ok" + service.updateReadingStatusForManga(mangaId, readingStatusDto).result == "ok" } } @@ -203,43 +142,26 @@ class FollowsHandler( */ suspend fun fetchAllFollows(): List> { return withIOContext { - val results = client.mdListCall { - followsListRequest(it) + val results = async { + mdListCall { + service.userFollowList(it) + } } - val statuses = client.newCall(mangaStatusListRequest()).await() - .parseAs().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) - } } diff --git a/app/src/main/java/exh/md/handlers/MangaHandler.kt b/app/src/main/java/exh/md/handlers/MangaHandler.kt index 680bd8241..893f77e54 100644 --- a/app/src/main/java/exh/md/handlers/MangaHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaHandler.kt @@ -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> { - 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 { - *//* if (forceLatestCovers) { - val covers = client.newCall(coverRequest(manga)).await().parseAs(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 { return withIOContext { - val results = client.mdListCall { - 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): Map { - 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(MdUtil.jsonParser) - .results.map { group -> group.data.id to group.data.attributes.name } - } else { - emptyList() - } - }.toMap() - }.getOrNull().orEmpty() + private fun getGroupMap(results: List): Map { + 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, 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 { - } } diff --git a/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt b/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt index b3af76263..f7778c5ec 100644 --- a/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaPlusHandler.kt @@ -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 diff --git a/app/src/main/java/exh/md/handlers/PageHandler.kt b/app/src/main/java/exh/md/handlers/PageHandler.kt index ab6b53d00..328dc973c 100644 --- a/app/src/main/java/exh/md/handlers/PageHandler.kt +++ b/app/src/main/java/exh/md/handlers/PageHandler.kt @@ -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> { - 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 { + 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 { + 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 } } diff --git a/app/src/main/java/exh/md/handlers/PopularHandler.kt b/app/src/main/java/exh/md/handlers/PopularHandler.kt deleted file mode 100644 index dfdf32e53..000000000 --- a/app/src/main/java/exh/md/handlers/PopularHandler.kt +++ /dev/null @@ -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 { - 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(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) - } -} diff --git a/app/src/main/java/exh/md/handlers/SearchHandler.kt b/app/src/main/java/exh/md/handlers/SearchHandler.kt deleted file mode 100644 index f95f7fc57..000000000 --- a/app/src/main/java/exh/md/handlers/SearchHandler.kt +++ /dev/null @@ -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 { - 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(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(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() - } -} diff --git a/app/src/main/java/exh/md/handlers/SimilarHandler.kt b/app/src/main/java/exh/md/handlers/SimilarHandler.kt index f8bcc04d7..c4f79527f 100644 --- a/app/src/main/java/exh/md/handlers/SimilarHandler.kt +++ b/app/src/main/java/exh/md/handlers/SimilarHandler.kt @@ -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() - - 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() - - 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): 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) } } diff --git a/app/src/main/java/exh/md/handlers/serializers/Auth.kt b/app/src/main/java/exh/md/handlers/serializers/Auth.kt deleted file mode 100644 index 2692707f3..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/Auth.kt +++ /dev/null @@ -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) diff --git a/app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt deleted file mode 100644 index 685f83ad8..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt +++ /dev/null @@ -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, - val content: List, - val format: List, - val genre: List, - val theme: List, - val languages: List, - val related: List, - val external: MutableMap, - val last_updated: String, - val matches: List, -) - -@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, -) diff --git a/app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt deleted file mode 100644 index d1412fddd..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt +++ /dev/null @@ -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 -) - -@Serializable -data class ChapterResponse( - val result: String, - val data: NetworkChapter, - val relationships: List -) - -@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, - val dataSaver: List, - 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 -) - -@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, -) diff --git a/app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt deleted file mode 100644 index 4ce0f1cae..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt +++ /dev/null @@ -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? -) diff --git a/app/src/main/java/exh/md/handlers/serializers/ListCallSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ListCallSerializer.kt deleted file mode 100644 index a3f0d8b31..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/ListCallSerializer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package exh.md.handlers.serializers - -import kotlinx.serialization.Serializable - -@Serializable -data class ListCallResponse( - val limit: Int, - val offset: Int, - val total: Int, - val results: List -) diff --git a/app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt deleted file mode 100644 index ecdbf6671..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt +++ /dev/null @@ -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 -) - -@Serializable -data class MangaResponse( - val result: String, - val data: NetworkManga, - val relationships: List -) - -@Serializable -data class NetworkManga(val id: String, val type: String, val attributes: NetworkMangaAttributes) - -@Serializable -data class NetworkMangaAttributes( - val title: Map, - val altTitles: List>, - val description: Map, - val links: Map?, - val originalLanguage: String, - val lastVolume: String?, - val lastChapter: String?, - val contentRating: String?, - val publicationDemographic: String?, - val status: String?, - val year: Int?, - val tags: List -) - -@Serializable -data class TagsSerializer( - val id: String, - val attributes: TagAttributes -) - -@Serializable -data class TagAttributes( - val name: Map -) - -@Serializable -data class Relationships( - val id: String, - val type: String, -) - -@Serializable -data class AuthorResponseList( - val results: List, -) - -@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 -) - -@Serializable -data class CoverListResponse( - val results: List, -) - -@Serializable -data class CoverResponse( - val data: Cover, - val relationships: List -) - -@Serializable -data class Cover( - val attributes: CoverAttributes, -) - -@Serializable -data class CoverAttributes( - val fileName: String, -) diff --git a/app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt b/app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt deleted file mode 100644 index 222b6f6d7..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt +++ /dev/null @@ -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? = null -) - -@Serializable -data class FollowedSerializer(val mangaId: String, val mangaTitle: String, val followType: Int) diff --git a/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt b/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt index 1b4afad2a..28431855d 100644 --- a/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt +++ b/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt @@ -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(MdUtil.jsonParser) - return body.isAuthenticated +class MangaDexLoginHelper(val authServiceLazy: Lazy, 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(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(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(MdUtil.jsonParser) - return body.result == "ok" + suspend fun logout(): Boolean { + return authService.logout().result == "ok" } } diff --git a/app/src/main/java/exh/md/network/TokenAuthenticator.kt b/app/src/main/java/exh/md/network/TokenAuthenticator.kt index 698657447..9279d6780 100644 --- a/app/src/main/java/exh/md/network/TokenAuthenticator.kt +++ b/app/src/main/java/exh/md/network/TokenAuthenticator.kt @@ -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 } } diff --git a/app/src/main/java/exh/md/service/MangaDexAuthService.kt b/app/src/main/java/exh/md/service/MangaDexAuthService.kt new file mode 100644 index 000000000..3d4ff3cdb --- /dev/null +++ b/app/src/main/java/exh/md/service/MangaDexAuthService.kt @@ -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) + } +} diff --git a/app/src/main/java/exh/md/service/MangaDexService.kt b/app/src/main/java/exh/md/service/MangaDexService.kt new file mode 100644 index 000000000..d72e33df8 --- /dev/null +++ b/app/src/main/java/exh/md/service/MangaDexService.kt @@ -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 + ): 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 { + 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) } + } +} diff --git a/app/src/main/java/exh/md/service/SimilarService.kt b/app/src/main/java/exh/md/service/SimilarService.kt new file mode 100644 index 000000000..f24b26779 --- /dev/null +++ b/app/src/main/java/exh/md/service/SimilarService.kt @@ -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() + } +} diff --git a/app/src/main/java/exh/md/utils/FollowStatus.kt b/app/src/main/java/exh/md/utils/FollowStatus.kt index 43542e9c0..5fc99c923 100644 --- a/app/src/main/java/exh/md/utils/FollowStatus.kt +++ b/app/src/main/java/exh/md/utils/FollowStatus.kt @@ -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 diff --git a/app/src/main/java/exh/md/utils/MdApi.kt b/app/src/main/java/exh/md/utils/MdApi.kt new file mode 100644 index 000000000..e02144a5b --- /dev/null +++ b/app/src/main/java/exh/md/utils/MdApi.kt @@ -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" +} diff --git a/app/src/main/java/exh/md/utils/MdConstants.kt b/app/src/main/java/exh/md/utils/MdConstants.kt new file mode 100644 index 000000000..e52adb727 --- /dev/null +++ b/app/src/main/java/exh/md/utils/MdConstants.kt @@ -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 +} diff --git a/app/src/main/java/exh/md/utils/MdExtensions.kt b/app/src/main/java/exh/md/utils/MdExtensions.kt new file mode 100644 index 000000000..f7bb417a0 --- /dev/null +++ b/app/src/main/java/exh/md/utils/MdExtensions.kt @@ -0,0 +1,17 @@ +package exh.md.utils + +import exh.md.dto.ListCallDto +import exh.util.under + +suspend fun mdListCall(request: suspend (offset: Int) -> ListCallDto): List { + val results = mutableListOf() + var offset = 0 + + do { + val list = request(offset) + results += list.results + offset += list.limit + } while (offset under list.total) + + return results +} diff --git a/app/src/main/java/exh/md/utils/MdUtil.kt b/app/src/main/java/exh/md/utils/MdUtil.kt index 9f3768dac..12e34053c 100644 --- a/app/src/main/java/exh/md/utils/MdUtil.kt +++ b/app/src/main/java/exh/md/utils/MdUtil.kt @@ -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.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() - val fileName = coverResponse.data.attributes.fileName + fun cdnCoverUrl(dexId: String, fileName: String): String { return "$cdnUrl/covers/$dexId/$fileName" } - suspend fun getCoversFromMangaList(mangaResponseList: List, client: OkHttpClient): Map { - 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, client: OkHttpClient): Map { - 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(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(it) + jsonParser.decodeFromString(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() .filter { it.lang in languages } .filterNot { it.id.toString() in disabledSourceIds } + .toList() + } + + inline fun encodeToBody(body: T): RequestBody { + return jsonParser.encodeToString(body) + .toRequestBody("application/json".toMediaType()) } } } - -suspend inline fun OkHttpClient.mdListCall(request: (offset: Int) -> Request): List { - val results = mutableListOf() - var offset = 0 - - do { - val response = newCall(request(offset)).await() - if (response.code == 204) { - break - } - val mangaListResponse = response.parseAs>(MdUtil.jsonParser) - results += mangaListResponse.results - offset += mangaListResponse.limit - } while (offset under mangaListResponse.total) - - return results -}