Rewrite and enable Mangadex delegation for V5 of Mangadex (Thanks Cesco)

Co-authored-by: CarlosEsco <CarlosEsco@users.noreply.github.com>
This commit is contained in:
Jobobby04 2021-05-06 21:19:30 -04:00
parent 8686fecb1f
commit b9b5ef55ab
51 changed files with 1587 additions and 1292 deletions

View File

@ -24,6 +24,14 @@ if (!gradle.startParameter.taskRequests.toString().contains("Debug")) {
shortcutHelper.setFilePath("./shortcuts.xml")
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion("1.4.32")
}
}
}
android {
compileSdkVersion(AndroidConfig.compileSdk)
buildToolsVersion(AndroidConfig.buildTools)
@ -170,7 +178,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON
val kotlinSerializationVersion = "1.2.0"
val kotlinSerializationVersion = "1.1.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6")
@ -303,7 +311,7 @@ dependencies {
// JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.12.0")
implementation("com.mikepenz:fastadapter:5.4.0")
implementation("com.mikepenz:fastadapter:5.4.1")
// SY <--
}

View File

@ -25,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */
const val DATABASE_VERSION = /* SY --> */ 6 /* SY <-- */
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -78,6 +78,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(SimilarTable.createTableQuery)
db.execSQL(SimilarTable.createMangaIdIndexQuery)
}
if (oldVersion < 6) {
db.execSQL(MangaTable.addFilteredScanlators)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
@ -68,7 +69,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_VIEWER to obj.viewer_flags,
COL_CHAPTER_FLAGS to obj.chapter_flags,
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
COL_DATE_ADDED to obj.date_added
COL_DATE_ADDED to obj.date_added,
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
)
}
@ -91,6 +93,7 @@ interface BaseMangaGetResolver {
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
}
}

View File

@ -23,6 +23,8 @@ interface Manga : SManga {
var cover_last_modified: Long
var filtered_scanlators: String?
fun setChapterOrder(order: Int) {
setChapterFlags(order, CHAPTER_SORT_MASK)
}

View File

@ -62,6 +62,8 @@ open class MangaImpl : Manga {
override var cover_last_modified: Long = 0
override var filtered_scanlators: String? = null
// SY -->
lateinit var ogTitle: String
private set

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
@ -153,6 +154,13 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaCoverLastModifiedPutResolver())
.prepare()
// SY -->
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFilteredScanlatorsPutResolver())
.prepare()
// SY <--
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
// [EXH]
class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FILTERED_SCANLATORS} = ?")
.whereArgs(manga.filtered_scanlators)
.build()
fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators
)
}

View File

@ -40,6 +40,8 @@ object MangaTable {
// SY ->>
const val COL_READ = "read"
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
// SY <--
const val COL_CATEGORY = "category"
@ -65,7 +67,8 @@ object MangaTable {
$COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
$COL_DATE_ADDED LONG NOT NULL
$COL_DATE_ADDED LONG NOT NULL,
$COL_FILTERED_SCANLATORS TEXT
)"""
val createUrlIndexQuery: String
@ -90,4 +93,7 @@ object MangaTable {
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_ID)"
val addFilteredScanlators: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT"
}

View File

@ -561,9 +561,9 @@ class LibraryUpdateService(
val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
val size: Int
mangaDex.fetchAllFollows(true)
mangaDex.fetchAllFollows()
.filter { (_, metadata) ->
syncFollowStatusInts.contains(metadata.follow_status)
syncFollowStatusInts.contains(metadata.followStatus)
}
.also { size = it.size }
.forEach { (networkManga, metadata) ->

View File

@ -18,7 +18,7 @@ class TrackManager(context: Context) {
const val BANGUMI = 5
// SY --> Mangadex from Neko
const val MDLIST = 6
const val MDLIST = 60
// SY <--
// SY -->

View File

@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable
@ -17,10 +16,12 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MdList(private val context: Context, id: Int) : TrackService(id) {
private val mdex by lazy { MdUtil.getEnabledMangaDex() }
private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) }
@StringRes
override fun nameRes(): Int = R.string.mdlist
@ -47,6 +48,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track {
throw Exception("Mangadex api is read-only")
return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException()
@ -96,9 +98,9 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
val mdex = mdex ?: throw MangaDexNotFoundException()
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
track.copyPersonalFrom(remoteTrack)
if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
/*if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) {
track.total_chapters = mangaMetadata.maxChapterNumber ?: 0
}
}*/
track
}
}
@ -136,8 +138,5 @@ class MdList(private val context: Context, id: Int) : TrackService(id) {
override suspend fun login(username: String, password: String): Unit = throw Exception("not used")
override val isLogged: Boolean
get() = false
class MangaDexNotFoundException : Exception("Mangadex not enabled")
}

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.Hitomi
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.source.online.all.PervEden
@ -203,13 +204,13 @@ open class SourceManager(private val context: Context) {
"eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class
),
/*DelegatedSource(
DelegatedSource(
"MangaDex",
fillInSourceId,
"eu.kanade.tachiyomi.extension.all.mangadex",
MangaDex::class,
true
),*/
),
DelegatedSource(
"HBrowse",
HBROWSE_SOURCE_ID,

View File

@ -15,7 +15,7 @@ interface FollowsSource : CatalogueSource {
*
* @param SManga all smanga found for user
*/
suspend fun fetchAllFollows(forceHd: Boolean = false): List<Pair<SManga, RaisedSearchMetadata>>
suspend fun fetchAllFollows(): List<Pair<SManga, RaisedSearchMetadata>>
/**
* updates the follow status for a manga

View File

@ -1,17 +1,13 @@
package eu.kanade.tachiyomi.source.online.all
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import eu.kanade.tachiyomi.data.database.models.Manga
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.asObservableSuccess
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
@ -21,14 +17,10 @@ import eu.kanade.tachiyomi.source.online.FollowsSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.source.online.NamespaceSource
import eu.kanade.tachiyomi.source.online.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.GalleryAddEvent
import exh.GalleryAdder
import exh.md.MangaDexFabHeaderAdapter
import exh.md.handlers.ApiChapterParser
import exh.md.handlers.ApiMangaParser
@ -36,25 +28,20 @@ import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler
import exh.md.handlers.SimilarHandler
import exh.md.network.MangaDexLoginHelper
import exh.md.network.NoSessionException
import exh.md.network.TokenAuthenticator
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 exh.util.urlImportFetchSearchManga
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.EOFException
import rx.Observable
import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo
@ -67,45 +54,53 @@ import kotlin.reflect.KClass
class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate),
MetadataSource<MangaDexSearchMetadata, Response>,
UrlImportableSource,
// UrlImportableSource,
FollowsSource,
LoginSource,
BrowseSourceFilterHeader,
RandomMangaSource {
RandomMangaSource,
NamespaceSource {
override val lang: String = delegate.lang
override val headers: Headers = super.headers.newBuilder().apply {
add("X-Requested-With", "XMLHttpRequest")
add("Referer", MdUtil.baseUrl)
}.build()
private val mdLang by lazy {
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
MdLang.fromExt(lang) ?: MdLang.ENGLISH
}
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
// override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
val preferences: PreferencesHelper by injectLazy()
val trackManager: TrackManager by injectLazy()
private val sourcePreferences: SharedPreferences by lazy {
context.getSharedPreferences("source_$id", 0x0000)
val mdList: MdList by lazy {
Injekt.get<TrackManager>().mdList
}
private fun useLowQualityThumbnail() = sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
/*private val sourcePreferences: SharedPreferences by lazy {
context.getSharedPreferences("source_$id", 0x0000)
}*/
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
private val loginHelper by lazy {
MangaDexLoginHelper(networkHttpClient, preferences, mdList)
}
override val baseHttpClient: OkHttpClient = super.client.newBuilder()
.authenticator(
TokenAuthenticator(loginHelper)
)
.build()
private fun useLowQualityThumbnail() = false // sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
/*override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
urlImportFetchSearchManga(context, query) {
importIdToMdId(query) {
super.fetchSearchManga(page, query, filters)
}
}
}*/
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
/*override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
"/manga/" + uri.pathSegments[1]
} else {
null
}
@ -119,44 +114,43 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? {
val id = uri.pathSegments.getOrNull(2) ?: return null
val mangaId = MangaHandler(client, headers, mdLang).getMangaIdFromChapterId(id)
val mangaId = MangaHandler(baseHttpClient, headers, mdLang).getMangaIdFromChapterId(id)
return MdUtil.mapMdIdToMangaUrl(mangaId)
}
}*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga, id)
}
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id)
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id)
}
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
}
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getChapterList(manga)
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).getChapterList(manga)
}
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return if (chapter.scanlator == "MangaPlus") {
client.newCall(mangaPlusPageListRequest(chapter))
baseHttpClient.newCall(mangaPlusPageListRequest(chapter))
.asObservableSuccess()
.map { response ->
val chapterId = ApiChapterParser().externalParse(response)
MangaPlusHandler(client).fetchPageList(chapterId)
MangaPlusHandler(baseHttpClient).fetchPageList(chapterId)
}
} else super.fetchPageList(chapter)
}
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
val urlChapterId = MdUtil.getChapterId(chapter.url)
return GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK)
}
override fun fetchImage(page: Page): Observable<Response> {
return if (page.imageUrl!!.contains("mangaplus", true)) {
return if (page.imageUrl?.contains("mangaplus", true) == true) {
MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
.asObservableSuccess()
} else super.fetchImage(page)
@ -169,28 +163,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
}
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
ApiMangaParser(mdLang).parseIntoMetadata(metadata, input, emptyList())
ApiMangaParser(baseHttpClient, mdLang.lang).parseIntoMetadata(metadata, input, emptyList())
}
override suspend fun fetchFollows(): MangasPage {
return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchFollows()
}
override val requiresLogin: Boolean = true
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED
override val twoFactorAuth = LoginSource.AuthSupport.NOT_SUPPORTED
override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrl()
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
return mdList.isLogged
}
override fun getUsername(): String {
return trackManager.mdList.getUsername()
return mdList.getUsername()
}
override fun getPassword(): String {
return trackManager.mdList.getPassword()
return mdList.getPassword()
}
override suspend fun login(
@ -198,96 +191,52 @@ class MangaDex(delegate: HttpSource, val context: Context) :
password: String,
twoFactorCode: String?
): Boolean {
return withIOContext {
val formBody = FormBody.Builder().apply {
add("login_username", username)
add("login_password", password)
add("no_js", "1")
add("remember_me", "1")
add("two_factor", twoFactorCode ?: "")
}
runCatching {
client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
headers,
formBody.build()
)
).await().closeQuietly()
}
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
withIOContext { response.body?.string() }.let { jsonData ->
if (jsonData != null) {
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
} else {
throw Exception("Json data was null")
}
}.also {
preferences.setTrackCredentials(trackManager.mdList, username, password)
}
}
val result = loginHelper.login(username, password)
return if (result is MangaDexLoginHelper.LoginResult.Success) {
MdUtil.updateLoginToken(result.token, preferences, mdList)
mdList.saveCredentials(username, password)
true
} else false
}
override suspend fun logout(): Boolean {
return withIOContext {
// https://mangadex.org/ajax/actions.ajax.php?function=logout
val httpUrl = MdUtil.baseUrl.toHttpUrl()
val listOfDexCookies = network.cookieManager.get(httpUrl)
val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
val token = cookie?.value
if (token.isNullOrEmpty()) {
return@withIOContext true
}
try {
val result = client.newCall(
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
).await()
val resultStr = withIOContext { result.body?.string() }
if (resultStr?.contains("success", true) == true) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
}
} catch (e: EOFException) {
network.cookieManager.remove(httpUrl)
trackManager.mdList.logout()
return@withIOContext true
val result = try {
loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList))
} catch (e: NoSessionException) {
true
}
false
}
return if (result) {
mdList.logout()
true
} else false
}
override suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> {
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchAllFollows(forceHd) }
override suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchAllFollows()
}
suspend fun updateReadingProgress(track: Track): Boolean {
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateReadingProgress(track) }
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateReadingProgress(track)
}
suspend fun updateRating(track: Track): Boolean {
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateRating(track) }
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateRating(track)
}
override suspend fun fetchTrackingInfo(url: String): Track {
return withIOContext {
if (!isLogged()) {
throw Exception("Not Logged in")
}
FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchTrackingInfo(url)
}
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchTrackingInfo(url)
}
suspend fun getTrackingAndMangaInfo(track: Track): Pair<Track, MangaDexSearchMetadata> {
return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail())
return MangaHandler(baseHttpClient, headers, mdLang.lang).getTrackingInfo(track, useLowQualityThumbnail(), mdList)
}
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateFollowStatus(mangaID, followStatus) }
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateFollowStatus(mangaID, followStatus)
}
override fun getFilterHeader(controller: BaseController<*>): MangaDexFabHeaderAdapter {
@ -295,14 +244,14 @@ class MangaDex(delegate: HttpSource, val context: Context) :
}
override suspend fun fetchRandomMangaUrl(): String {
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
return MangaHandler(baseHttpClient, headers, mdLang.lang).fetchRandomMangaId()
}
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
suspend fun fetchMangaSimilar(manga: Manga): MangasPage {
return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
}
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
/*private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
when {
query.toIntOrNull() != null -> {
runAsObservable({
@ -320,11 +269,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
}
}
else -> fail()
}
}*/
companion object {
/*companion object {
private const val REMEMBER_ME = "mangadex_rememberme_token"
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
private const val LOW_QUALITY = 1
}
}*/
}

View File

@ -51,7 +51,7 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.follow_status?.let {
metadata.followStatus?.let {
binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.localText.isVisible = true
}

View File

@ -48,7 +48,7 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.follow_status?.let {
metadata.followStatus?.let {
binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.localText.isVisible = true
}

View File

@ -50,7 +50,7 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
// SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) {
metadata.follow_status?.let {
metadata.followStatus?.let {
binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.localText.isVisible = true
}

View File

@ -46,14 +46,13 @@ import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Stat
import exh.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper
import exh.log.xLogD
import exh.log.xLogE
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.md.utils.scanlatorList
import exh.merged.sql.models.MergedMangaReference
import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource
import exh.source.isEhBasedSource
@ -184,11 +183,6 @@ class MangaPresenter(
.subscribeLatestCache({ view, (manga, flatMetadata) ->
flatMetadata?.let { metadata ->
view.onNextMetaInfo(metadata)
meta?.let {
it.filteredScanlators?.let {
if (chapters.isNotEmpty()) chaptersRelay.call(chapters)
}
}
}
// SY <--
view.onNextMangaInfo(manga, source)
@ -219,7 +213,7 @@ class MangaPresenter(
// Find downloaded chapters
setDownloadedChapters(chapters)
allChapterScanlators = chapters.flatMap { it.chapter.scanlatorList() }.toSet()
allChapterScanlators = chapters.flatMap { MdUtil.getScanlators(it.chapter.scanlator) }.toSet()
// Store the last emission
this.chapters = chapters
@ -307,6 +301,7 @@ class MangaPresenter(
withUIContext { view?.onFetchMangaInfoDone() }
} catch (e: Throwable) {
xLogE("Error getting manga details", e)
withUIContext { view?.onFetchMangaInfoError(e) }
}
}
@ -840,11 +835,9 @@ class MangaPresenter(
}
// SY -->
meta?.let { metadata ->
metadata.filteredScanlators?.let { filteredScanlatorString ->
manga.filtered_scanlators?.let { filteredScanlatorString ->
val filteredScanlators = MdUtil.getScanlators(filteredScanlatorString)
observable = observable.filter { it.scanlatorList().any { group -> filteredScanlators.contains(group) } }
}
observable = observable.filter { MdUtil.getScanlators(it.scanlator).any { group -> filteredScanlators.contains(group) } }
}
// SY <--
@ -1043,12 +1036,10 @@ class MangaPresenter(
}
// SY -->
suspend fun setScanlatorFilter(filteredScanlators: Set<String>) {
val meta = meta ?: return
meta.filteredScanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators)
meta.flatten().let {
db.insertFlatMetadataAsync(it).await()
}
fun setScanlatorFilter(filteredScanlators: Set<String>) {
val manga = manga
manga.filtered_scanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators)
db.updateMangaFilteredScanlators(manga).executeAsBlocking()
refreshChapters()
}
// SY <--

View File

@ -9,18 +9,14 @@ import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Router
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.online.MetadataSource
import eu.kanade.tachiyomi.ui.manga.MangaPresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.popupMenu
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.supervisorScope
class ChaptersSettingsSheet(
@ -88,7 +84,7 @@ class ChaptersSettingsSheet(
* Returns true if there's at least one filter from [FilterGroup] active.
*/
fun hasActiveFilters(): Boolean {
return filterGroup.items.any { it.state != State.IGNORE.value } || (presenter.meta?.let { it is MangaDexSearchMetadata && it.filteredScanlators != null } ?: false)
return filterGroup.items.any { it.state != State.IGNORE.value } || presenter.manga.filtered_scanlators != null
}
inner class FilterGroup : Group {
@ -100,7 +96,7 @@ class ChaptersSettingsSheet(
private val scanlatorFilters = Item.DrawableSelection(0, this, R.string.scanlator, R.drawable.ic_outline_people_alt_24dp)
override val header = null
override val items = listOf(downloaded, unread, bookmarked) + if (presenter.source.getMainSource() is MetadataSource<*, *>) listOf(scanlatorFilters) else emptyList()
override val items = listOf(downloaded, unread, bookmarked, scanlatorFilters)
override val footer = null
override fun initModels() {
@ -116,16 +112,8 @@ class ChaptersSettingsSheet(
override fun onItemClicked(item: Item) {
if (item is Item.DrawableSelection) {
val meta = presenter.meta
if (meta == null) {
context.toast(R.string.metadata_corrupted)
return
} else if (presenter.allChapterScanlators.isEmpty()) {
context.toast(R.string.no_scanlators)
return
}
val scanlators = presenter.allChapterScanlators.toList()
val filteredScanlators = meta.filteredScanlators?.let { MdUtil.getScanlators(it) }
val filteredScanlators = presenter.manga.filtered_scanlators?.let { MdUtil.getScanlators(it) }
val preselected = if (filteredScanlators.isNullOrEmpty()) scanlators.mapIndexed { index, _ -> index }.toIntArray() else filteredScanlators.map { scanlators.indexOf(it) }.toIntArray()
MaterialDialog(context)

View File

@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.updateCoverLastModified
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.md.utils.scanlatorList
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.source.MERGED_SOURCE_ID
@ -116,7 +115,7 @@ class ReaderPresenter(
private val chapterList by lazy {
val manga = manga!!
// SY -->
val filteredScanlators = meta?.filteredScanlators?.let { MdUtil.getScanlators(it) }
val filteredScanlators = manga.filtered_scanlators?.let { MdUtil.getScanlators(it) }
// SY <--
val dbChapters = /* SY --> */ if (manga.source == MERGED_SOURCE_ID) {
(sourceManager.get(MERGED_SOURCE_ID) as MergedSource)
@ -142,7 +141,7 @@ class ReaderPresenter(
) ||
(manga.bookmarkedFilter == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) ||
// SY -->
(filteredScanlators != null && it.scanlatorList().none { group -> filteredScanlators.contains(group) })
(filteredScanlators != null && MdUtil.getScanlators(it.scanlator).none { group -> filteredScanlators.contains(group) })
// SY <--
) {
return@filter false

View File

@ -3,6 +3,7 @@ package exh
import android.content.Context
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.BuildConfig
@ -12,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -242,7 +244,6 @@ object EXHMigrations {
// UpdaterJob.cancelTask(context)
// }
}
if (oldVersion under 17) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -264,6 +265,15 @@ object EXHMigrations {
putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key")
}
// Delete old mangadex trackers
db.db.lowLevel().delete(
DeleteQuery.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(6)
.build()
)
}
// if (oldVersion under 1) { } (1 is current release version)

View File

@ -2,7 +2,7 @@ package exh.md.handlers
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.Page
import exh.md.handlers.serializers.ApiChapterSerializer
import exh.md.handlers.serializers.ChapterResponse
import exh.md.utils.MdUtil
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
@ -10,18 +10,26 @@ import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Response
class ApiChapterParser {
// Only used in [PageHandler], which means its currently unused, kept for reference
fun pageListParse(response: Response): List<Page> {
val networkApiChapter = response.parseAs<ApiChapterSerializer>(MdUtil.jsonParser)
fun pageListParse(response: Response, host: String, dataSaver: Boolean): List<Page> {
val networkApiChapter = response.parseAs<ChapterResponse>(MdUtil.jsonParser)
val hash = networkApiChapter.data.hash
val pageArray = networkApiChapter.data.pages
val server = networkApiChapter.data.server
val pages = mutableListOf<Page>()
return pageArray.mapIndexed { index, page ->
val url = "$hash/$page"
Page(index, "$server,${response.request.url},${System.currentTimeMillis()}", url)
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 {

View File

@ -1,34 +1,32 @@
package exh.md.handlers
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import exh.log.xLogE
import exh.md.handlers.serializers.ApiChapterSerializer
import exh.md.handlers.serializers.ApiMangaSerializer
import exh.md.handlers.serializers.ChapterSerializer
import exh.md.utils.MdLang
import exh.md.handlers.serializers.AuthorResponseList
import exh.md.handlers.serializers.ChapterResponse
import exh.md.handlers.serializers.MangaResponse
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.metadata.metadata.base.insertFlatMetadataCompletable
import exh.util.executeOnIO
import exh.util.floor
import exh.util.nullIfZero
import okhttp3.OkHttpClient
import okhttp3.Response
import rx.Completable
import rx.Single
import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
import java.util.Locale
class ApiMangaParser(private val lang: String) {
val db: DatabaseHelper get() = Injekt.get()
class ApiMangaParser(val client: OkHttpClient, private val lang: String) {
val db: DatabaseHelper by injectLazy()
val metaClass = MangaDexSearchMetadata::class
@ -40,44 +38,18 @@ class ApiMangaParser(private val lang: String) {
}?.call()
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
/**
* Parses metadata from the input and then copies it into the manga
*
* Will also save the metadata to the DB if possible
*/
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
Single.fromCallable {
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
}.map {
it?.raise(metaClass) ?: newMetaInstance()
}
} else {
Single.just(newMetaInstance())
}
return metaObservable.map {
parseIntoMetadata(it, input, coverUrls)
it.copyTo(manga)
it
}.flatMapCompletable {
if (mangaId != null) {
it.mangaId = mangaId
db.insertFlatMetadataCompletable(it.flatten())
} else Completable.complete()
}
}
suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List<String>, sourceId: Long): MangaInfo {
return parseToManga(manga, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls, sourceId)
}
suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, coverUrls: List<String>, sourceId: Long): MangaInfo {
val mangaId = db.getManga(manga.key, sourceId).executeOnIO()?.id
val metadata = if (mangaId != null) {
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO()
flatMetadata?.raise(metaClass) ?: newMetaInstance()
} else newMetaInstance()
parseInfoIntoMetadata(metadata, input, coverUrls)
parseIntoMetadata(metadata, input, coverUrls)
if (mangaId != null) {
metadata.mangaId = mangaId
db.insertFlatMetadata(metadata.flatten())
@ -86,69 +58,82 @@ class ApiMangaParser(private val lang: String) {
return metadata.createMangaInfo(manga)
}
fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) = parseIntoMetadata(metadata, input, coverUrls)
/**
* Parse the manga details json into metadata object
*/
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) {
parseIntoMetadata(metadata, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls)
}
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, networkApiManga: MangaResponse, coverUrls: List<String>) {
with(metadata) {
try {
val networkApiManga = input.parseAs<ApiMangaSerializer>(MdUtil.jsonParser)
val networkManga = networkApiManga.data.manga
mdId = MdUtil.getMangaId(input.request.url.toString())
mdUrl = input.request.url.toString()
title = MdUtil.cleanString(networkManga.title)
thumbnail_url = if (coverUrls.isNotEmpty()) {
val networkManga = networkApiManga.data.attributes
mdUuid = networkApiManga.data.id
title = MdUtil.cleanString(networkManga.title[lang] ?: networkManga.title["en"]!!)
altTitles = networkManga.altTitles.mapNotNull { it[lang] }
cover =
if (coverUrls.isNotEmpty()) {
coverUrls.last()
} else {
networkManga.mainCover
null
// networkManga.mainCover
}
description = MdUtil.cleanDescription(networkManga.description)
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
users = it.users
}
networkManga.links?.let { links ->
links.al?.let { anilist_id = it }
links.kt?.let { kitsu_id = it }
links.mal?.let { my_anime_list_id = it }
links.mu?.let { manga_updates_id = it }
links.ap?.let { anime_planet_id = it }
}
val filteredChapters = filterChapterForChecking(networkApiManga)
description = MdUtil.cleanDescription(networkManga.description["en"]!!)
val tempStatus = parseStatus(networkManga.publication!!.status)
val authorIds = networkApiManga.relationships.filter { it.type.equals("author", true) }.distinct()
authors = runCatching {
val ids = authorIds.joinToString("&ids[]=", "?ids[]=")
val response = client.newCall(GET("${MdUtil.authorUrl}$ids")).execute()
val json = response.parseAs<AuthorResponseList>(MdUtil.jsonParser)
json.results.map { MdUtil.cleanString(it.data.attributes.name) }.takeUnless { it.isEmpty() }
}.getOrNull()
langFlag = networkManga.originalLanguage
val lastChapter = networkManga.lastChapter.toFloatOrNull()
lastChapterNumber = lastChapter?.floor()
/*networkManga.rating?.let {
manga.rating = it.bayesian ?: it.mean
manga.users = it.users
}*/
networkManga.links?.let {
it["al"]?.let { anilistId = it }
it["kt"]?.let { kitsuId = it }
it["mal"]?.let { myAnimeListId = it }
it["mu"]?.let { mangaUpdatesId = it }
it["ap"]?.let { animePlanetId = it }
}
// val filteredChapters = filterChapterForChecking(networkApiManga)
val tempStatus = parseStatus(networkManga.status ?: "")
val publishedOrCancelled =
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
status = SManga.COMPLETED
missing_chapters = null
maxChapterNumber = networkApiManga.data.manga.lastChapter?.toDoubleOrNull()?.floor()
} else {
/*if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
manga.status = SManga.COMPLETED
manga.missing_chapters = null
} else {*/
status = tempStatus
}
// }
val genres =
networkManga.tags.mapNotNull { FilterHandler.allTypes[it.toString()] }
.toMutableList()
// things that will go with the genre tags but aren't actually genre
val nonGenres = listOfNotNull(
networkManga.publicationDemographic?.let { RaisedTag("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
networkManga.contentRating?.let { RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) },
)
networkManga.publication.demographic?.let { demographicInt ->
val demographic = FilterHandler.demographics().firstOrNull { it.id.toInt() == demographicInt }
if (demographic != null) {
genres.add(0, demographic.name)
}
}
if (networkManga.isHentai) {
genres.add("Hentai")
val genres = nonGenres + networkManga.tags
.mapNotNull { dexTag ->
dexTag.attributes.name[lang] ?: dexTag.attributes.name["en"]
}.map {
RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
}
if (tags.isNotEmpty()) tags.clear()
tags += genres.map { RaisedTag(null, it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT) }
tags += genres
} catch (e: Exception) {
xLogE("Parse into metadata error", e)
throw e
@ -160,15 +145,14 @@ class ApiMangaParser(private val lang: String) {
* If chapter title is oneshot or a chapter exists which matches the last chapter in the required language
* return manga is complete
*/
private fun isMangaCompleted(
/*private fun isMangaCompleted(
serializer: ApiMangaSerializer,
filteredChapters: List<ChapterSerializer>
): Boolean {
val finalChapterNumber = serializer.data.manga.lastChapter
if (filteredChapters.isEmpty() || finalChapterNumber.isNullOrEmpty()) {
if (filteredChapters.isEmpty() || serializer.data.manga.lastChapter.isNullOrEmpty()) {
return false
}
// just to fix the stupid lint
val finalChapterNumber = serializer.data.manga.lastChapter!!
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
filteredChapters.firstOrNull()?.let {
if (isOneShot(it, finalChapterNumber)) {
@ -177,15 +161,18 @@ class ApiMangaParser(private val lang: String) {
}
}
val removeOneshots = filteredChapters.asSequence()
.map { it.chapter?.toDoubleOrNull()?.floor()?.nullIfZero() }
.filterNotNull()
.map { it.chapter!!.toDoubleOrNull() }
.filter { it != null }
.map { floor(it!!).toInt() }
.filter { it != 0 }
.toList().distinctBy { it }
return removeOneshots.toList().size == finalChapterNumber.toDouble().floor()
}
return removeOneshots.toList().size == floor(finalChapterNumber.toDouble()).toInt()
}*/
private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<ChapterSerializer> {
/* private fun filterChapterForChecking(serializer: ApiMangaSerializer): List<ChapterSerializer> {
serializer.data.chapters ?: return emptyList()
return serializer.data.chapters.asSequence()
.filter { lang == it.language }
.filter { langs.contains(it.language) }
.filter {
it.chapter?.let { chapterNumber ->
if (chapterNumber.toDoubleOrNull() == null) {
@ -195,18 +182,18 @@ class ApiMangaParser(private val lang: String) {
}
return@filter false
}.toList()
}
}*/
private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean {
/*private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean {
return chapter.title.equals("oneshot", true) ||
((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber))
}
}*/
private fun parseStatus(status: Int) = when (status) {
1 -> SManga.ONGOING
2 -> SManga.PUBLICATION_COMPLETE
3 -> SManga.CANCELLED
4 -> SManga.HIATUS
private fun parseStatus(status: String) = when (status) {
"ongoing" -> SManga.ONGOING
"complete" -> SManga.PUBLICATION_COMPLETE
"abandoned" -> SManga.CANCELLED
"hiatus" -> SManga.HIATUS
else -> SManga.UNKNOWN
}
@ -214,88 +201,69 @@ class ApiMangaParser(private val lang: String) {
* Parse for the random manga id from the [MdUtil.randMangaPage] response.
*/
fun randomMangaIdParse(response: Response): String {
val randMangaUrl = response.asJsoup()
.select("link[rel=canonical]")
.attr("href")
return MdUtil.getMangaId(randMangaUrl)
return response.parseAs<MangaResponse>(MdUtil.jsonParser).data.id
}
fun chapterListParse(response: Response): List<ChapterInfo> {
return chapterListParse(response.parseAs<ApiMangaSerializer>(MdUtil.jsonParser))
fun chapterListParse(chapterListResponse: List<ChapterResponse>, groupMap: Map<String, String>): List<ChapterInfo> {
val now = Date().time
return chapterListResponse.asSequence()
.map {
mapChapter(it, groupMap)
}.filter {
it.dateUpload <= now && "MangaPlus" != it.scanlator
}.toList()
}
fun chapterListParse(networkApiManga: ApiMangaSerializer): List<ChapterInfo> {
val now = System.currentTimeMillis()
val networkManga = networkApiManga.data.manga
val networkChapters = networkApiManga.data.chapters
val groups = networkApiManga.data.groups.mapNotNull {
if (it.name == null) {
null
} else {
it.id to it.name
}
}.toMap()
val status = networkManga.publication!!.status
val finalChapterNumber = networkManga.lastChapter
// Skip chapters that don't match the desired language, or are future releases
val chapLang = MdLang.values().firstOrNull { lang == it.dexLang }
return networkChapters.asSequence()
.filter { lang == it.language && (it.timestamp * 1000) <= now }
.map { mapChapter(it, finalChapterNumber, status, chapLang, networkChapters.size, groups) }.toList()
}
fun chapterParseForMangaId(response: Response): Int {
fun chapterParseForMangaId(response: Response): String {
try {
return response.parseAs<ApiChapterSerializer>().data.mangaId
return response.parseAs<ChapterResponse>(MdUtil.jsonParser)
.relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
} catch (e: Exception) {
xLogE("Parse for manga id error", e)
XLog.e(e)
throw e
}
}
private fun mapChapter(
networkChapter: ChapterSerializer,
finalChapterNumber: String?,
status: Int,
chapLang: MdLang?,
totalChapterCount: Int,
groups: Map<Long, String>
networkChapter: ChapterResponse,
groups: Map<String, String>,
): ChapterInfo {
val key = MdUtil.oldApiChapter + networkChapter.id
// Build chapter name
val chapter = SChapter.create()
val attributes = networkChapter.data.attributes
val key = MdUtil.chapterSuffix + networkChapter.data.id
val chapterName = mutableListOf<String>()
// Build chapter name
if (!networkChapter.volume.isNullOrBlank()) {
val vol = "Vol." + networkChapter.volume
if (attributes.volume != null) {
val vol = "Vol." + attributes.volume
chapterName.add(vol)
// todo
// chapter.vol = vol
}
if (!networkChapter.chapter.isNullOrBlank()) {
val chp = "Ch." + networkChapter.chapter
chapterName.add(chp)
// chapter.chapter_txt = chp
}
if (!networkChapter.title.isNullOrBlank()) {
if (attributes.chapter.isNullOrBlank().not()) {
if (chapterName.isNotEmpty()) {
chapterName.add("-")
}
// todo
chapterName.add(networkChapter.title)
// chapter.chapter_title = MdUtil.cleanString(networkChapter.title)
val chp = "Ch.${attributes.chapter}"
chapterName.add(chp)
// chapter.chapter_txt = chp
}
if (attributes.title.isNullOrBlank().not()) {
if (chapterName.isNotEmpty()) {
chapterName.add("-")
}
chapterName.add(attributes.title!!)
chapter.name = MdUtil.cleanString(attributes.title)
}
// if volume, chapter and title is empty its a oneshot
if (chapterName.isEmpty()) {
chapterName.add("Oneshot")
}
if ((status == 2 || status == 3)) {
/*if ((status == 2 || status == 3)) {
if (finalChapterNumber != null) {
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
@ -303,26 +271,25 @@ class ApiMangaParser(private val lang: String) {
chapterName.add("[END]")
}
}
}
}*/
val name = MdUtil.cleanString(chapterName.joinToString(" "))
// Convert from unix time
val dateUpload = networkChapter.timestamp * 1000
val scanlatorName = mutableSetOf<String>()
val dateUpload = MdUtil.parseDate(attributes.publishAt)
networkChapter.groups.mapNotNull { groups[it] }.forEach { scanlatorName.add(it) }
val scanlatorName = networkChapter.relationships.filter { it.type == "scanlation_group" }.mapNotNull { groups[it.id] }.toSet()
val scanlator = MdUtil.cleanString(MdUtil.getScanlatorString(scanlatorName))
// val mangadexChapterId = MdUtil.getChapterId(chapter.url)
// chapter.mangadex_chapter_id = MdUtil.getChapterId(chapter.url)
// val language = chapLang?.name
// chapter.language = MdLang.fromIsoCode(attributes.translatedLanguage)?.prettyPrint ?: ""
return ChapterInfo(
key = key,
name = name,
scanlator = scanlator,
dateUpload = dateUpload,
scanlator = scanlator
)
}
}

View File

@ -1,180 +1,261 @@
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 {
class FilterHandler(private val preferencesHelper: PreferencesHelper) {
class TextField(name: String, val key: String) : Filter.Text(name)
class Tag(val id: String, name: String) : Filter.TriState(name)
class Switch(val id: String, name: String) : Filter.CheckBox(name)
class ContentList(contents: List<Tag>) : Filter.Group<Tag>("Content", contents)
class FormatList(formats: List<Tag>) : Filter.Group<Tag>("Format", formats)
class GenreList(genres: List<Tag>) : Filter.Group<Tag>("Genres", genres)
class PublicationStatusList(statuses: List<Switch>) : Filter.Group<Switch>("Publication Status", statuses)
class DemographicList(demographics: List<Switch>) : Filter.Group<Switch>("Demographic", demographics)
class R18 : Filter.Select<String>("R18+", arrayOf("Default", "Show all", "Show only", "Show none"))
class ThemeList(themes: List<Tag>) : Filter.Group<Tag>("Themes", themes)
class TagInclusionMode : Filter.Select<String>("Tag inclusion", arrayOf("All (and)", "Any (or)"), 0)
class TagExclusionMode : Filter.Select<String>("Tag exclusion", arrayOf("All (and)", "Any (or)"), 1)
class SortFilter : Filter.Sort(
"Sort",
sortables().map { it.first }.toTypedArray(),
Selection(0, false)
)
class OriginalLanguage : Filter.Select<String>("Original Language", sourceLang().map { it.first }.toTypedArray())
fun getFilterList() = FilterList(
TextField("Author", "author"),
TextField("Artist", "artist"),
R18(),
SortFilter(),
DemographicList(demographics()),
PublicationStatusList(publicationStatus()),
OriginalLanguage(),
ContentList(contentType()),
FormatList(formats()),
GenreList(genre()),
ThemeList(themes()),
internal fun getMDFilterList(): FilterList {
val filters = mutableListOf(
OriginalLanguageList(getOriginalLanguage()),
DemographicList(getDemographics()),
StatusList(getStatus()),
SortFilter(sortableList.map { it.first }.toTypedArray()),
TagList(getTags()),
TagInclusionMode(),
TagExclusionMode()
).toMutableList()
if (true) { // preferencesHelper.showR18Filter()) {
filters.add(2, ContentRatingList(getContentRating()))
}
return FilterList(list = filters.toList())
}
private class Demographic(name: String) : Filter.CheckBox(name)
private class DemographicList(demographics: List<Demographic>) :
Filter.Group<Demographic>("Publication Demographic", demographics)
private fun getDemographics() = listOf(
Demographic("None"),
Demographic("Shounen"),
Demographic("Shoujo"),
Demographic("Seinen"),
Demographic("Josei")
)
companion object {
fun demographics() = listOf(
Switch("1", "Shounen"),
Switch("2", "Shoujo"),
Switch("3", "Seinen"),
Switch("4", "Josei")
private class Status(name: String) : Filter.CheckBox(name)
private class StatusList(status: List<Status>) :
Filter.Group<Status>("Status", status)
private fun getStatus() = listOf(
Status("Onging"),
Status("Completed"),
Status("Hiatus"),
Status("Abandoned"),
)
fun publicationStatus() = listOf(
Switch("1", "Ongoing"),
Switch("2", "Completed"),
Switch("3", "Cancelled"),
Switch("4", "Hiatus")
private class ContentRating(name: String) : Filter.CheckBox(name)
private class ContentRatingList(contentRating: List<ContentRating>) :
Filter.Group<ContentRating>("Content Rating", contentRating)
private fun getContentRating() = listOf(
ContentRating("Safe"),
ContentRating("Suggestive"),
ContentRating("Erotica"),
ContentRating("Pornographic")
)
fun sortables() = listOf(
Triple("Update date", 1, 0),
Triple("Alphabetically", 2, 3),
Triple("Number of comments", 4, 5),
Triple("Rating", 6, 7),
Triple("Views", 8, 9),
Triple("Follows", 10, 11)
private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name)
private class OriginalLanguageList(originalLanguage: List<OriginalLanguage>) :
Filter.Group<OriginalLanguage>("Original language", originalLanguage)
private fun getOriginalLanguage() = listOf(
OriginalLanguage("Japanese (Manga)", "jp"),
OriginalLanguage("Chinese (Manhua)", "cn"),
OriginalLanguage("Korean (Manhwa)", "kr"),
)
fun sourceLang() = listOf(
Pair("All", "0"),
Pair("Japanese", "2"),
Pair("English", "1"),
Pair("Polish", "3"),
Pair("German", "8"),
Pair("French", "10"),
Pair("Vietnamese", "12"),
Pair("Chinese", "21"),
Pair("Indonesian", "27"),
Pair("Korean", "28"),
Pair("Spanish (LATAM)", "29"),
Pair("Thai", "32"),
Pair("Filipino", "34")
internal class Tag(val id: String, name: String) : Filter.TriState(name)
private class TagList(tags: List<Tag>) : Filter.Group<Tag>("Tags", tags)
internal fun getTags() = listOf(
Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"),
Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", "Adaptation"),
Tag("87cc87cd-a395-47af-b27a-93258283bbc6", "Adventure"),
Tag("e64f6742-c834-471d-8d72-dd51fc02b835", "Aliens"),
Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", "Animals"),
Tag("51d83883-4103-437c-b4b1-731cb73d786c", "Anthology"),
Tag("0a39b5a1-b235-4886-a747-1d05d216532d", "Award Winning"),
Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", "Boy Love"),
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("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"),
Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"),
Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"),
Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", "Drama"),
Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Ecchi"),
Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", "Fan Colored"),
Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", "Fantasy"),
Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", "4-koma"),
Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", "Full Color"),
Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", "Genderswap"),
Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", "Ghosts"),
Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", "Girl Love"),
Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", "Gore"),
Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Gyaru"),
Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", "Harem"),
Tag("33771934-028e-4cb3-8744-691e866a923e", "Historical"),
Tag("cdad7e68-1419-41dd-bdce-27753074a640", "Horror"),
Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", "Incest"),
Tag("ace04997-f6bd-436e-b261-779182193d3d", "Isekai"),
Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", "Loli"),
Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", "Long Strip"),
Tag("85daba54-a71c-4554-8a28-9901a8b0afad", "Mafia"),
Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", "Magic"),
Tag("81c836c9-914a-4eca-981a-560dad663e73", "Magical Girls"),
Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", "Martial Arts"),
Tag("50880a9d-5440-4732-9afb-8f457127e836", "Mecha"),
Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", "Medical"),
Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", "Military"),
Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", "Monster Girls"),
Tag("t36fd93ea-e8b8-445e-b836-358f02b3d33d", "Monsters"),
Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", "Music"),
Tag("ee968100-4191-4968-93d3-f82d72be7e46", "Mystery"),
Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Ninja"),
Tag("92d6d951-ca5e-429c-ac78-451071cbf064", "Office Workers"),
Tag("320831a8-4026-470b-94f6-8353740e6f04", "Official Colored"),
Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", "Oneshot"),
Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", "Philosophical"),
Tag("df33b754-73a3-4c54-80e6-1a74a8058539", "Police"),
Tag("9467335a-1b83-4497-9231-765337a00b96", "Post-Apocalyptic"),
Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", "Psychological"),
Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", "Reincarnation"),
Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", "Reverse Harem"),
Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", "Romance"),
Tag("81183756-1453-4c81-aa9e-f6e1b63be016", "Samurai"),
Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", "School Life"),
Tag("256c8bd9-4904-4360-bf4f-508a76d67183", "Sci-Fi"),
Tag("97893a4c-12af-4dac-b6be-0dffb353568e", "Sexual Violence"),
Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", "Shota"),
Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", "Slice of Life"),
Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", "Sports"),
Tag("7064a261-a137-4d3a-8848-2d385de3a99c", "Superhero"),
Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", "Supernatural"),
Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", "Survival"),
Tag("07251805-a27e-4d59-b488-f0bfbec15168", "Thriller"),
Tag("292e862b-2d17-4062-90a2-0356caa4ae27", "Time Travel"),
Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", "Tragedy"),
Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", "Traditional Games"),
Tag("891cf039-b895-47f0-9229-bef4c96eccd4", "User Created"),
Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", "Vampires"),
Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", "Video Games"),
Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", "Villainess"),
Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", "Virtual Reality"),
Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", "Web Comic"),
Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", "Wuxia"),
Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", "Zombies")
)
fun contentType() = listOf(
Tag("9", "Ecchi"),
Tag("32", "Smut"),
Tag("49", "Gore"),
Tag("50", "Sexual Violence")
).sortedWith(compareBy { it.name })
private class TagInclusionMode :
Filter.Select<String>("Included tags mode", arrayOf("And", "Or"), 0)
fun formats() = listOf(
Tag("1", "4-koma"),
Tag("4", "Award Winning"),
Tag("7", "Doujinshi"),
Tag("21", "Oneshot"),
Tag("36", "Long Strip"),
Tag("42", "Adaptation"),
Tag("43", "Anthology"),
Tag("44", "Web Comic"),
Tag("45", "Full Color"),
Tag("46", "User Created"),
Tag("47", "Official Colored"),
Tag("48", "Fan Colored")
).sortedWith(compareBy { it.name })
private class TagExclusionMode :
Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1)
fun genre() = listOf(
Tag("2", "Action"),
Tag("3", "Adventure"),
Tag("5", "Comedy"),
Tag("8", "Drama"),
Tag("10", "Fantasy"),
Tag("13", "Historical"),
Tag("14", "Horror"),
Tag("17", "Mecha"),
Tag("18", "Medical"),
Tag("20", "Mystery"),
Tag("22", "Psychological"),
Tag("23", "Romance"),
Tag("25", "Sci-Fi"),
Tag("28", "Shoujo Ai"),
Tag("30", "Shounen Ai"),
Tag("31", "Slice of Life"),
Tag("33", "Sports"),
Tag("35", "Tragedy"),
Tag("37", "Yaoi"),
Tag("38", "Yuri"),
Tag("41", "Isekai"),
Tag("51", "Crime"),
Tag("52", "Magical Girls"),
Tag("53", "Philosophical"),
Tag("54", "Superhero"),
Tag("55", "Thriller"),
Tag("56", "Wuxia")
).sortedWith(compareBy { it.name })
val sortableList = listOf(
Pair("Default (Asc/Desc doesn't matter)", ""),
Pair("Created at", "createdAt"),
Pair("Updated at", "updatedAt"),
)
fun themes() = listOf(
Tag("6", "Cooking"),
Tag("11", "Gyaru"),
Tag("12", "Harem"),
Tag("16", "Martial Arts"),
Tag("19", "Music"),
Tag("24", "School Life"),
Tag("34", "Supernatural"),
Tag("40", "Video Games"),
Tag("57", "Aliens"),
Tag("58", "Animals"),
Tag("59", "Crossdressing"),
Tag("60", "Demons"),
Tag("61", "Delinquents"),
Tag("62", "Genderswap"),
Tag("63", "Ghosts"),
Tag("64", "Monster Girls"),
Tag("65", "Loli"),
Tag("66", "Magic"),
Tag("67", "Military"),
Tag("68", "Monsters"),
Tag("69", "Ninja"),
Tag("70", "Office Workers"),
Tag("71", "Police"),
Tag("72", "Post-Apocalyptic"),
Tag("73", "Reincarnation"),
Tag("74", "Reverse Harem"),
Tag("75", "Samurai"),
Tag("76", "Shota"),
Tag("77", "Survival"),
Tag("78", "Time Travel"),
Tag("79", "Vampires"),
Tag("80", "Traditional Games"),
Tag("81", "Virtual Reality"),
Tag("82", "Zombies"),
Tag("83", "Incest"),
Tag("84", "Mafia"),
Tag("85", "Villainess")
).sortedWith(compareBy { it.name })
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(0, false))
val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap()
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
)
}
}
}
is ContentRatingList -> {
filter.state.forEach { rating ->
if (rating.state) {
addQueryParameter(
"contentRating[]",
rating.name.toLowerCase(Locale.US)
)
}
}
}
is DemographicList -> {
filter.state.forEach { demographic ->
if (demographic.state) {
addQueryParameter(
"publicationDemographic[]",
demographic.name.toLowerCase(
Locale.US
)
)
}
}
}
is StatusList -> {
filter.state.forEach { status ->
if (status.state) {
addQueryParameter(
"status[]",
status.name.toLowerCase(
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 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].toUpperCase(Locale.US)
)
}
is TagExclusionMode -> {
addQueryParameter(
"excludedTagsMode",
filter.values[filter.state].toUpperCase(Locale.US)
)
}
}
}
if (false) { // preferencesHelper.showR18Filter().not()) {
addQueryParameter("contentRating[]", "safe")
}
}
return url.toString()
}
}

View File

@ -3,44 +3,69 @@ 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.MangasPage
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.log.xLogD
import exh.log.xLogE
import exh.md.handlers.serializers.FollowPage
import exh.md.handlers.serializers.FollowsIndividualSerializer
import exh.md.handlers.serializers.FollowsPageSerializer
import exh.md.handlers.serializers.MangaListResponse
import exh.md.handlers.serializers.MangaResponse
import exh.md.handlers.serializers.UpdateReadingStatus
import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.util.awaitResponse
import exh.util.floor
import kotlinx.serialization.decodeFromString
import exh.util.under
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.FormBody
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 okio.EOFException
import tachiyomi.source.model.MangaInfo
import java.util.Locale
class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
class FollowsHandler(
val client: OkHttpClient,
val headers: Headers,
val preferences: PreferencesHelper,
private val lang: String,
private val useLowQualityCovers: Boolean,
private val mdList: MdList
) {
/**
* fetch follows by page
* fetch all follows
*/
suspend fun fetchFollows(): MangasPage {
return client.newCall(followsListRequest())
.await()
.let { response ->
followsParseMangaPage(response)
suspend fun fetchFollows(): MetadataMangasPage {
return withIOContext {
val response = client.newCall(followsListRequest(0)).await()
val mangaListResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val results = mangaListResponse.results.toMutableList()
var hasMoreResults = mangaListResponse.limit + mangaListResponse.offset under mangaListResponse.total
var lastOffset = mangaListResponse.offset
while (hasMoreResults) {
val offset = lastOffset + mangaListResponse.limit
val newMangaListResponse = client.newCall(followsListRequest(offset)).await()
.parseAs<MangaListResponse>(MdUtil.jsonParser)
results.addAll(newMangaListResponse.results)
hasMoreResults = newMangaListResponse.limit + newMangaListResponse.offset under newMangaListResponse.total
lastOffset = newMangaListResponse.offset
}
val statusListResponse = client.newCall(statusListRequest()).await().parseAs<JsonObject>()
followsParseMangaPage(results, statusListResponse)
}
}
@ -48,157 +73,133 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
* Parse follows api to manga page
* used when multiple follows
*/
private fun followsParseMangaPage(response: Response, forceHd: Boolean = false): MetadataMangasPage {
val followsPageResult = try {
MdUtil.jsonParser.decodeFromString(
response.body?.string().orEmpty()
)
} catch (e: Exception) {
xLogE("error parsing follows", e)
FollowsPageSerializer(404, emptyList())
private fun followsParseMangaPage(response: List<MangaResponse>, statusListResponse: JsonObject): MetadataMangasPage {
val comparator = compareBy<Pair<MangaInfo, MangaDexSearchMetadata>> { it.second.followStatus }
.thenBy { it.first.title }
val result = response.map {
MdUtil.createMangaEntry(it, lang, useLowQualityCovers) to MangaDexSearchMetadata().apply {
followStatus = getFollowStatus(statusListResponse, it.data.id).int
}
}.sortedWith(comparator)
if (followsPageResult.data.isNullOrEmpty() || followsPageResult.code != 200) {
return MetadataMangasPage(emptyList(), false, emptyList())
}
val lowQualityCovers = if (forceHd) false else useLowQualityCovers
val follows = followsPageResult.data.map {
followFromElement(it, lowQualityCovers)
}
val comparator = compareBy<Pair<SManga, MangaDexSearchMetadata>> { it.second.follow_status }.thenBy { it.first.title }
val result = follows.sortedWith(comparator)
return MetadataMangasPage(result.map { it.first }, false, result.map { it.second })
return MetadataMangasPage(result.map { it.first.toSManga() }, false, result.map { it.second })
}
/**
* fetch follow status used when fetching status for 1 manga
*/
private fun followStatusParse(response: Response): Track {
val followsPageResult = try {
response.parseAs<FollowsIndividualSerializer>(MdUtil.jsonParser)
} catch (e: Exception) {
xLogE("error parsing follows", e)
throw e
}
private fun followStatusParse(response: Response, statusListResponse: JsonObject): Track {
val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
val track = Track.create(TrackManager.MDLIST)
if (followsPageResult.code == 404) {
track.status = FollowStatus.UNFOLLOWED.int
} else {
val follow = followsPageResult.data ?: throw Exception("Invalid response ${followsPageResult.code}")
track.status = follow.followType
if (follow.chapter.isNotBlank()) {
track.status = getFollowStatus(statusListResponse, mangaResponse.data.id).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()
}
track.tracking_url = MdUtil.baseUrl + follow.mangaId.toString()
track.title = follow.mangaTitle
}
}*/
return track
}
/**
* build Request for follows page
*/
private fun followsListRequest(): Request {
return GET("${MdUtil.apiUrl}${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK)
}
private fun followsListRequest(offset: Int): Request {
val tempUrl = MdUtil.userFollows.toHttpUrl().newBuilder()
/**
* Parse result element to manga
*/
private fun followFromElement(result: FollowPage, lowQualityCovers: Boolean): Pair<SManga, MangaDexSearchMetadata> {
val manga = SManga.create()
manga.title = MdUtil.cleanString(result.mangaTitle)
manga.url = "/manga/${result.mangaId}/"
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers)
return manga to MangaDexSearchMetadata().apply {
title = manga.title
mdUrl = manga.url
thumbnail_url = manga.thumbnail_url
follow_status = FollowStatus.fromInt(result.followType).int
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
*/
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
suspend fun updateFollowStatus(mangaId: String, followStatus: FollowStatus): Boolean {
return withIOContext {
if (followStatus == FollowStatus.UNFOLLOWED) {
client.newCall(
GET(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_unfollow&id=$mangaID&type=$mangaID",
headers,
CacheControl.FORCE_NETWORK
val status = when (followStatus == FollowStatus.UNFOLLOWED) {
true -> null
false -> followStatus.name.toLowerCase(Locale.US)
}
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())
)
)
} else {
val status = followStatus.int
client.newCall(
GET(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_follow&id=$mangaID&type=$status",
headers,
CacheControl.FORCE_NETWORK
)
)
}.succeeded()
).await()
postResult.isSuccessful
}
}
suspend fun updateReadingProgress(track: Track): Boolean {
return withIOContext {
val mangaID = MdUtil.getMangaId(track.tracking_url)
return true
/*return withIOContext {
val mangaID = getMangaId(track.tracking_url)
val formBody = FormBody.Builder()
.add("volume", "0")
.add("chapter", track.last_chapter_read.toString())
xLogD("chapter to update %s", track.last_chapter_read.toString())
XLog.d("chapter to update %s", track.last_chapter_read.toString())
val result = runCatching {
client.newCall(
POST(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=edit_progress&id=$mangaID",
"$baseUrl/ajax/actions.ajax.php?function=edit_progress&id=$mangaID",
headers,
formBody.build()
)
).succeeded()
).execute()
}
result.exceptionOrNull()?.let {
if (it is EOFException) {
return@withIOContext true
} else {
XLog.e("error updating reading progress", it)
return@withIOContext false
}
}
result.isSuccess
}*/
}
suspend fun updateRating(track: Track): Boolean {
return withIOContext {
val mangaID = MdUtil.getMangaId(track.tracking_url)
return true
/*return withIOContext {
val mangaID = getMangaId(track.tracking_url)
val result = runCatching {
client.newCall(
GET(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
"$baseUrl/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
headers
)
).succeeded()
}
)
.execute()
}
private suspend fun Call.succeeded() = withIOContext {
try {
await().body?.string().let { body ->
(body != null && body.isEmpty()).also {
if (!it) xLogD(body)
result.exceptionOrNull()?.let {
if (it is EOFException) {
return@withIOContext true
} else {
XLog.e("error updating rating", it)
return@withIOContext false
}
}
} catch (e: EOFException) {
true
}
result.isSuccess
}*/
}
/**
* fetch all manga from all possible pages
*/
suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> {
suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
return withIOContext {
val response = client.newCall(followsListRequest()).await()
val mangasPage = followsParseMangaPage(response, forceHd)
mangasPage.mangas.mapIndexed { index, sManga ->
sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata
val metadata: List<MangaDexSearchMetadata>
fetchFollows().also { metadata = it.mangasMetadata.filterIsInstance<MangaDexSearchMetadata>() }.mangas.mapIndexed { index, manga ->
manga to metadata[index]
}
}
}
@ -206,12 +207,20 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
suspend fun fetchTrackingInfo(url: String): Track {
return withIOContext {
val request = GET(
MdUtil.apiUrl + MdUtil.followsMangaApi + MdUtil.getMangaId(url),
headers,
MdUtil.mangaUrl + "/" + MdUtil.getMangaId(url),
MdUtil.getAuthHeaders(headers, preferences, mdList),
CacheControl.FORCE_NETWORK
)
val response = client.newCall(request).awaitResponse()
followStatusParse(response)
val response = client.newCall(request).await()
val statusListResponse = client.newCall(statusListRequest()).await().parseAs<JsonObject>(MdUtil.jsonParser)
followStatusParse(response, statusListResponse)
}
}
private fun getFollowStatus(jsonObject: JsonObject, id: String) =
FollowStatus.fromDex(jsonObject["statuses"]?.jsonObject?.get(id)?.jsonPrimitive?.content)
private fun statusListRequest(): Request {
return GET(MdUtil.mangaStatus, MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK)
}
}

View File

@ -1,6 +1,7 @@
package exh.md.handlers
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.mdlist.MdList
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await
@ -9,12 +10,15 @@ 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.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.ApiCovers
import exh.md.handlers.serializers.ApiMangaSerializer
import exh.md.handlers.serializers.ChapterListResponse
import exh.md.handlers.serializers.ChapterResponse
import exh.md.handlers.serializers.GroupListResponse
import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.util.under
import kotlinx.coroutines.async
import okhttp3.CacheControl
import okhttp3.Headers
@ -26,124 +30,179 @@ import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MangaHandler(val client: OkHttpClient, val headers: Headers, val lang: String, val forceLatestCovers: Boolean = false) {
class MangaHandler(val client: OkHttpClient, val headers: Headers, private val lang: String, private val forceLatestCovers: Boolean = false) {
// TODO make use of this
suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<ChapterInfo>> {
return withIOContext {
val apiNetworkManga = client.newCall(apiRequest(manga)).await().parseAs<ApiMangaSerializer>(MdUtil.jsonParser)
val response = client.newCall(mangaRequest(manga)).await()
val covers = getCovers(manga, forceLatestCovers)
val parser = ApiMangaParser(lang)
val parser = ApiMangaParser(client, lang)
// TODO fix this
/*val mangaInfo = parser.parseToManga(manga, response, covers, sourceId)
val chapterList = parser.chapterListParse(apiNetworkManga)
mangaInfo to chapterList*/
manga to emptyList()
parser.parseToManga(manga, response, covers, sourceId) to getChapterList(manga)
}
}
private suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> {
return if (forceLatestCovers) {
suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> {
/* if (forceLatestCovers) {
val covers = client.newCall(coverRequest(manga)).await().parseAs<ApiCovers>(MdUtil.jsonParser)
covers.data.map { it.url }
} else {
emptyList()
}
return covers.data.map { it.url }
} else {*/
return emptyList<String>()
// }
}
suspend fun getMangaIdFromChapterId(urlChapterId: String): Int {
suspend fun getMangaIdFromChapterId(urlChapterId: String): String {
return withIOContext {
val request = GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
val request = GET(MdUtil.chapterUrl + urlChapterId)
val response = client.newCall(request).await()
ApiMangaParser(lang).chapterParseForMangaId(response)
ApiMangaParser(client, lang).chapterParseForMangaId(response)
}
}
suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long): MangaInfo {
return withIOContext {
val response = client.newCall(apiRequest(manga)).await()
val response = client.newCall(mangaRequest(manga)).await()
val covers = getCovers(manga, forceLatestCovers)
ApiMangaParser(lang).parseToManga(manga, response, covers, sourceId)
ApiMangaParser(client, lang).parseToManga(manga, response, covers, sourceId)
}
}
fun fetchMangaDetailsObservable(manga: SManga): Observable<SManga> {
return client.newCall(apiRequest(manga.toMangaInfo()))
fun fetchMangaDetailsObservable(manga: SManga, sourceId: Long): Observable<SManga> {
return client.newCall(mangaRequest(manga.toMangaInfo()))
.asObservableSuccess()
.flatMap { response ->
runAsObservable({
getCovers(manga.toMangaInfo(), forceLatestCovers)
}).map {
response to it
}
}
.flatMap {
ApiMangaParser(lang).parseToManga(manga, it.first, it.second).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
ApiMangaParser(client, lang).parseToManga(manga.toMangaInfo(), response, emptyList(), sourceId).toSManga()
})
}
}
fun fetchChapterListObservable(manga: SManga): Observable<List<SChapter>> {
return client.newCall(apiRequest(manga.toMangaInfo()))
return client.newCall(mangaFeedRequest(manga.toMangaInfo(), 0, lang))
.asObservableSuccess()
.map { response ->
ApiMangaParser(lang).chapterListParse(response).map { it.toSChapter() }
val chapterListResponse = response.parseAs<ChapterListResponse>(MdUtil.jsonParser)
val results = chapterListResponse.results.toMutableList()
var hasMoreResults = chapterListResponse.limit + chapterListResponse.offset under chapterListResponse.total
var lastOffset = chapterListResponse.offset
while (hasMoreResults) {
val offset = lastOffset + chapterListResponse.limit
val newChapterListResponse = client.newCall(mangaFeedRequest(manga.toMangaInfo(), offset, lang)).execute()
.parseAs<ChapterListResponse>(MdUtil.jsonParser)
results.addAll(newChapterListResponse.results)
hasMoreResults = newChapterListResponse.limit + newChapterListResponse.offset under newChapterListResponse.total
lastOffset = newChapterListResponse.offset
}
val groupIds =
results.asSequence()
.map { chapter -> chapter.relationships }
.flatten()
.filter { it.type == "scanlation_group" }
.map { it.id }
.distinct()
.toList()
val groupMap = runCatching {
groupIds.chunked(100).mapIndexed { index, ids ->
val groupList = client.newCall(groupIdRequest(ids, 100 * index)).execute()
.parseAs<GroupListResponse>(MdUtil.jsonParser)
groupList.results.map { group -> Pair(group.data.id, group.data.attributes.name) }
}.flatten().toMap()
}.getOrNull() ?: emptyMap()
ApiMangaParser(client, lang).chapterListParse(results, groupMap).map { it.toSChapter() }
}
}
suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return withIOContext {
val response = client.newCall(apiRequest(manga)).await()
ApiMangaParser(lang).chapterListParse(response)
val chapterListResponse = client.newCall(mangaFeedRequest(manga, 0, lang)).await().parseAs<ChapterListResponse>(MdUtil.jsonParser)
val results = chapterListResponse.results
var hasMoreResults = chapterListResponse.limit + chapterListResponse.offset under chapterListResponse.total
var lastOffset = chapterListResponse.offset
while (hasMoreResults) {
val offset = lastOffset + chapterListResponse.limit
val newChapterListResponse = client.newCall(mangaFeedRequest(manga, offset, lang)).await()
.parseAs<ChapterListResponse>(MdUtil.jsonParser)
hasMoreResults = newChapterListResponse.limit + newChapterListResponse.offset under newChapterListResponse.total
lastOffset = newChapterListResponse.offset
}
val groupMap = getGroupMap(results)
ApiMangaParser(client, lang).chapterListParse(results, groupMap)
}
}
fun fetchRandomMangaIdObservable(): Observable<String> {
return client.newCall(randomMangaRequest())
.asObservableSuccess()
.map { response ->
ApiMangaParser(lang).randomMangaIdParse(response)
}
private suspend fun getGroupMap(results: List<ChapterResponse>): Map<String, String> {
val groupIds = results.map { chapter -> chapter.relationships }.flatten().filter { it.type == "scanlation_group" }.map { it.id }.distinct()
val groupMap = runCatching {
groupIds.chunked(100).mapIndexed { index, ids ->
client.newCall(groupIdRequest(ids, 100 * index)).await()
.parseAs<GroupListResponse>(MdUtil.jsonParser)
.results.map { group -> Pair(group.data.id, group.data.attributes.name) }
}.flatten().toMap()
}.getOrNull() ?: emptyMap()
return groupMap
}
suspend fun fetchRandomMangaId(): String {
return withIOContext {
val response = client.newCall(randomMangaRequest()).await()
ApiMangaParser(lang).randomMangaIdParse(response)
ApiMangaParser(client, lang).randomMangaIdParse(response)
}
}
suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean): Pair<Track, MangaDexSearchMetadata> {
suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean, mdList: MdList): Pair<Track, MangaDexSearchMetadata> {
return withIOContext {
val metadata = async {
val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt())
val mangaUrl = "/manga/" + MdUtil.getMangaId(track.tracking_url)
val manga = MangaInfo(mangaUrl, track.title)
val response = client.newCall(apiRequest(manga)).await()
val response = client.newCall(mangaRequest(manga)).await()
val metadata = MangaDexSearchMetadata()
ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList())
ApiMangaParser(client, lang).parseIntoMetadata(metadata, response, emptyList())
metadata
}
val remoteTrack = async { FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url) }
val remoteTrack = async {
FollowsHandler(
client,
headers,
Injekt.get(),
lang,
useLowQualityCovers,
mdList
).fetchTrackingInfo(track.tracking_url)
}
remoteTrack.await() to metadata.await()
}
}
private fun randomMangaRequest(): Request {
return GET(MdUtil.baseUrl + MdUtil.randMangaPage, cache = CacheControl.FORCE_NETWORK)
return GET(MdUtil.randomMangaUrl, cache = CacheControl.FORCE_NETWORK)
}
private fun apiRequest(manga: MangaInfo): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK)
private fun mangaRequest(manga: MangaInfo): Request {
return GET(MdUtil.mangaUrl + "/" + MdUtil.getMangaId(manga.key), headers, CacheControl.FORCE_NETWORK)
}
private fun coverRequest(manga: MangaInfo): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK)
private fun mangaFeedRequest(manga: MangaInfo, offset: Int, lang: String): Request {
return GET(MdUtil.mangaFeedUrl(MdUtil.getMangaId(manga.key), offset, lang), headers, CacheControl.FORCE_NETWORK)
}
private fun groupIdRequest(id: List<String>, offset: Int): Request {
val urlSuffix = id.joinToString("&ids[]=", "?limit=100&offset=$offset&ids[]=")
return GET(MdUtil.groupUrl + urlSuffix, headers)
}
/* private fun coverRequest(manga: SManga): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK)
}*/
companion object {
}
}

View File

@ -11,8 +11,7 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
// Unused, kept for reference todo
class PageHandler(val client: OkHttpClient, val headers: Headers, private val imageServer: String, val dataSaver: String?) {
class PageHandler(val client: OkHttpClient, val headers: Headers, private val dataSaver: Boolean) {
fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
if (chapter.scanlator.equals("MangaPlus")) {
@ -26,12 +25,12 @@ class PageHandler(val client: OkHttpClient, val headers: Headers, private val im
return client.newCall(pageListRequest(chapter))
.asObservableSuccess()
.map { response ->
ApiChapterParser().pageListParse(response)
val host = MdUtil.atHomeUrlHostUrl("${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}", client)
ApiChapterParser().pageListParse(response, host, dataSaver)
}
}
private fun pageListRequest(chapter: SChapter): Request {
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix)
return GET("${MdUtil.apiUrl}${chpUrl}${MdUtil.apiChapterSuffix}&server=$imageServer&saver=$dataSaver", headers, CacheControl.FORCE_NETWORK)
return GET("${MdUtil.chapterUrl}${MdUtil.getChapterId(chapter.url)}", headers, CacheControl.FORCE_NETWORK)
}
}

View File

@ -2,24 +2,23 @@ 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.SManga
import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.source.model.toSManga
import exh.md.handlers.serializers.MangaListResponse
import exh.md.utils.MdUtil
import exh.md.utils.setMDUrlWithoutDomain
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
// Unused, kept for reference todo
/**
* Returns the latest manga from the updates url since it actually respects the users settings
*/
class PopularHandler(val client: OkHttpClient, private val headers: Headers, private val useLowQualityCovers: Boolean) {
class PopularHandler(val client: OkHttpClient, private val headers: Headers, private val lang: String, private val useLowQualityCovers: Boolean) {
fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page))
@ -30,38 +29,20 @@ class PopularHandler(val client: OkHttpClient, private val headers: Headers, pri
}
private fun popularMangaRequest(page: Int): Request {
return GET("${MdUtil.baseUrl}/updates/$page/", headers, CacheControl.FORCE_NETWORK)
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 fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(popularMangaSelector).map { element ->
popularMangaFromElement(element)
}.distinctBy { it.url }
val hasNextPage = popularMangaNextPageSelector.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
}
private fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a.manga_title").first().let {
val url = MdUtil.modifyMangaUrl(it.attr("href"))
manga.setMDUrlWithoutDomain(url)
manga.title = it.text().trim()
}
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, useLowQualityCovers)
return manga
}
companion object {
const val popularMangaSelector = "tr a.manga_title"
const val popularMangaNextPageSelector = ".pagination li:not(.disabled) span[title*=last page]:not(disabled)"
val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() }
return MangasPage(mangaList, hasMoreResults)
}
}

View File

@ -2,50 +2,38 @@ 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.SManga
import eu.kanade.tachiyomi.util.asJsoup
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 exh.md.utils.setMDUrlWithoutDomain
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable
// Unused, kept for reference todo
class SearchHandler(val client: OkHttpClient, private val headers: Headers, val lang: String, private val useLowQualityCovers: Boolean) {
class SearchHandler(val client: OkHttpClient, private val headers: Headers, val lang: String, val filterHandler: FilterHandler, private val useLowQualityCovers: Boolean) {
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
return when {
query.startsWith(PREFIX_ID_SEARCH) -> {
fun fetchSearchManga(page: Int, query: String, filters: FilterList, sourceId: Long): Observable<MangasPage> {
return if (query.startsWith(PREFIX_ID_SEARCH)) {
val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
client.newCall(searchMangaByIdRequest(realQuery))
.asObservableSuccess()
.map { response ->
val details = SManga.create()
details.url = "/manga/$realQuery/"
ApiMangaParser(lang).parseToManga(details, response, emptyList()).await()
.flatMap { response ->
runAsObservable({
val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
val details = ApiMangaParser(client, lang)
.parseToManga(MdUtil.createMangaEntry(mangaResponse, lang, useLowQualityCovers), response, emptyList(), sourceId).toSManga()
MangasPage(listOf(details), false)
})
}
}
query.startsWith(PREFIX_GROUP_SEARCH) -> {
val realQuery = query.removePrefix(PREFIX_GROUP_SEARCH)
client.newCall(searchMangaByGroupRequest(realQuery))
.asObservableSuccess()
.map { response ->
response.asJsoup().select(groupSelector).firstOrNull()?.attr("abs:href")
?.let {
searchMangaParse(client.newCall(GET("$it/manga/0", headers)).execute())
}
?: MangasPage(emptyList(), false)
}
}
else -> {
} else {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
@ -53,147 +41,33 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val
}
}
}
}
private fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup()
val mangas = document.select(searchMangaSelector).map { element ->
searchMangaFromElement(element)
}
val hasNextPage = searchMangaNextPageSelector.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() }
return MangasPage(mangaList, hasMoreResults)
}
private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tags = mutableListOf<String>()
val statuses = mutableListOf<String>()
val demographics = mutableListOf<String>()
val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder()
// Do traditional search
val url = "${MdUtil.baseUrl}/?page=search".toHttpUrl().newBuilder()
.addQueryParameter("p", page.toString())
.addQueryParameter("title", query.replace(WHITESPACE_REGEX, " "))
filters.forEach { filter ->
when (filter) {
is FilterHandler.TextField -> url.addQueryParameter(filter.key, filter.state)
is FilterHandler.DemographicList -> {
filter.state.forEach { demographic ->
if (demographic.state) {
demographics.add(demographic.id)
tempUrl.apply {
addQueryParameter("limit", MdUtil.mangaLimit.toString())
addQueryParameter("offset", (MdUtil.getMangaListOffset(page)))
val actualQuery = query.replace(WHITESPACE_REGEX, " ")
if (actualQuery.isNotBlank()) {
addQueryParameter("title", actualQuery)
}
}
}
is FilterHandler.PublicationStatusList -> {
filter.state.forEach { status ->
if (status.state) {
statuses.add(status.id)
}
}
}
is FilterHandler.OriginalLanguage -> {
if (filter.state != 0) {
val number: String =
FilterHandler.sourceLang().first { it -> it.first == filter.values[filter.state] }
.second
url.addQueryParameter("lang_id", number)
}
}
is FilterHandler.TagInclusionMode -> {
url.addQueryParameter("tag_mode_inc", arrayOf("all", "any")[filter.state])
}
is FilterHandler.TagExclusionMode -> {
url.addQueryParameter("tag_mode_exc", arrayOf("all", "any")[filter.state])
}
is FilterHandler.ContentList -> {
filter.state.forEach { content ->
if (content.isExcluded()) {
tags.add("-${content.id}")
} else if (content.isIncluded()) {
tags.add(content.id)
}
}
}
is FilterHandler.FormatList -> {
filter.state.forEach { format ->
if (format.isExcluded()) {
tags.add("-${format.id}")
} else if (format.isIncluded()) {
tags.add(format.id)
}
}
}
is FilterHandler.GenreList -> {
filter.state.forEach { genre ->
if (genre.isExcluded()) {
tags.add("-${genre.id}")
} else if (genre.isIncluded()) {
tags.add(genre.id)
}
}
}
is FilterHandler.ThemeList -> {
filter.state.forEach { theme ->
if (theme.isExcluded()) {
tags.add("-${theme.id}")
} else if (theme.isIncluded()) {
tags.add(theme.id)
}
}
}
is FilterHandler.SortFilter -> {
if (filter.state != null) {
val sortables = FilterHandler.sortables()
if (filter.state!!.ascending) {
url.addQueryParameter(
"s",
sortables[filter.state!!.index].second.toString()
)
} else {
url.addQueryParameter(
"s",
sortables[filter.state!!.index].third.toString()
)
}
}
}
}
}
// Manually append genres list to avoid commas being encoded
var urlToUse = url.toString()
if (demographics.isNotEmpty()) {
urlToUse += "&demos=" + demographics.joinToString(",")
}
if (statuses.isNotEmpty()) {
urlToUse += "&statuses=" + statuses.joinToString(",")
}
if (tags.isNotEmpty()) {
urlToUse += "&tags=" + tags.joinToString(",")
}
return GET(urlToUse, headers, CacheControl.FORCE_NETWORK)
}
val finalUrl = filterHandler.addFiltersToUrl(tempUrl, filters)
private fun searchMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a.manga_title").first().let {
val url = MdUtil.modifyMangaUrl(it.attr("href"))
manga.setMDUrlWithoutDomain(url)
manga.title = it.text().trim()
}
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, useLowQualityCovers)
return manga
return GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
}
private fun searchMangaByIdRequest(id: String): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + id + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK)
return GET(MdUtil.mangaUrl + "/" + id, headers, CacheControl.FORCE_NETWORK)
}
private fun searchMangaByGroupRequest(group: String): Request {
@ -204,9 +78,5 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val
const val PREFIX_ID_SEARCH = "id:"
const val PREFIX_GROUP_SEARCH = "group:"
val WHITESPACE_REGEX = "\\s".toRegex()
const val searchMangaNextPageSelector =
".pagination li:not(.disabled) span[title*=last page]:not(disabled)"
const val searchMangaSelector = "div.manga-entry"
const val groupSelector = ".table > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(2) > a"
}
}

View File

@ -5,25 +5,22 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga
import exh.md.similar.sql.models.MangaSimilar
import exh.md.similar.sql.models.MangaSimilarImpl
import exh.md.utils.MdUtil
import rx.Observable
import exh.util.executeOnIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SimilarHandler(val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
/*
/**
* fetch our similar mangas
*/
fun fetchSimilar(manga: Manga): Observable<MangasPage> {
suspend fun fetchSimilar(manga: Manga): MangasPage {
// Parse the Mangadex id from the URL
return Observable.just(MdUtil.getMangaId(manga.url).toLong())
.flatMap { mangaId ->
Injekt.get<DatabaseHelper>().getSimilar(mangaId).asRxObservable()
}.map { similarMangaDb: MangaSimilar? ->
if (similarMangaDb != null) {
val mangaId = MdUtil.getMangaId(manga.url).toLong()
val similarMangaDb = Injekt.get<DatabaseHelper>().getSimilar(mangaId).executeOnIO()
return if (similarMangaDb != null) {
val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
@ -37,4 +34,3 @@ class SimilarHandler(val preferences: PreferencesHelper, private val useLowQuali
} else MangasPage(mutableListOf(), false)
}
}
}

View File

@ -1,24 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
/*
* Copyright (C) 2020 The Neko Manga Open Source Project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@Serializable
data class ApiChapterSerializer(
val data: ChapterPageSerializer
)
@Serializable
data class ChapterPageSerializer(
val hash: String,
val pages: List<String>,
val server: String,
val mangaId: Int
)

View File

@ -1,76 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class ApiMangaSerializer(
val data: DataSerializer,
val status: String
)
@Serializable
data class DataSerializer(
val manga: MangaSerializer,
val chapters: List<ChapterSerializer>,
val groups: List<GroupSerializer>,
)
@Serializable
data class MangaSerializer(
val artist: List<String>,
val author: List<String>,
val mainCover: String,
val description: String,
val tags: List<Int>,
val isHentai: Boolean,
val lastChapter: String? = null,
val publication: PublicationSerializer? = null,
val links: LinksSerializer? = null,
val rating: RatingSerializer? = null,
val title: String
)
@Serializable
data class PublicationSerializer(
val language: String? = null,
val status: Int,
val demographic: Int?
)
@Serializable
data class LinksSerializer(
val al: String? = null,
val amz: String? = null,
val ap: String? = null,
val engtl: String? = null,
val kt: String? = null,
val mal: String? = null,
val mu: String? = null,
val raw: String? = null
)
@Serializable
data class RatingSerializer(
val bayesian: String? = null,
val mean: String? = null,
val users: String? = null
)
@Serializable
data class ChapterSerializer(
val id: Long,
val volume: String? = null,
val chapter: String? = null,
val title: String? = null,
val language: String,
val groups: List<Long>,
val timestamp: Long
)
@Serializable
data class GroupSerializer(
val id: Long,
val name: String? = null
)

View File

@ -0,0 +1,39 @@
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 LogoutResponse(val result: String)
/**
* Check if session token is valid
*/
@Serializable
data class CheckTokenResponse(val isAuthenticated: Boolean)
/**
* Request to refresh token
*/
@Serializable
data class RefreshTokenRequest(val token: String)

View File

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

View File

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

View File

@ -1,14 +0,0 @@
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

@ -1,24 +0,0 @@
package exh.md.handlers.serializers
import kotlinx.serialization.Serializable
@Serializable
data class FollowsPageSerializer(
val code: Int,
val data: List<FollowPage>?
)
@Serializable
data class FollowsIndividualSerializer(
val code: Int,
val data: FollowPage? = null
)
@Serializable
data class FollowPage(
val mangaTitle: String,
val chapter: String,
val followType: Int,
val mangaId: Int,
val volume: String
)

View File

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

View File

@ -56,7 +56,6 @@ data class TitleDetailView(
@ProtoNumber(5) val nextTimeStamp: Int = 0,
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
@ProtoNumber(7) val viewingPeriodDescription: String = "",
@ProtoNumber(8) val nonAppearanceInfo: String = "",
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
@ProtoNumber(14) val isSimulReleased: Boolean = true,

View File

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

View File

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

View File

@ -0,0 +1,106 @@
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.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.LogoutResponse
import exh.md.handlers.serializers.RefreshTokenRequest
import exh.md.utils.MdUtil
import kotlinx.serialization.SerializationException
import kotlinx.serialization.encodeToString
import okhttp3.CacheControl
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
class MangaDexLoginHelper(val client: OkHttpClient, val preferences: PreferencesHelper, val mdList: MdList) {
suspend fun isAuthenticated(authHeaders: Headers): Boolean {
val response = client.newCall(GET(MdUtil.checkTokenUrl, authHeaders, CacheControl.FORCE_NETWORK)).await()
val body = response.parseAs<CheckTokenResponse>(MdUtil.jsonParser)
return body.isAuthenticated
}
suspend fun refreshToken(authHeaders: Headers): Boolean {
val refreshToken = MdUtil.refreshToken(preferences, mdList)
if (refreshToken.isNullOrEmpty()) {
return false
}
val result = RefreshTokenRequest(refreshToken)
val jsonString = MdUtil.jsonParser.encodeToString(result)
val postResult = client.newCall(
POST(
MdUtil.refreshTokenUrl,
authHeaders,
jsonString.toRequestBody("application/json".toMediaType())
)
).await()
val refresh = runCatching {
val jsonResponse = postResult.parseAs<LoginResponse>(MdUtil.jsonParser)
preferences.trackToken(mdList).set(MdUtil.jsonParser.encodeToString(jsonResponse.token))
}
return refresh.isSuccess
}
suspend fun login(
username: String,
password: String,
): LoginResult {
return withIOContext {
val loginRequest = LoginRequest(username, password)
val jsonString = MdUtil.jsonParser.encodeToString(loginRequest)
val postResult = client.newCall(
POST(
MdUtil.loginUrl,
Headers.Builder().build(),
jsonString.toRequestBody("application/json".toMediaType())
)
).await()
// if it fails to parse then login failed
val loginResponse = try {
postResult.parseAs<LoginResponse>(MdUtil.jsonParser)
} catch (e: SerializationException) {
null
}
if (postResult.code == 200 && loginResponse != null) {
LoginResult.Success(loginResponse.token)
} else {
LoginResult.Failure
}
}
}
sealed class LoginResult {
object Failure : LoginResult()
data class Success(val token: LoginBodyToken) : LoginResult()
}
suspend fun login(): LoginResult {
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 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<LogoutResponse>(MdUtil.jsonParser)
return body.result == "ok"
}
}

View File

@ -0,0 +1,3 @@
package exh.md.network
class NoSessionException : IllegalArgumentException("Session token does not exist")

View File

@ -0,0 +1,73 @@
package exh.md.network
import exh.log.xLogD
import exh.log.xLogI
import exh.md.utils.MdUtil
import kotlinx.coroutines.runBlocking
import okhttp3.Authenticator
import okhttp3.Headers
import okhttp3.Request
import okhttp3.Response
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)
if (token.isEmpty()) {
return null
throw Exception("Unable to authenticate request, please re login")
}
return response.request.newBuilder().header("Authorization", token).build()
}
@Synchronized
fun refreshToken(loginHelper: MangaDexLoginHelper): String {
var validated = false
runBlocking {
val checkToken = try {
loginHelper.isAuthenticated(
MdUtil.getAuthHeaders(
Headers.Builder().build(),
loginHelper.preferences,
loginHelper.mdList
)
)
} catch (e: NoSessionException) {
xLogD("Session token does not exist")
false
}
if (checkToken) {
xLogI("Token is valid, other thread must have refreshed it")
validated = true
}
if (validated.not()) {
xLogI("Token is invalid trying to refresh")
validated = loginHelper.refreshToken(
MdUtil.getAuthHeaders(
Headers.Builder().build(), loginHelper.preferences, loginHelper.mdList
)
)
}
if (validated.not()) {
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
}
}
return when {
validated -> "bearer: ${MdUtil.sessionToken(loginHelper.preferences, loginHelper.mdList)!!}"
else -> ""
}
}
}

View File

@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import eu.kanade.tachiyomi.util.lang.runAsObservable
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
@ -15,7 +16,7 @@ import rx.schedulers.Schedulers
class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
override fun requestNext(): Observable<MangasPage> {
return source.fetchMangaSimilar(manga)
return runAsObservable({ source.fetchMangaSimilar(manga) })
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext {

View File

@ -1,5 +1,7 @@
package exh.md.utils
import java.util.Locale
enum class FollowStatus(val int: Int) {
UNFOLLOWED(0),
READING(1),
@ -10,6 +12,7 @@ enum class FollowStatus(val int: Int) {
RE_READING(6);
companion object {
fun fromInt(value: Int): FollowStatus = values().find { it.int == value } ?: UNFOLLOWED
fun fromDex(value: String?): FollowStatus = values().firstOrNull { it.name.toLowerCase(Locale.US) == value } ?: UNFOLLOWED
fun fromInt(value: Int): FollowStatus = values().firstOrNull { it.int == value } ?: UNFOLLOWED
}
}

View File

@ -1,45 +1,58 @@
package exh.md.utils
enum class MdLang(val lang: String, val dexLang: String, val langId: Int) {
English("en", "gb", 1),
Japanese("ja", "jp", 2),
Polish("pl", "pl", 3),
SerboCroatian("sh", "rs", 4),
Dutch("nl", "nl", 5),
Italian("it", "it", 6),
Russian("ru", "ru", 7),
German("de", "de", 8),
Hungarian("hu", "hu", 9),
French("fr", "fr", 10),
Finnish("fi", "fi", 11),
Vietnamese("vi", "vn", 12),
Greek("el", "gr", 13),
Bulgarian("bg", "bg", 14),
Spanish("es", "es", 15),
PortugeseBrazilian("pt-BR", "br", 16),
Portuguese("pt", "pt", 17),
Swedish("sv", "se", 18),
Arabic("ar", "sa", 19),
Danish("da", "dk", 20),
ChineseSimplifed("zh-Hans", "cn", 21),
Bengali("bn", "bd", 22),
Romanian("ro", "ro", 23),
Czech("cs", "cz", 24),
Mongolian("mn", "mn", 25),
Turkish("tr", "tr", 26),
Indonesian("id", "id", 27),
Korean("ko", "kr", 28),
SpanishLTAM("es-419", "mx", 29),
Persian("fa", "ir", 30),
Malay("ms", "my", 31),
Thai("th", "th", 32),
Catalan("ca", "ct", 33),
Filipino("fil", "ph", 34),
ChineseTraditional("zh-Hant", "hk", 35),
Ukrainian("uk", "ua", 36),
Burmese("my", "mm", 37),
Lithuanian("lt", "il", 38),
Hebrew("he", "il", 39),
Hindi("hi", "in", 40),
Norwegian("no", "no", 42)
enum class MdLang(val lang: String, val prettyPrint: String, val extLang: String = lang) {
ENGLISH("en", "English"),
JAPANESE("jp", "Japanese", "ja"),
POLISH("pl", "Polish"),
SERBO_CROATIAN("rs", "Serbo-Croatian", "sh"),
DUTCH("nl", "Dutch"),
ITALIAN("it", "IT"),
RUSSIAN("ru", "Russian"),
GERMAN("de", "German"),
HUNGARIAN("hu", "Hungarian"),
FRENCH("fr", "French"),
FINNISH("fi", "Finnish"),
VIETNAMESE("vn", "Vietnamese", "vi"),
GREEK("gr", "Greek", "el"),
BULGARIAN("bg", "BULGARIN"),
SPANISH_ES("es", "Spanish (Es)"),
PORTUGUESE_BR("br", "Portuguese (Br)", "pt-br"),
PORTUGUESE("pt", "Portuguese (Pt)"),
SWEDISH("se", "Swedish", "sv"),
ARABIC("sa", "Arabic", "ar"),
DANISH("dk", "Danish", "da"),
CHINESE_SIMPLIFIED("cn", "Chinese (Simp)", "zh"),
BENGALI("bd", "Bengali", "bn"),
ROMANIAN("ro", "Romanian"),
CZECH("cz", "Czech", "cs"),
MONGOLIAN("mn", "Mongolian"),
TURKISH("tr", "Turkish"),
INDONESIAN("id", "Indonesian"),
KOREAN("kr", "Korean", "ko"),
SPANISH_LATAM("mx", "Spanish (LATAM)", "es-la"),
PERSIAN("ir", "Persian", "fa"),
MALAY("my", "Malay", "ms"),
THAI("th", "Thai"),
CATALAN("ct", "Catalan", "ca"),
FILIPINO("ph", "Filipino", "fi"),
CHINESE_TRAD("hk", "Chinese (Trad)", "zh-hk"),
UKRAINIAN("ua", "Ukrainian", "uk"),
BURMESE("mm", "Burmese", "my"),
LINTHUANIAN("lt", "Lithuanian"),
HEBREW("il", "Hebrew", "he"),
HINDI("in", "Hindi", "hi"),
NORWEGIAN("no", "Norwegian")
;
companion object {
fun fromIsoCode(isoCode: String): MdLang? =
values().firstOrNull {
it.lang == isoCode
}
fun fromExt(extLang: String): MdLang? =
values().firstOrNull {
it.extLang == extLang
}
}
}

View File

@ -1,43 +1,83 @@
package exh.md.utils
import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Chapter
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.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.md.handlers.serializers.AtHomeResponse
import exh.md.handlers.serializers.LoginBodyToken
import exh.md.handlers.serializers.MangaResponse
import exh.md.network.NoSessionException
import exh.source.getMainSource
import exh.util.floor
import exh.util.nullIfBlank
import exh.util.nullIfZero
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import org.jsoup.parser.Parser
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.net.URISyntaxException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@Suppress("unused")
class MdUtil {
companion object {
const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org"
const val baseUrl = "https://mangadex.org"
const val randMangaPage = "/manga/"
const val apiUrl = "https://api.mangadex.org"
const val apiManga = "/v2/manga/"
const val includeChapters = "?include=chapters"
const val oldApiChapter = "/api/chapter/"
const val newApiChapter = "/v2/chapter/"
const val apiChapterSuffix = "?mark_read=0"
const val apiUrlCdnCache = "https://cdn.statically.io/gh/goldbattle/MangadexRecomendations/master/output/api/"
const val apiUrlCache = "https://raw.githubusercontent.com/goldbattle/MangadexRecomendations/master/output/api/"
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 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("locales[]", language)
}.build().toString()
}
const val groupSearchUrl = "$baseUrl/groups/0/1/"
const val followsAllApi = "/v2/user/me/followed-manga"
const val isLoggedInApi = "/v2/user/me"
const val followsMangaApi = "/v2/user/me/manga/"
const val apiCovers = "/covers"
const val reportUrl = "https://api.mangadex.network/report"
const val imageUrl = "$baseUrl/data"
val jsonParser = Json {
const val mdAtHomeTokenLifespan = 10 * 60 * 1000
const val mangaLimit = 25
/**
* Get the manga offset pages are 1 based, so subtract 1
*/
fun getMangaListOffset(page: Int): String = (mangaLimit * (page - 1)).toString()
val jsonParser =
Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
@ -164,24 +204,9 @@ class MdUtil {
}
// Get the ID from the manga url
fun getMangaId(url: String): String {
val lastSection = url.trimEnd('/').substringAfterLast("/")
return if (lastSection.toIntOrNull() != null) {
lastSection
} else {
// this occurs if person has manga from before that had the id/name/
url.trimEnd('/').substringBeforeLast("/").substringAfterLast("/")
}
}
fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/")
fun getChapterId(url: String) = url.substringBeforeLast(apiChapterSuffix).substringAfterLast("/")
// creates the manga url from the browse for the api
fun modifyMangaUrl(url: String): String =
url.replace("/title/", "/manga/").substringBeforeLast("/") + "/"
// Removes the ?timestamp from image urls
fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?")
fun getChapterId(url: String) = url.substringAfterLast("/")
fun cleanString(string: String): String {
var cleanedString = string
@ -222,8 +247,8 @@ class MdUtil {
return baseUrl + attr
}
fun getScanlators(scanlators: String): List<String> {
if (scanlators.isBlank()) return emptyList()
fun getScanlators(scanlators: String?): List<String> {
if (scanlators.isNullOrBlank()) return emptyList()
return scanlators.split(scanlatorSeparator).distinct()
}
@ -234,7 +259,6 @@ class MdUtil {
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
if (mangaStatus == SManga.COMPLETED) return null
// TODO
val remove0ChaptersFromCount = chapters.distinctBy {
/*if (it.chapter_txt.isNotEmpty()) {
it.vol + it.chapter_txt
@ -257,15 +281,63 @@ class MdUtil {
return null
}
fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? {
fun atHomeUrlHostUrl(requestUrl: String, client: OkHttpClient): String {
val atHomeRequest = GET(requestUrl)
val atHomeResponse = client.newCall(atHomeRequest).execute()
return atHomeResponse.parseAs<AtHomeResponse>(jsonParser).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, lowQualityCovers: Boolean): MangaInfo {
val key = "/manga/" + json.data.id
return MangaInfo(
key = key,
title = cleanString(json.data.attributes.title[lang] ?: json.data.attributes.title["en"]!!),
cover = formThumbUrl(key, lowQualityCovers)
)
}
fun sessionToken(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
try {
jsonParser.decodeFromString<LoginBodyToken>(it)
} catch (e: SerializationException) {
xLogD("Unable to load session token")
null
}
}?.session
fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
try {
jsonParser.decodeFromString<LoginBodyToken>(it)
} catch (e: SerializationException) {
xLogD("Unable to load session token")
null
}
}?.refresh
fun updateLoginToken(token: LoginBodyToken, preferences: PreferencesHelper, mdList: MdList) {
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
}
fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) =
headers.newBuilder().add("Authorization", "Bearer ${sessionToken(preferences, mdList) ?: throw NoSessionException()}").build()
fun getEnabledMangaDex(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): MangaDex? {
return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs ->
preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()?.let { preferredMangaDexId ->
preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()
?.let { preferredMangaDexId ->
mangadexs.firstOrNull { it.id == preferredMangaDexId }
} ?: mangadexs.firstOrNull()
}
?: mangadexs.firstOrNull()
}
}
fun getEnabledMangaDexs(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
fun getEnabledMangaDexs(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
val languages = preferences.enabledLanguages().get()
val disabledSourceIds = preferences.disabledSources().get()
@ -275,54 +347,5 @@ class MdUtil {
.filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds }
}
fun mapMdIdToMangaUrl(id: Int) = "/manga/$id/"
}
}
/**
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the chapter.
*/
fun SChapter.setMDUrlWithoutDomain(url: String) {
this.url = getMDUrlWithoutDomain(url)
}
/**
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
* database and the urls could still work after a domain change.
*
* @param url the full url to the manga.
*/
fun SManga.setMDUrlWithoutDomain(url: String) {
this.url = getMDUrlWithoutDomain(url)
}
/**
* Returns the url of the given string without the scheme and domain.
*
* @param orig the full url.
*/
private fun getMDUrlWithoutDomain(orig: String): String {
return try {
val uri = orig.toUri()
var out = uri.path.orEmpty()
if (uri.query != null) {
out += "?" + uri.query
}
if (uri.fragment != null) {
out += "#" + uri.fragment
}
out
} catch (e: URISyntaxException) {
orig
}
}
fun Chapter.scanlatorList(): List<String> {
return this.scanlator?.let {
MdUtil.getScanlators(it)
} ?: listOf("No scanlator")
}

View File

@ -1,65 +1,56 @@
package exh.metadata.metadata
import android.content.Context
import androidx.core.net.toUri
import eu.kanade.tachiyomi.R
import exh.md.utils.MdUtil
import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.util.nullIfBlank
import kotlinx.serialization.Serializable
import tachiyomi.source.model.MangaInfo
@Serializable
class MangaDexSearchMetadata : RaisedSearchMetadata() {
var mdId: String? = null
var mdUuid: String? = null
var mdUrl: String? = null
// var mdUrl: String? = null
var thumbnail_url: String? = null
var cover: String? = null
var title: String? by titleDelegate(TITLE_TYPE_MAIN)
var altTitles: List<String>? = null
var description: String? = null
var author: String? = null
var artist: String? = null
var authors: List<String>? = null
var lang_flag: String? = null
var langFlag: String? = null
var last_chapter_number: Int? = null
var rating: String? = null
var users: String? = null
var lastChapterNumber: Int? = null
// var rating: String? = null
// var users: String? = null
var anilist_id: String? = null
var kitsu_id: String? = null
var my_anime_list_id: String? = null
var manga_updates_id: String? = null
var anime_planet_id: String? = null
var anilistId: String? = null
var kitsuId: String? = null
var myAnimeListId: String? = null
var mangaUpdatesId: String? = null
var animePlanetId: String? = null
var status: Int? = null
var missing_chapters: String? = null
// var missing_chapters: String? = null
var follow_status: Int? = null
var followStatus: Int? = null
var maxChapterNumber: Int? = null
// var maxChapterNumber: Int? = null
override fun createMangaInfo(manga: MangaInfo): MangaInfo {
val key = mdUrl?.let {
try {
val uri = it.toUri()
val out = uri.path!!.removePrefix("/api")
out + if (out.endsWith("/")) "" else "/"
} catch (e: Exception) {
it
}
}
val key = mdUuid?.let { "/manga/$it" }
val title = title
val cover = thumbnail_url
val cover = cover ?: manga.cover.nullIfBlank() ?: "https://i.imgur.com/6TrIues.jpg" // cover
val author = author
val artist = artist
val author = authors?.joinToString()?.let { MdUtil.cleanString(it) }
val status = status
@ -72,7 +63,6 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
title = title ?: manga.title,
cover = cover ?: manga.cover,
author = author ?: manga.author,
artist = artist ?: manga.artist,
status = status ?: manga.status,
genres = genres,
description = description ?: manga.description
@ -81,29 +71,30 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
val pairs = mutableListOf<Pair<String, String>>()
mdId?.let { pairs += context.getString(R.string.id) to it }
mdUrl?.let { pairs += context.getString(R.string.url) to it }
thumbnail_url?.let { pairs += context.getString(R.string.thumbnail_url) to it }
mdUuid?.let { pairs += context.getString(R.string.id) to it }
// mdUrl?.let { pairs += context.getString(R.string.url) to it }
cover?.let { pairs += context.getString(R.string.thumbnail_url) to it }
title?.let { pairs += context.getString(R.string.title) to it }
author?.let { pairs += context.getString(R.string.author) to it }
artist?.let { pairs += context.getString(R.string.artist) to it }
lang_flag?.let { pairs += context.getString(R.string.language) to it }
last_chapter_number?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() }
rating?.let { pairs += context.getString(R.string.average_rating) to it }
users?.let { pairs += context.getString(R.string.total_ratings) to it }
authors?.let { pairs += context.getString(R.string.author) to it.joinToString() }
// artist?.let { pairs += context.getString(R.string.artist) to it }
langFlag?.let { pairs += context.getString(R.string.language) to it }
lastChapterNumber?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() }
// rating?.let { pairs += context.getString(R.string.average_rating) to it }
// users?.let { pairs += context.getString(R.string.total_ratings) to it }
status?.let { pairs += context.getString(R.string.status) to it.toString() }
missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it }
follow_status?.let { pairs += context.getString(R.string.follow_status) to it.toString() }
anilist_id?.let { pairs += context.getString(R.string.anilist_id) to it }
kitsu_id?.let { pairs += context.getString(R.string.kitsu_id) to it }
my_anime_list_id?.let { pairs += context.getString(R.string.mal_id) to it }
manga_updates_id?.let { pairs += context.getString(R.string.manga_updates_id) to it }
anime_planet_id?.let { pairs += context.getString(R.string.anime_planet_id) to it }
// missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it }
followStatus?.let { pairs += context.getString(R.string.follow_status) to it.toString() }
anilistId?.let { pairs += context.getString(R.string.anilist_id) to it }
kitsuId?.let { pairs += context.getString(R.string.kitsu_id) to it }
myAnimeListId?.let { pairs += context.getString(R.string.mal_id) to it }
mangaUpdatesId?.let { pairs += context.getString(R.string.manga_updates_id) to it }
animePlanetId?.let { pairs += context.getString(R.string.anime_planet_id) to it }
return pairs
}
companion object {
private const val TITLE_TYPE_MAIN = 0
private const val TITLE_TYPE_ALT_TITLE = 1
const val TAG_TYPE_DEFAULT = 0
}

View File

@ -48,8 +48,6 @@ abstract class RaisedSearchMetadata {
@Transient
val titles = mutableListOf<RaisedTitle>()
var filteredScanlators: String? = null
fun getTitleOfType(type: Int): String? = titles.find { it.type == type }?.title
fun replaceTitleOfType(type: Int, newTitle: String?) {

View File

@ -1,20 +1,18 @@
package exh.ui.metadata.adapters
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.DescriptionAdapterMdBinding
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import exh.metadata.MetadataUtil.getRatingString
import exh.metadata.bindDrawable
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.ui.metadata.MetadataViewController
import kotlin.math.round
class MangaDexDescriptionAdapter(
private val controller: MangaController
@ -40,10 +38,13 @@ class MangaDexDescriptionAdapter(
val meta = controller.presenter.meta
if (meta == null || meta !is MangaDexSearchMetadata) return
val ratingFloat = meta.rating?.toFloatOrNull()
// todo
/*val ratingFloat = meta.rating?.toFloatOrNull()
binding.ratingBar.rating = ratingFloat?.div(2F) ?: 0F
@SuppressLint("SetTextI18n")
binding.rating.text = (round((meta.rating?.toFloatOrNull() ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(itemView.context, ratingFloat)
binding.rating.text = (round((meta.rating?.toFloatOrNull() ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(itemView.context, ratingFloat)*/
binding.rating.isVisible = false
binding.ratingBar.isVisible = false
binding.moreInfo.bindDrawable(itemView.context, R.drawable.ic_info_24dp)