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 27465a5e8..b00ea6bc1 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 @@ -165,7 +165,7 @@ class MangaDex(delegate: HttpSource, val context: Context) : } override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { - ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input, preferences.mangaDexForceLatestCovers().get()) + ApiMangaParser(listOf(mdLang)).parseIntoMetadata(metadata, input, emptyList()) } override suspend fun fetchFollows(): MangasPage { diff --git a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt index 42753ac45..8bffb9c3f 100644 --- a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt +++ b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt @@ -47,7 +47,7 @@ class ApiMangaParser(private val langs: List) { * * Will also save the metadata to the DB if possible */ - fun parseToManga(manga: SManga, input: Response, forceLatestCover: Boolean): Completable { + fun parseToManga(manga: SManga, input: Response, coverUrls: List): Completable { val mangaId = (manga as? Manga)?.id val metaObservable = if (mangaId != null) { // We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions @@ -61,7 +61,7 @@ class ApiMangaParser(private val langs: List) { } return metaObservable.map { - parseIntoMetadata(it, input, forceLatestCover) + parseIntoMetadata(it, input, coverUrls) it.copyTo(manga) it }.flatMapCompletable { @@ -72,14 +72,14 @@ class ApiMangaParser(private val langs: List) { } } - suspend fun parseToManga(manga: MangaInfo, input: Response, forceLatestCover: Boolean, sourceId: Long): MangaInfo { + suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List, sourceId: Long): MangaInfo { val mangaId = db.getManga(manga.key, sourceId).await()?.id val metadata = if (mangaId != null) { val flatMetadata = db.getFlatMetadataForManga(mangaId).await() flatMetadata?.raise(metaClass) ?: newMetaInstance() } else newMetaInstance() - parseInfoIntoMetadata(metadata, input, forceLatestCover) + parseInfoIntoMetadata(metadata, input, coverUrls) if (mangaId != null) { metadata.mangaId = mangaId db.insertFlatMetadata(metadata.flatten()).await() @@ -88,28 +88,26 @@ class ApiMangaParser(private val langs: List) { return metadata.createMangaInfo(manga) } - fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) = parseIntoMetadata(metadata, input, forceLatestCover) + fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List) = parseIntoMetadata(metadata, input, coverUrls) - fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) { + fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List) { with(metadata) { try { val networkApiManga = MdUtil.jsonParser.decodeFromString(input.body!!.string()) - val networkManga = networkApiManga.manga + val networkManga = networkApiManga.data.manga mdId = MdUtil.getMangaId(input.request.url.toString()) mdUrl = input.request.url.toString() title = MdUtil.cleanString(networkManga.title) - val coverList = networkManga.covers - thumbnail_url = MdUtil.cdnUrl + - if (forceLatestCover && coverList.isNotEmpty()) { - coverList.last() - } else { - MdUtil.removeTimeParamUrl(networkManga.cover_url) - } + thumbnail_url = if (coverUrls.isNotEmpty()) { + coverUrls.last() + } else { + networkManga.mainCover + } description = MdUtil.cleanDescription(networkManga.description) - author = MdUtil.cleanString(networkManga.author) - artist = MdUtil.cleanString(networkManga.artist) - lang_flag = networkManga.lang_flag - last_chapter_number = networkManga.last_chapter?.toFloatOrNull()?.floor() + author = MdUtil.cleanString(networkManga.author.joinToString()) + artist = MdUtil.cleanString(networkManga.artist.joinToString()) + lang_flag = networkManga.publication?.language + last_chapter_number = networkManga.lastChapter?.toFloatOrNull()?.floor() networkManga.rating?.let { rating = it.bayesian ?: it.mean @@ -124,7 +122,7 @@ class ApiMangaParser(private val langs: List) { } val filteredChapters = filterChapterForChecking(networkApiManga) - val tempStatus = parseStatus(networkManga.status) + val tempStatus = parseStatus(networkManga.publication!!.status) val publishedOrCancelled = tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) { @@ -134,17 +132,19 @@ class ApiMangaParser(private val langs: List) { status = tempStatus } - val demographic = FilterHandler.demographics().filter { it.id == networkManga.demographic }.firstOrNull() - val genres = - networkManga.genres.mapNotNull { FilterHandler.allTypes[it.toString()] } + networkManga.tags.mapNotNull { FilterHandler.allTypes[it.toString()] } .toMutableList() - if (demographic != null) { - genres.add(0, demographic.name) + networkManga.publication.demographic?.let { demographicInt -> + val demographic = FilterHandler.demographics().firstOrNull { it.id.toInt() == demographicInt } + + if (demographic != null) { + genres.add(0, demographic.name) + } } - if (networkManga.hentai == 1) { + if (networkManga.isHentai) { genres.add("Hentai") } @@ -163,41 +163,39 @@ class ApiMangaParser(private val langs: List) { */ private fun isMangaCompleted( serializer: ApiMangaSerializer, - filteredChapters: List> + filteredChapters: List ): Boolean { - if (filteredChapters.isEmpty() || serializer.manga.last_chapter.isNullOrEmpty()) { + val finalChapterNumber = serializer.data.manga.lastChapter + if (filteredChapters.isEmpty() || finalChapterNumber.isNullOrEmpty()) { return false } // just to fix the stupid lint - val lastMangaChapter: String? = serializer.manga.last_chapter - val finalChapterNumber = lastMangaChapter!! if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) { filteredChapters.firstOrNull()?.let { - if (isOneShot(it.value, finalChapterNumber)) { + if (isOneShot(it, finalChapterNumber)) { return true } } } val removeOneshots = filteredChapters.asSequence() - .map { it.value.chapter?.toDoubleOrNull()?.floor()?.nullIfZero() } + .map { it.chapter?.toDoubleOrNull()?.floor()?.nullIfZero() } .filterNotNull() .toList().distinctBy { it } return removeOneshots.toList().size == finalChapterNumber.toDouble().floor() } - private fun filterChapterForChecking(serializer: ApiMangaSerializer): List> { - serializer.chapter ?: return emptyList() - return serializer.chapter.entries - .filter { langs.contains(it.value.lang_code) } + private fun filterChapterForChecking(serializer: ApiMangaSerializer): List { + return serializer.data.chapters.asSequence() + .filter { langs.contains(it.language) } .filter { - it.value.chapter?.let { chapterNumber -> + it.chapter?.let { chapterNumber -> if (chapterNumber.toDoubleOrNull() == null) { return@filter false } return@filter true } return@filter false - }.distinctBy { it.value.chapter } + }.toList() } private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean { @@ -230,24 +228,20 @@ class ApiMangaParser(private val langs: List) { fun chapterListParse(jsonData: String): List { val now = System.currentTimeMillis() val networkApiManga = MdUtil.jsonParser.decodeFromString(jsonData) - val networkManga = networkApiManga.manga - val networkChapters = networkApiManga.chapter - if (networkChapters.isNullOrEmpty()) { - return listOf() - } - val status = networkManga.status + val networkManga = networkApiManga.data.manga + val networkChapters = networkApiManga.data.chapters + val groups = networkApiManga.data.groups - val finalChapterNumber = networkManga.last_chapter!! + val status = networkManga.publication!!.status - val chapters = mutableListOf() + val finalChapterNumber = networkManga.lastChapter // Skip chapters that don't match the desired language, or are future releases val chapLangs = MdLang.values().filter { langs.contains(it.dexLang) } - networkChapters.filter { langs.contains(it.value.lang_code) && (it.value.timestamp * 1000) <= now } - .mapTo(chapters) { mapChapter(it.key, it.value, finalChapterNumber, status, chapLangs, networkChapters.size) } - - return chapters + return networkChapters.asSequence() + .filter { langs.contains(it.language) && (it.timestamp * 1000) <= now } + .map { mapChapter(it, finalChapterNumber, status, chapLangs, networkChapters.size, groups) }.toList() } fun chapterParseForMangaId(response: Response): Int { @@ -267,15 +261,15 @@ class ApiMangaParser(private val langs: List) { } private fun mapChapter( - chapterId: String, networkChapter: ChapterSerializer, - finalChapterNumber: String, + finalChapterNumber: String?, status: Int, chapLangs: List, - totalChapterCount: Int + totalChapterCount: Int, + groups: Map ): SChapter { val chapter = SChapter.create() - chapter.url = MdUtil.apiChapter + chapterId + chapter.url = MdUtil.apiChapter + networkChapter.id val chapterName = mutableListOf() // Build chapter name @@ -305,10 +299,12 @@ class ApiMangaParser(private val langs: List) { chapterName.add("Oneshot") } if ((status == 2 || status == 3)) { - if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) || - networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0 - ) { - chapterName.add("[END]") + if (finalChapterNumber != null) { + if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) || + networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0 + ) { + chapterName.add("[END]") + } } } @@ -317,21 +313,13 @@ class ApiMangaParser(private val langs: List) { chapter.date_upload = networkChapter.timestamp * 1000 val scanlatorName = mutableSetOf() - networkChapter.group_name?.let { - scanlatorName.add(it) - } - networkChapter.group_name_2?.let { - scanlatorName.add(it) - } - networkChapter.group_name_3?.let { - scanlatorName.add(it) - } + networkChapter.groups.mapNotNull { groups[it] }.forEach { scanlatorName.add(it) } chapter.scanlator = MdUtil.cleanString(MdUtil.getScanlatorString(scanlatorName)) // chapter.mangadex_chapter_id = MdUtil.getChapterId(chapter.url) - // chapter.language = chapLangs.firstOrNull { it.dexLang == networkChapter.lang_code }?.name + // chapter.language = chapLangs.firstOrNull { it.dexLang == networkChapter.language }?.name return chapter } diff --git a/app/src/main/java/exh/md/handlers/MangaHandler.kt b/app/src/main/java/exh/md/handlers/MangaHandler.kt index 3a1789f5a..bdc8111cf 100644 --- a/app/src/main/java/exh/md/handlers/MangaHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaHandler.kt @@ -4,9 +4,13 @@ import com.elvishew.xlog.XLog import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess 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 import eu.kanade.tachiyomi.source.model.toSManga +import eu.kanade.tachiyomi.util.lang.runAsObservable +import exh.md.handlers.serializers.ApiCovers import exh.md.utils.MdUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -20,9 +24,10 @@ import tachiyomi.source.model.MangaInfo class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: List, val forceLatestCovers: Boolean = false) { // TODO make use of this - suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair> { + suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair> { return withContext(Dispatchers.IO) { - val response = client.newCall(apiRequest(manga)).await() + val response = client.newCall(apiRequest(manga.toSManga())).await() + val covers = getCovers(manga, forceLatestCovers) val parser = ApiMangaParser(langs) val jsonData = withContext(Dispatchers.IO) { response.body!!.string() } @@ -31,7 +36,7 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li throw Exception("Error from MangaDex Response code ${response.code} ") } - parser.parseToManga(manga, response, forceLatestCovers).await() + parser.parseToManga(manga, response, covers, sourceId) val chapterList = parser.chapterListParse(jsonData) Pair( manga, @@ -40,6 +45,15 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li } } + suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List { + return if (forceLatestCovers) { + val covers = client.newCall(coverRequest(manga.toSManga())).await().parseAs() + covers.data.map { it.url } + } else { + emptyList() + } + } + suspend fun getMangaIdFromChapterId(urlChapterId: String): Int { return withContext(Dispatchers.IO) { val request = GET(MdUtil.baseUrl + MdUtil.apiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK) @@ -48,28 +62,26 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li } } - suspend fun fetchMangaDetails(manga: SManga): SManga { - return withContext(Dispatchers.IO) { - val response = client.newCall(apiRequest(manga)).await() - ApiMangaParser(langs).parseToManga(manga, response, forceLatestCovers).await() - manga.apply { - initialized = true - } - } - } - suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long): MangaInfo { return withContext(Dispatchers.IO) { val response = client.newCall(apiRequest(manga.toSManga())).await() - ApiMangaParser(langs).parseToManga(manga, response, forceLatestCovers, sourceId) + val covers = getCovers(manga, forceLatestCovers) + ApiMangaParser(langs).parseToManga(manga, response, covers, sourceId) } } fun fetchMangaDetailsObservable(manga: SManga): Observable { return client.newCall(apiRequest(manga)) .asObservableSuccess() + .flatMap { response -> + runAsObservable({ + getCovers(manga.toMangaInfo(), forceLatestCovers) + }).map { + response to it + } + } .flatMap { - ApiMangaParser(langs).parseToManga(manga, it, forceLatestCovers).andThen( + ApiMangaParser(langs).parseToManga(manga, it.first, it.second).andThen( Observable.just( manga.apply { initialized = true @@ -114,6 +126,10 @@ class MangaHandler(val client: OkHttpClient, val headers: Headers, val langs: Li } private fun apiRequest(manga: SManga): Request { - return GET(MdUtil.baseUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url), headers, CacheControl.FORCE_NETWORK) + return GET(MdUtil.baseUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url) + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK) + } + + private fun coverRequest(manga: SManga): Request { + return GET(MdUtil.baseUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK) } } diff --git a/app/src/main/java/exh/md/handlers/SearchHandler.kt b/app/src/main/java/exh/md/handlers/SearchHandler.kt index 57aa772cc..a112c349e 100644 --- a/app/src/main/java/exh/md/handlers/SearchHandler.kt +++ b/app/src/main/java/exh/md/handlers/SearchHandler.kt @@ -1,6 +1,5 @@ package exh.md.handlers -import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.source.model.FilterList @@ -17,13 +16,10 @@ import okhttp3.Request import okhttp3.Response import org.jsoup.nodes.Element import rx.Observable -import uy.kohesive.injekt.injectLazy // Unused, kept for reference todo class SearchHandler(val client: OkHttpClient, private val headers: Headers, val langs: List, private val useLowQualityCovers: Boolean) { - private val preferences: PreferencesHelper by injectLazy() - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { return when { query.startsWith(PREFIX_ID_SEARCH) -> { @@ -33,7 +29,7 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val .map { response -> val details = SManga.create() details.url = "/manga/$realQuery/" - ApiMangaParser(langs).parseToManga(details, response, preferences.mangaDexForceLatestCovers().get()).await() + ApiMangaParser(langs).parseToManga(details, response, emptyList()).await() MangasPage(listOf(details), false) } } diff --git a/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt index ac2e27a44..c5c5a9231 100644 --- a/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt +++ b/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt @@ -4,53 +4,39 @@ import kotlinx.serialization.Serializable @Serializable data class ApiMangaSerializer( - val chapter: Map? = null, - val manga: MangaSerializer, + val data: DataSerializer, val status: String ) @Serializable -data class MangaSerializer( - val artist: String, - val author: String, - val cover_url: String, - val description: String, - val demographic: String, - val genres: List, - val covers: List, - val hentai: Int, - val lang_flag: String, - val lang_name: String, - val last_chapter: String? = null, - val links: LinksSerializer? = null, - val rating: RatingSerializer? = null, - val status: Int, - val title: String +data class DataSerializer( + val manga: MangaSerializer, + val chapters: List, + val groups: Map, + ) @Serializable -data class MangaSerializerTwo( +data class MangaSerializer( val artist: List, val author: List, val mainCover: String, val description: String, - val publication: Publication, val tags: List, - // val covers: List, val isHentai: Boolean, - // val lang_flag: String, - // val lang_name: String, val lastChapter: String? = null, + val publication: PublicationSerializer? = null, val links: LinksSerializer? = null, - val rating: RatingSerializerTwo? = null, + val rating: RatingSerializer? = null, val title: String ) @Serializable -data class Publication( - val language: String, +data class PublicationSerializer( + val language: String? = null, val status: Int, - val demographic: Int + val demographic: Int? + ) @Serializable @@ -72,53 +58,13 @@ data class RatingSerializer( val users: String? = null ) -@Serializable -data class RatingSerializerTwo( - val bayesian: Float? = null, - val mean: Float? = null, - val users: Int? = null -) - @Serializable data class ChapterSerializer( - val volume: String? = null, - val chapter: String? = null, - val title: String? = null, - val lang_code: String, - val group_id: Int? = null, - val group_name: String? = null, - val group_id_2: Int? = null, - val group_name_2: String? = null, - val group_id_3: Int? = null, - val group_name_3: String? = null, - val timestamp: Long -) - -@Serializable -data class ChapterSerializerTwo( + val id: Long, val volume: String? = null, val chapter: String? = null, val title: String? = null, val language: String, - val groups: List = emptyList(), + val groups: List, val timestamp: Long ) - -@Serializable -data class GroupSerializer( - val id: Int, - val name: String? = null -) - -@Serializable -data class CoversResult( - val covers: List = emptyList(), - val status: String -) - -@Serializable -data class ImageReportResult( - val url: String, - val success: Boolean, - val bytes: Int? -) diff --git a/app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt new file mode 100644 index 000000000..7db2edc64 --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt @@ -0,0 +1,14 @@ +package exh.md.handlers.serializers + +import kotlinx.serialization.Serializable + +@Serializable +data class ApiCovers( + val data: List, +) + +@Serializable +data class CoversResult( + val volume: String, + val url: String +) diff --git a/app/src/main/java/exh/md/utils/MdUtil.kt b/app/src/main/java/exh/md/utils/MdUtil.kt index 8c4654100..2f89adc0b 100644 --- a/app/src/main/java/exh/md/utils/MdUtil.kt +++ b/app/src/main/java/exh/md/utils/MdUtil.kt @@ -23,13 +23,14 @@ class MdUtil { const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org" const val baseUrl = "https://mangadex.org" const val randMangaPage = "/manga/" - const val apiManga = "/api/manga/" + const val apiManga = "/api/v2/manga/" + const val includeChapters = "?include=chapters" const val apiChapter = "/api/chapter/" const val apiChapterSuffix = "?mark_read=0" const val groupSearchUrl = "$baseUrl/groups/0/1/" const val followsAllApi = "/api/?type=manga_follows" const val followsMangaApi = "/api/?type=manga_follows&manga_id=" - const val coversApi = "/api/index.php?type=covers&id=" + const val apiCovers = "/covers" const val reportUrl = "https://api.mangadex.network/report" const val imageUrl = "$baseUrl/data" diff --git a/app/src/main/res/drawable/manga_info_more_gradient.xml b/app/src/main/res/drawable/manga_info_more_gradient.xml index f3a077d04..eed17e090 100644 --- a/app/src/main/res/drawable/manga_info_more_gradient.xml +++ b/app/src/main/res/drawable/manga_info_more_gradient.xml @@ -4,10 +4,8 @@ - + android:endColor="#00000000" /> - - + \ No newline at end of file