Mangadex api v2 for manga info

(cherry picked from commit 38ec991a15d2eebc7ebd0522f8615c2d8dd7003b)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/source/online/handlers/ApiMangaParser.kt
#	app/src/main/java/eu/kanade/tachiyomi/source/online/handlers/serializers/CoversSerializer.kt
#	app/src/main/java/exh/md/handlers/MangaHandler.kt
#	app/src/main/java/exh/md/handlers/SearchHandler.kt
#	app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt
#	app/src/main/res/drawable/manga_info_more_gradient.xml
This commit is contained in:
Carlos 2020-11-07 20:29:00 -05:00 committed by Jobobby04
parent 68c12d79ee
commit f1a65edd3a
8 changed files with 124 additions and 165 deletions

View File

@ -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 {

View File

@ -47,7 +47,7 @@ class ApiMangaParser(private val langs: List<String>) {
*
* 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<String>): 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<String>) {
}
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<String>) {
}
}
suspend fun parseToManga(manga: MangaInfo, input: Response, forceLatestCover: Boolean, sourceId: Long): MangaInfo {
suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List<String>, 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<String>) {
return metadata.createMangaInfo(manga)
}
fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) = parseIntoMetadata(metadata, input, forceLatestCover)
fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) = parseIntoMetadata(metadata, input, coverUrls)
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, forceLatestCover: Boolean) {
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) {
with(metadata) {
try {
val networkApiManga = MdUtil.jsonParser.decodeFromString<ApiMangaSerializer>(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<String>) {
}
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<String>) {
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<String>) {
*/
private fun isMangaCompleted(
serializer: ApiMangaSerializer,
filteredChapters: List<Map.Entry<String, ChapterSerializer>>
filteredChapters: List<ChapterSerializer>
): 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<Map.Entry<String, ChapterSerializer>> {
serializer.chapter ?: return emptyList()
return serializer.chapter.entries
.filter { langs.contains(it.value.lang_code) }
private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<ChapterSerializer> {
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<String>) {
fun chapterListParse(jsonData: String): List<SChapter> {
val now = System.currentTimeMillis()
val networkApiManga = MdUtil.jsonParser.decodeFromString<ApiMangaSerializer>(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<SChapter>()
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<String>) {
}
private fun mapChapter(
chapterId: String,
networkChapter: ChapterSerializer,
finalChapterNumber: String,
finalChapterNumber: String?,
status: Int,
chapLangs: List<MdLang>,
totalChapterCount: Int
totalChapterCount: Int,
groups: Map<Long, String>
): SChapter {
val chapter = SChapter.create()
chapter.url = MdUtil.apiChapter + chapterId
chapter.url = MdUtil.apiChapter + networkChapter.id
val chapterName = mutableListOf<String>()
// Build chapter name
@ -305,10 +299,12 @@ class ApiMangaParser(private val langs: List<String>) {
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<String>) {
chapter.date_upload = networkChapter.timestamp * 1000
val scanlatorName = mutableSetOf<String>()
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
}

View File

@ -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<String>, val forceLatestCovers: Boolean = false) {
// TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: SManga): Pair<SManga, List<SChapter>> {
suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<SChapter>> {
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<String> {
return if (forceLatestCovers) {
val covers = client.newCall(coverRequest(manga.toSManga())).await().parseAs<ApiCovers>()
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<SManga> {
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)
}
}

View File

@ -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<String>, private val useLowQualityCovers: Boolean) {
private val preferences: PreferencesHelper by injectLazy()
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
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)
}
}

View File

@ -4,53 +4,39 @@ import kotlinx.serialization.Serializable
@Serializable
data class ApiMangaSerializer(
val chapter: Map<String, ChapterSerializer>? = 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<Int>,
val covers: List<String>,
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<ChapterSerializer>,
val groups: Map<Long, String>,
)
@Serializable
data class MangaSerializerTwo(
data class MangaSerializer(
val artist: List<String>,
val author: List<String>,
val mainCover: String,
val description: String,
val publication: Publication,
val tags: List<Int>,
// val covers: List<String>,
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<GroupSerializer> = emptyList(),
val groups: List<Long>,
val timestamp: Long
)
@Serializable
data class GroupSerializer(
val id: Int,
val name: String? = null
)
@Serializable
data class CoversResult(
val covers: List<String> = emptyList(),
val status: String
)
@Serializable
data class ImageReportResult(
val url: String,
val success: Boolean,
val bytes: Int?
)

View File

@ -0,0 +1,14 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class ApiCovers(
val data: List<CoversResult>,
)
@Serializable
data class CoversResult(
val volume: String,
val url: String
)

View File

@ -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"

View File

@ -4,10 +4,8 @@
<gradient
android:angle="180"
android:startColor="#ff000000"
android:centerColor="#ff000000"
android:endColor="#00000000"
android:startColor="#ff000000" />
android:endColor="#00000000" />
<corners android:radius="0dp" />
</shape>
</shape>