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") shortcutHelper.setFilePath("./shortcuts.xml")
configurations.all {
resolutionStrategy.eachDependency {
if (requested.group == "org.jetbrains.kotlin") {
useVersion("1.4.32")
}
}
}
android { android {
compileSdkVersion(AndroidConfig.compileSdk) compileSdkVersion(AndroidConfig.compileSdk)
buildToolsVersion(AndroidConfig.buildTools) buildToolsVersion(AndroidConfig.buildTools)
@ -170,7 +178,7 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.1") implementation("org.conscrypt:conscrypt-android:2.5.1")
// JSON // 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-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
implementation("com.google.code.gson:gson:2.8.6") implementation("com.google.code.gson:gson:2.8.6")
@ -303,7 +311,7 @@ dependencies {
// JsonReader for similar manga // JsonReader for similar manga
implementation("com.squareup.moshi:moshi:1.12.0") implementation("com.squareup.moshi:moshi:1.12.0")
implementation("com.mikepenz:fastadapter:5.4.0") implementation("com.mikepenz:fastadapter:5.4.1")
// SY <-- // SY <--
} }

View File

@ -25,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * 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) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -78,6 +78,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(SimilarTable.createTableQuery) db.execSQL(SimilarTable.createTableQuery)
db.execSQL(SimilarTable.createMangaIdIndexQuery) db.execSQL(SimilarTable.createMangaIdIndexQuery)
} }
if (oldVersion < 6) {
db.execSQL(MangaTable.addFilteredScanlators)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { 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_DATE_ADDED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION 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_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_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED 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_VIEWER to obj.viewer_flags,
COL_CHAPTER_FLAGS to obj.chapter_flags, COL_CHAPTER_FLAGS to obj.chapter_flags,
COL_COVER_LAST_MODIFIED to obj.cover_last_modified, 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)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED)) cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED)) 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 cover_last_modified: Long
var filtered_scanlators: String?
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setChapterFlags(order, CHAPTER_SORT_MASK) setChapterFlags(order, CHAPTER_SORT_MASK)
} }

View File

@ -62,6 +62,8 @@ open class MangaImpl : Manga {
override var cover_last_modified: Long = 0 override var cover_last_modified: Long = 0
override var filtered_scanlators: String? = null
// SY --> // SY -->
lateinit var ogTitle: String lateinit var ogTitle: String
private set 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.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver 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.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
@ -153,6 +154,13 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaCoverLastModifiedPutResolver()) .withPutResolver(MangaCoverLastModifiedPutResolver())
.prepare() .prepare()
// SY -->
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFilteredScanlatorsPutResolver())
.prepare()
// SY <--
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).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 ->> // SY ->>
const val COL_READ = "read" const val COL_READ = "read"
const val COL_FILTERED_SCANLATORS = "filtered_scanlators"
// SY <-- // SY <--
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
@ -65,7 +67,8 @@ object MangaTable {
$COL_VIEWER INTEGER NOT NULL, $COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL, $COL_CHAPTER_FLAGS INTEGER NOT NULL,
$COL_COVER_LAST_MODIFIED LONG 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 val createUrlIndexQuery: String
@ -90,4 +93,7 @@ object MangaTable {
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " + "FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " + "ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_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 syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() }
val size: Int val size: Int
mangaDex.fetchAllFollows(true) mangaDex.fetchAllFollows()
.filter { (_, metadata) -> .filter { (_, metadata) ->
syncFollowStatusInts.contains(metadata.follow_status) syncFollowStatusInts.contains(metadata.followStatus)
} }
.also { size = it.size } .also { size = it.size }
.forEach { (networkManga, metadata) -> .forEach { (networkManga, metadata) ->

View File

@ -18,7 +18,7 @@ class TrackManager(context: Context) {
const val BANGUMI = 5 const val BANGUMI = 5
// SY --> Mangadex from Neko // SY --> Mangadex from Neko
const val MDLIST = 6 const val MDLIST = 60
// SY <-- // SY <--
// 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.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch 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.source.model.toMangaInfo
import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.awaitSingle
import eu.kanade.tachiyomi.util.lang.runAsObservable 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.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import tachiyomi.source.model.MangaInfo 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) { 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 @StringRes
override fun nameRes(): Int = R.string.mdlist 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 add(track: Track): Track = update(track)
override suspend fun update(track: Track): Track { override suspend fun update(track: Track): Track {
throw Exception("Mangadex api is read-only")
return withIOContext { return withIOContext {
val mdex = mdex ?: throw MangaDexNotFoundException() 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 mdex = mdex ?: throw MangaDexNotFoundException()
val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track) val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track)
track.copyPersonalFrom(remoteTrack) 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.total_chapters = mangaMetadata.maxChapterNumber ?: 0
} }*/
track 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 suspend fun login(username: String, password: String): Unit = throw Exception("not used")
override val isLogged: Boolean
get() = false
class MangaDexNotFoundException : Exception("Mangadex not enabled") 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.HttpSource
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.source.online.all.Hitomi 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.MergedSource
import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.NHentai
import eu.kanade.tachiyomi.source.online.all.PervEden 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", "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino",
Tsumino::class Tsumino::class
), ),
/*DelegatedSource( DelegatedSource(
"MangaDex", "MangaDex",
fillInSourceId, fillInSourceId,
"eu.kanade.tachiyomi.extension.all.mangadex", "eu.kanade.tachiyomi.extension.all.mangadex",
MangaDex::class, MangaDex::class,
true true
),*/ ),
DelegatedSource( DelegatedSource(
"HBrowse", "HBrowse",
HBROWSE_SOURCE_ID, HBROWSE_SOURCE_ID,

View File

@ -15,7 +15,7 @@ interface FollowsSource : CatalogueSource {
* *
* @param SManga all smanga found for user * @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 * updates the follow status for a manga

View File

@ -1,17 +1,13 @@
package eu.kanade.tachiyomi.source.online.all package eu.kanade.tachiyomi.source.online.all
import android.content.Context 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.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager 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.GET
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess 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.MangasPage
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter 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.HttpSource
import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.LoginSource
import eu.kanade.tachiyomi.source.online.MetadataSource 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.RandomMangaSource
import eu.kanade.tachiyomi.source.online.UrlImportableSource
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.manga.MangaController 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.MangaDexFabHeaderAdapter
import exh.md.handlers.ApiChapterParser import exh.md.handlers.ApiChapterParser
import exh.md.handlers.ApiMangaParser import exh.md.handlers.ApiMangaParser
@ -36,25 +28,20 @@ import exh.md.handlers.FollowsHandler
import exh.md.handlers.MangaHandler import exh.md.handlers.MangaHandler
import exh.md.handlers.MangaPlusHandler import exh.md.handlers.MangaPlusHandler
import exh.md.handlers.SimilarHandler 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.FollowStatus
import exh.md.utils.MdLang import exh.md.utils.MdLang
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.DelegatedHttpSource import exh.source.DelegatedHttpSource
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter 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.CacheControl
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.EOFException
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@ -67,45 +54,53 @@ import kotlin.reflect.KClass
class MangaDex(delegate: HttpSource, val context: Context) : class MangaDex(delegate: HttpSource, val context: Context) :
DelegatedHttpSource(delegate), DelegatedHttpSource(delegate),
MetadataSource<MangaDexSearchMetadata, Response>, MetadataSource<MangaDexSearchMetadata, Response>,
UrlImportableSource, // UrlImportableSource,
FollowsSource, FollowsSource,
LoginSource, LoginSource,
BrowseSourceFilterHeader, BrowseSourceFilterHeader,
RandomMangaSource { RandomMangaSource,
NamespaceSource {
override val lang: String = delegate.lang 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 { 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 preferences: PreferencesHelper by injectLazy()
val trackManager: TrackManager by injectLazy() val mdList: MdList by lazy {
Injekt.get<TrackManager>().mdList
private val sourcePreferences: SharedPreferences by lazy {
context.getSharedPreferences("source_$id", 0x0000)
} }
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) { urlImportFetchSearchManga(context, query) {
importIdToMdId(query) { importIdToMdId(query) {
super.fetchSearchManga(page, query, filters) 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 val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") { return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt()) "/manga/" + uri.pathSegments[1]
} else { } else {
null null
} }
@ -119,44 +114,43 @@ class MangaDex(delegate: HttpSource, val context: Context) :
override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? { override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? {
val id = uri.pathSegments.getOrNull(2) ?: return null 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) return MdUtil.mapMdIdToMangaUrl(mangaId)
} }*/
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { 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 { 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>> { 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> { 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>> { override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
return if (chapter.scanlator == "MangaPlus") { return if (chapter.scanlator == "MangaPlus") {
client.newCall(mangaPlusPageListRequest(chapter)) baseHttpClient.newCall(mangaPlusPageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .map { response ->
val chapterId = ApiChapterParser().externalParse(response) val chapterId = ApiChapterParser().externalParse(response)
MangaPlusHandler(client).fetchPageList(chapterId) MangaPlusHandler(baseHttpClient).fetchPageList(chapterId)
} }
} else super.fetchPageList(chapter) } else super.fetchPageList(chapter)
} }
private fun mangaPlusPageListRequest(chapter: SChapter): Request { private fun mangaPlusPageListRequest(chapter: SChapter): Request {
val urlChapterId = MdUtil.getChapterId(chapter.url) return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK)
return GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
} }
override fun fetchImage(page: Page): Observable<Response> { 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)) MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
.asObservableSuccess() .asObservableSuccess()
} else super.fetchImage(page) } else super.fetchImage(page)
@ -169,28 +163,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { 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 { 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 requiresLogin: Boolean = true
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED override val twoFactorAuth = LoginSource.AuthSupport.NOT_SUPPORTED
override fun isLogged(): Boolean { override fun isLogged(): Boolean {
val httpUrl = MdUtil.baseUrl.toHttpUrl() return mdList.isLogged
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
} }
override fun getUsername(): String { override fun getUsername(): String {
return trackManager.mdList.getUsername() return mdList.getUsername()
} }
override fun getPassword(): String { override fun getPassword(): String {
return trackManager.mdList.getPassword() return mdList.getPassword()
} }
override suspend fun login( override suspend fun login(
@ -198,96 +191,52 @@ class MangaDex(delegate: HttpSource, val context: Context) :
password: String, password: String,
twoFactorCode: String? twoFactorCode: String?
): Boolean { ): Boolean {
return withIOContext { val result = loginHelper.login(username, password)
val formBody = FormBody.Builder().apply { return if (result is MangaDexLoginHelper.LoginResult.Success) {
add("login_username", username) MdUtil.updateLoginToken(result.token, preferences, mdList)
add("login_password", password) mdList.saveCredentials(username, password)
add("no_js", "1") true
add("remember_me", "1") } else false
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)
}
}
} }
override suspend fun logout(): Boolean { override suspend fun logout(): Boolean {
return withIOContext { val result = try {
// https://mangadex.org/ajax/actions.ajax.php?function=logout loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList))
val httpUrl = MdUtil.baseUrl.toHttpUrl() } catch (e: NoSessionException) {
val listOfDexCookies = network.cookieManager.get(httpUrl) true
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
}
false
} }
return if (result) {
mdList.logout()
true
} else false
} }
override suspend fun fetchAllFollows(forceHd: Boolean): List<Pair<SManga, MangaDexSearchMetadata>> { override suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchAllFollows(forceHd) } return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchAllFollows()
} }
suspend fun updateReadingProgress(track: Track): Boolean { 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 { 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 { override suspend fun fetchTrackingInfo(url: String): Track {
return withIOContext { if (!isLogged()) {
if (!isLogged()) { throw Exception("Not Logged in")
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> { 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 { 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 { override fun getFilterHeader(controller: BaseController<*>): MangaDexFabHeaderAdapter {
@ -295,14 +244,14 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
override suspend fun fetchRandomMangaUrl(): String { 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) 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 { when {
query.toIntOrNull() != null -> { query.toIntOrNull() != null -> {
runAsObservable({ runAsObservable({
@ -320,11 +269,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
} }
} }
else -> fail() else -> fail()
} }*/
companion object { /*companion object {
private const val REMEMBER_ME = "mangadex_rememberme_token" private const val REMEMBER_ME = "mangadex_rememberme_token"
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault" private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
private const val LOW_QUALITY = 1 private const val LOW_QUALITY = 1
} }*/
} }

View File

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

View File

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

View File

@ -50,7 +50,7 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) :
// SY --> // SY -->
override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) {
if (metadata is MangaDexSearchMetadata) { 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.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it]
binding.localText.isVisible = true 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.debug.DebugToggles
import exh.eh.EHentaiUpdateHelper import exh.eh.EHentaiUpdateHelper
import exh.log.xLogD import exh.log.xLogD
import exh.log.xLogE
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.md.utils.scanlatorList
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.FlatMetadata
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import exh.source.isEhBasedSource import exh.source.isEhBasedSource
@ -184,11 +183,6 @@ class MangaPresenter(
.subscribeLatestCache({ view, (manga, flatMetadata) -> .subscribeLatestCache({ view, (manga, flatMetadata) ->
flatMetadata?.let { metadata -> flatMetadata?.let { metadata ->
view.onNextMetaInfo(metadata) view.onNextMetaInfo(metadata)
meta?.let {
it.filteredScanlators?.let {
if (chapters.isNotEmpty()) chaptersRelay.call(chapters)
}
}
} }
// SY <-- // SY <--
view.onNextMangaInfo(manga, source) view.onNextMangaInfo(manga, source)
@ -219,7 +213,7 @@ class MangaPresenter(
// Find downloaded chapters // Find downloaded chapters
setDownloadedChapters(chapters) setDownloadedChapters(chapters)
allChapterScanlators = chapters.flatMap { it.chapter.scanlatorList() }.toSet() allChapterScanlators = chapters.flatMap { MdUtil.getScanlators(it.chapter.scanlator) }.toSet()
// Store the last emission // Store the last emission
this.chapters = chapters this.chapters = chapters
@ -307,6 +301,7 @@ class MangaPresenter(
withUIContext { view?.onFetchMangaInfoDone() } withUIContext { view?.onFetchMangaInfoDone() }
} catch (e: Throwable) { } catch (e: Throwable) {
xLogE("Error getting manga details", e)
withUIContext { view?.onFetchMangaInfoError(e) } withUIContext { view?.onFetchMangaInfoError(e) }
} }
} }
@ -840,11 +835,9 @@ class MangaPresenter(
} }
// SY --> // SY -->
meta?.let { metadata -> manga.filtered_scanlators?.let { filteredScanlatorString ->
metadata.filteredScanlators?.let { filteredScanlatorString -> val filteredScanlators = MdUtil.getScanlators(filteredScanlatorString)
val filteredScanlators = MdUtil.getScanlators(filteredScanlatorString) observable = observable.filter { MdUtil.getScanlators(it.scanlator).any { group -> filteredScanlators.contains(group) } }
observable = observable.filter { it.scanlatorList().any { group -> filteredScanlators.contains(group) } }
}
} }
// SY <-- // SY <--
@ -1043,12 +1036,10 @@ class MangaPresenter(
} }
// SY --> // SY -->
suspend fun setScanlatorFilter(filteredScanlators: Set<String>) { fun setScanlatorFilter(filteredScanlators: Set<String>) {
val meta = meta ?: return val manga = manga
meta.filteredScanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators) manga.filtered_scanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators)
meta.flatten().let { db.updateMangaFilteredScanlators(manga).executeAsBlocking()
db.insertFlatMetadataAsync(it).await()
}
refreshChapters() refreshChapters()
} }
// SY <-- // SY <--

View File

@ -9,18 +9,14 @@ import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Router import com.bluelinelabs.conductor.Router
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga 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.ui.manga.MangaPresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext 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.util.view.popupMenu
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata
import exh.source.getMainSource
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
class ChaptersSettingsSheet( class ChaptersSettingsSheet(
@ -88,7 +84,7 @@ class ChaptersSettingsSheet(
* Returns true if there's at least one filter from [FilterGroup] active. * Returns true if there's at least one filter from [FilterGroup] active.
*/ */
fun hasActiveFilters(): Boolean { 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 { 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) private val scanlatorFilters = Item.DrawableSelection(0, this, R.string.scanlator, R.drawable.ic_outline_people_alt_24dp)
override val header = null 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 val footer = null
override fun initModels() { override fun initModels() {
@ -116,16 +112,8 @@ class ChaptersSettingsSheet(
override fun onItemClicked(item: Item) { override fun onItemClicked(item: Item) {
if (item is Item.DrawableSelection) { 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 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() val preselected = if (filteredScanlators.isNullOrEmpty()) scanlators.mapIndexed { index, _ -> index }.toIntArray() else filteredScanlators.map { scanlators.indexOf(it) }.toIntArray()
MaterialDialog(context) MaterialDialog(context)

View File

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

View File

@ -3,6 +3,7 @@ package exh
import android.content.Context import android.content.Context
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.BuildConfig 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.resolvers.MangaUrlPutResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable 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.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -242,7 +244,6 @@ object EXHMigrations {
// UpdaterJob.cancelTask(context) // UpdaterJob.cancelTask(context)
// } // }
} }
if (oldVersion under 17) { if (oldVersion under 17) {
// Migrate Rotation and Viewer values to default values for viewer_flags // Migrate Rotation and Viewer values to default values for viewer_flags
val prefs = PreferenceManager.getDefaultSharedPreferences(context) val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -264,6 +265,15 @@ object EXHMigrations {
putInt("pref_default_reading_mode_key", newReadingMode) putInt("pref_default_reading_mode_key", newReadingMode)
remove("pref_default_viewer_key") 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) // 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.network.parseAs
import eu.kanade.tachiyomi.source.model.Page 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 exh.md.utils.MdUtil
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
@ -10,18 +10,26 @@ import kotlinx.serialization.json.jsonPrimitive
import okhttp3.Response import okhttp3.Response
class ApiChapterParser { class ApiChapterParser {
// Only used in [PageHandler], which means its currently unused, kept for reference fun pageListParse(response: Response, host: String, dataSaver: Boolean): List<Page> {
fun pageListParse(response: Response): List<Page> { val networkApiChapter = response.parseAs<ChapterResponse>(MdUtil.jsonParser)
val networkApiChapter = response.parseAs<ApiChapterSerializer>(MdUtil.jsonParser)
val hash = networkApiChapter.data.hash val pages = mutableListOf<Page>()
val pageArray = networkApiChapter.data.pages
val server = networkApiChapter.data.server
return pageArray.mapIndexed { index, page -> val atHomeRequestUrl = response.request.url.toUrl().toString()
val url = "$hash/$page"
Page(index, "$server,${response.request.url},${System.currentTimeMillis()}", url) 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 { fun externalParse(response: Response): String {

View File

@ -1,34 +1,32 @@
package exh.md.handlers package exh.md.handlers
import com.elvishew.xlog.XLog
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.network.parseAs
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.asJsoup
import exh.log.xLogE import exh.log.xLogE
import exh.md.handlers.serializers.ApiChapterSerializer import exh.md.handlers.serializers.AuthorResponseList
import exh.md.handlers.serializers.ApiMangaSerializer import exh.md.handlers.serializers.ChapterResponse
import exh.md.handlers.serializers.ChapterSerializer import exh.md.handlers.serializers.MangaResponse
import exh.md.utils.MdLang
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.RaisedTag
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadata import exh.metadata.metadata.base.insertFlatMetadata
import exh.metadata.metadata.base.insertFlatMetadataCompletable
import exh.util.executeOnIO import exh.util.executeOnIO
import exh.util.floor import exh.util.floor
import exh.util.nullIfZero import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import rx.Completable
import rx.Single
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.api.get import java.util.Date
import java.util.Locale
class ApiMangaParser(private val lang: String) { class ApiMangaParser(val client: OkHttpClient, private val lang: String) {
val db: DatabaseHelper get() = Injekt.get() val db: DatabaseHelper by injectLazy()
val metaClass = MangaDexSearchMetadata::class val metaClass = MangaDexSearchMetadata::class
@ -40,44 +38,18 @@ class ApiMangaParser(private val lang: String) {
}?.call() }?.call()
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!") ?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
/** suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List<String>, sourceId: Long): MangaInfo {
* Parses metadata from the input and then copies it into the manga return parseToManga(manga, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls, sourceId)
*
* 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 { suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, coverUrls: List<String>, sourceId: Long): MangaInfo {
val mangaId = db.getManga(manga.key, sourceId).executeOnIO()?.id val mangaId = db.getManga(manga.key, sourceId).executeOnIO()?.id
val metadata = if (mangaId != null) { val metadata = if (mangaId != null) {
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO() val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO()
flatMetadata?.raise(metaClass) ?: newMetaInstance() flatMetadata?.raise(metaClass) ?: newMetaInstance()
} else newMetaInstance() } else newMetaInstance()
parseInfoIntoMetadata(metadata, input, coverUrls) parseIntoMetadata(metadata, input, coverUrls)
if (mangaId != null) { if (mangaId != null) {
metadata.mangaId = mangaId metadata.mangaId = mangaId
db.insertFlatMetadata(metadata.flatten()) db.insertFlatMetadata(metadata.flatten())
@ -86,69 +58,82 @@ class ApiMangaParser(private val lang: String) {
return metadata.createMangaInfo(manga) 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>) { 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) { with(metadata) {
try { try {
val networkApiManga = input.parseAs<ApiMangaSerializer>(MdUtil.jsonParser) val networkManga = networkApiManga.data.attributes
val networkManga = networkApiManga.data.manga mdUuid = networkApiManga.data.id
mdId = MdUtil.getMangaId(input.request.url.toString()) title = MdUtil.cleanString(networkManga.title[lang] ?: networkManga.title["en"]!!)
mdUrl = input.request.url.toString() altTitles = networkManga.altTitles.mapNotNull { it[lang] }
title = MdUtil.cleanString(networkManga.title) cover =
thumbnail_url = if (coverUrls.isNotEmpty()) { if (coverUrls.isNotEmpty()) {
coverUrls.last() coverUrls.last()
} else { } 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 { description = MdUtil.cleanDescription(networkManga.description["en"]!!)
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)
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 = val publishedOrCancelled =
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) { /*if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
status = SManga.COMPLETED manga.status = SManga.COMPLETED
missing_chapters = null manga.missing_chapters = null
maxChapterNumber = networkApiManga.data.manga.lastChapter?.toDoubleOrNull()?.floor() } else {*/
} else { status = tempStatus
status = tempStatus // }
}
val genres = // things that will go with the genre tags but aren't actually genre
networkManga.tags.mapNotNull { FilterHandler.allTypes[it.toString()] } val nonGenres = listOfNotNull(
.toMutableList() 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 genres = nonGenres + networkManga.tags
val demographic = FilterHandler.demographics().firstOrNull { it.id.toInt() == demographicInt } .mapNotNull { dexTag ->
dexTag.attributes.name[lang] ?: dexTag.attributes.name["en"]
if (demographic != null) { }.map {
genres.add(0, demographic.name) RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT)
} }
}
if (networkManga.isHentai) {
genres.add("Hentai")
}
if (tags.isNotEmpty()) tags.clear() if (tags.isNotEmpty()) tags.clear()
tags += genres.map { RaisedTag(null, it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT) } tags += genres
} catch (e: Exception) { } catch (e: Exception) {
xLogE("Parse into metadata error", e) xLogE("Parse into metadata error", e)
throw 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 * If chapter title is oneshot or a chapter exists which matches the last chapter in the required language
* return manga is complete * return manga is complete
*/ */
private fun isMangaCompleted( /*private fun isMangaCompleted(
serializer: ApiMangaSerializer, serializer: ApiMangaSerializer,
filteredChapters: List<ChapterSerializer> filteredChapters: List<ChapterSerializer>
): Boolean { ): Boolean {
val finalChapterNumber = serializer.data.manga.lastChapter if (filteredChapters.isEmpty() || serializer.data.manga.lastChapter.isNullOrEmpty()) {
if (filteredChapters.isEmpty() || finalChapterNumber.isNullOrEmpty()) {
return false return false
} }
// just to fix the stupid lint val finalChapterNumber = serializer.data.manga.lastChapter!!
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) { if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
filteredChapters.firstOrNull()?.let { filteredChapters.firstOrNull()?.let {
if (isOneShot(it, finalChapterNumber)) { if (isOneShot(it, finalChapterNumber)) {
@ -177,36 +161,39 @@ class ApiMangaParser(private val lang: String) {
} }
} }
val removeOneshots = filteredChapters.asSequence() val removeOneshots = filteredChapters.asSequence()
.map { it.chapter?.toDoubleOrNull()?.floor()?.nullIfZero() } .map { it.chapter!!.toDoubleOrNull() }
.filterNotNull() .filter { it != null }
.map { floor(it!!).toInt() }
.filter { it != 0 }
.toList().distinctBy { it } .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> {
return serializer.data.chapters.asSequence() serializer.data.chapters ?: return emptyList()
.filter { lang == it.language } return serializer.data.chapters.asSequence()
.filter { .filter { langs.contains(it.language) }
it.chapter?.let { chapterNumber -> .filter {
if (chapterNumber.toDoubleOrNull() == null) { it.chapter?.let { chapterNumber ->
return@filter false if (chapterNumber.toDoubleOrNull() == null) {
} return@filter false
return@filter true }
} return@filter true
return@filter false }
}.toList() 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) || return chapter.title.equals("oneshot", true) ||
((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) ((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber))
} }*/
private fun parseStatus(status: Int) = when (status) { private fun parseStatus(status: String) = when (status) {
1 -> SManga.ONGOING "ongoing" -> SManga.ONGOING
2 -> SManga.PUBLICATION_COMPLETE "complete" -> SManga.PUBLICATION_COMPLETE
3 -> SManga.CANCELLED "abandoned" -> SManga.CANCELLED
4 -> SManga.HIATUS "hiatus" -> SManga.HIATUS
else -> SManga.UNKNOWN else -> SManga.UNKNOWN
} }
@ -214,88 +201,69 @@ class ApiMangaParser(private val lang: String) {
* Parse for the random manga id from the [MdUtil.randMangaPage] response. * Parse for the random manga id from the [MdUtil.randMangaPage] response.
*/ */
fun randomMangaIdParse(response: Response): String { fun randomMangaIdParse(response: Response): String {
val randMangaUrl = response.asJsoup() return response.parseAs<MangaResponse>(MdUtil.jsonParser).data.id
.select("link[rel=canonical]")
.attr("href")
return MdUtil.getMangaId(randMangaUrl)
} }
fun chapterListParse(response: Response): List<ChapterInfo> { fun chapterListParse(chapterListResponse: List<ChapterResponse>, groupMap: Map<String, String>): List<ChapterInfo> {
return chapterListParse(response.parseAs<ApiMangaSerializer>(MdUtil.jsonParser)) 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> { fun chapterParseForMangaId(response: Response): String {
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 {
try { 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) { } catch (e: Exception) {
xLogE("Parse for manga id error", e) XLog.e(e)
throw e throw e
} }
} }
private fun mapChapter( private fun mapChapter(
networkChapter: ChapterSerializer, networkChapter: ChapterResponse,
finalChapterNumber: String?, groups: Map<String, String>,
status: Int,
chapLang: MdLang?,
totalChapterCount: Int,
groups: Map<Long, String>
): ChapterInfo { ): ChapterInfo {
val key = MdUtil.oldApiChapter + networkChapter.id val chapter = SChapter.create()
val attributes = networkChapter.data.attributes
// Build chapter name val key = MdUtil.chapterSuffix + networkChapter.data.id
val chapterName = mutableListOf<String>() val chapterName = mutableListOf<String>()
// Build chapter name
if (!networkChapter.volume.isNullOrBlank()) { if (attributes.volume != null) {
val vol = "Vol." + networkChapter.volume val vol = "Vol." + attributes.volume
chapterName.add(vol) chapterName.add(vol)
// todo // todo
// chapter.vol = vol // chapter.vol = vol
} }
if (!networkChapter.chapter.isNullOrBlank()) { if (attributes.chapter.isNullOrBlank().not()) {
val chp = "Ch." + networkChapter.chapter
chapterName.add(chp)
// chapter.chapter_txt = chp
}
if (!networkChapter.title.isNullOrBlank()) {
if (chapterName.isNotEmpty()) { if (chapterName.isNotEmpty()) {
chapterName.add("-") chapterName.add("-")
} }
// todo val chp = "Ch.${attributes.chapter}"
chapterName.add(networkChapter.title) chapterName.add(chp)
// chapter.chapter_title = MdUtil.cleanString(networkChapter.title) // 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 volume, chapter and title is empty its a oneshot
if (chapterName.isEmpty()) { if (chapterName.isEmpty()) {
chapterName.add("Oneshot") chapterName.add("Oneshot")
} }
if ((status == 2 || status == 3)) { /*if ((status == 2 || status == 3)) {
if (finalChapterNumber != null) { if (finalChapterNumber != null) {
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) || if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0 networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
@ -303,26 +271,25 @@ class ApiMangaParser(private val lang: String) {
chapterName.add("[END]") chapterName.add("[END]")
} }
} }
} }*/
val name = MdUtil.cleanString(chapterName.joinToString(" ")) val name = MdUtil.cleanString(chapterName.joinToString(" "))
// Convert from unix time // Convert from unix time
val dateUpload = networkChapter.timestamp * 1000 val dateUpload = MdUtil.parseDate(attributes.publishAt)
val scanlatorName = mutableSetOf<String>()
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 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( return ChapterInfo(
key = key, key = key,
name = name, name = name,
scanlator = scanlator,
dateUpload = dateUpload, dateUpload = dateUpload,
scanlator = scanlator
) )
} }
} }

View File

@ -1,180 +1,261 @@
package exh.md.handlers package exh.md.handlers
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList 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) internal fun getMDFilterList(): FilterList {
class Tag(val id: String, name: String) : Filter.TriState(name) val filters = mutableListOf(
class Switch(val id: String, name: String) : Filter.CheckBox(name) OriginalLanguageList(getOriginalLanguage()),
class ContentList(contents: List<Tag>) : Filter.Group<Tag>("Content", contents) DemographicList(getDemographics()),
class FormatList(formats: List<Tag>) : Filter.Group<Tag>("Format", formats) StatusList(getStatus()),
class GenreList(genres: List<Tag>) : Filter.Group<Tag>("Genres", genres) SortFilter(sortableList.map { it.first }.toTypedArray()),
class PublicationStatusList(statuses: List<Switch>) : Filter.Group<Switch>("Publication Status", statuses) TagList(getTags()),
class DemographicList(demographics: List<Switch>) : Filter.Group<Switch>("Demographic", demographics) TagInclusionMode(),
TagExclusionMode()
).toMutableList()
class R18 : Filter.Select<String>("R18+", arrayOf("Default", "Show all", "Show only", "Show none")) if (true) { // preferencesHelper.showR18Filter()) {
class ThemeList(themes: List<Tag>) : Filter.Group<Tag>("Themes", themes) filters.add(2, ContentRatingList(getContentRating()))
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( return FilterList(list = filters.toList())
"Sort", }
sortables().map { it.first }.toTypedArray(),
Selection(0, false) 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")
) )
class OriginalLanguage : Filter.Select<String>("Original Language", sourceLang().map { it.first }.toTypedArray()) private class Status(name: String) : Filter.CheckBox(name)
private class StatusList(status: List<Status>) :
Filter.Group<Status>("Status", status)
fun getFilterList() = FilterList( private fun getStatus() = listOf(
TextField("Author", "author"), Status("Onging"),
TextField("Artist", "artist"), Status("Completed"),
R18(), Status("Hiatus"),
SortFilter(), Status("Abandoned"),
DemographicList(demographics()),
PublicationStatusList(publicationStatus()),
OriginalLanguage(),
ContentList(contentType()),
FormatList(formats()),
GenreList(genre()),
ThemeList(themes()),
TagInclusionMode(),
TagExclusionMode()
) )
companion object { private class ContentRating(name: String) : Filter.CheckBox(name)
fun demographics() = listOf( private class ContentRatingList(contentRating: List<ContentRating>) :
Switch("1", "Shounen"), Filter.Group<ContentRating>("Content Rating", contentRating)
Switch("2", "Shoujo"),
Switch("3", "Seinen"),
Switch("4", "Josei")
)
fun publicationStatus() = listOf( private fun getContentRating() = listOf(
Switch("1", "Ongoing"), ContentRating("Safe"),
Switch("2", "Completed"), ContentRating("Suggestive"),
Switch("3", "Cancelled"), ContentRating("Erotica"),
Switch("4", "Hiatus") ContentRating("Pornographic")
) )
fun sortables() = listOf( private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name)
Triple("Update date", 1, 0), private class OriginalLanguageList(originalLanguage: List<OriginalLanguage>) :
Triple("Alphabetically", 2, 3), Filter.Group<OriginalLanguage>("Original language", originalLanguage)
Triple("Number of comments", 4, 5),
Triple("Rating", 6, 7),
Triple("Views", 8, 9),
Triple("Follows", 10, 11)
)
fun sourceLang() = listOf( private fun getOriginalLanguage() = listOf(
Pair("All", "0"), OriginalLanguage("Japanese (Manga)", "jp"),
Pair("Japanese", "2"), OriginalLanguage("Chinese (Manhua)", "cn"),
Pair("English", "1"), OriginalLanguage("Korean (Manhwa)", "kr"),
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")
)
fun contentType() = listOf( internal class Tag(val id: String, name: String) : Filter.TriState(name)
Tag("9", "Ecchi"), private class TagList(tags: List<Tag>) : Filter.Group<Tag>("Tags", tags)
Tag("32", "Smut"),
Tag("49", "Gore"),
Tag("50", "Sexual Violence")
).sortedWith(compareBy { it.name })
fun formats() = listOf( internal fun getTags() = listOf(
Tag("1", "4-koma"), Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"),
Tag("4", "Award Winning"), Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", "Adaptation"),
Tag("7", "Doujinshi"), Tag("87cc87cd-a395-47af-b27a-93258283bbc6", "Adventure"),
Tag("21", "Oneshot"), Tag("e64f6742-c834-471d-8d72-dd51fc02b835", "Aliens"),
Tag("36", "Long Strip"), Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", "Animals"),
Tag("42", "Adaptation"), Tag("51d83883-4103-437c-b4b1-731cb73d786c", "Anthology"),
Tag("43", "Anthology"), Tag("0a39b5a1-b235-4886-a747-1d05d216532d", "Award Winning"),
Tag("44", "Web Comic"), Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", "Boy Love"),
Tag("45", "Full Color"), Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"),
Tag("46", "User Created"), Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"),
Tag("47", "Official Colored"), Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"),
Tag("48", "Fan Colored") Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Crossdressing"),
).sortedWith(compareBy { it.name }) 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 genre() = listOf( private class TagInclusionMode :
Tag("2", "Action"), Filter.Select<String>("Included tags mode", arrayOf("And", "Or"), 0)
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 })
fun themes() = listOf( private class TagExclusionMode :
Tag("6", "Cooking"), Filter.Select<String>("Excluded tags mode", arrayOf("And", "Or"), 1)
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 })
val allTypes = (contentType() + formats() + genre() + themes()).map { it.id to it.name }.toMap() val sortableList = listOf(
Pair("Default (Asc/Desc doesn't matter)", ""),
Pair("Created at", "createdAt"),
Pair("Updated at", "updatedAt"),
)
class SortFilter(sortables: Array<String>) : Filter.Sort("Sort", sortables, Selection(0, false))
fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList): String {
url.apply {
// add filters
filters.forEach { filter ->
when (filter) {
is OriginalLanguageList -> {
filter.state.forEach { lang ->
if (lang.state) {
addQueryParameter(
"originalLanguage[]",
lang.isoCode
)
}
}
}
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,202 +3,203 @@ package exh.md.handlers
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager 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.GET
import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs 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.MetadataMangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.log.xLogD import exh.md.handlers.serializers.MangaListResponse
import exh.log.xLogE import exh.md.handlers.serializers.MangaResponse
import exh.md.handlers.serializers.FollowPage import exh.md.handlers.serializers.UpdateReadingStatus
import exh.md.handlers.serializers.FollowsIndividualSerializer
import exh.md.handlers.serializers.FollowsPageSerializer
import exh.md.utils.FollowStatus import exh.md.utils.FollowStatus
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.util.awaitResponse import exh.util.under
import exh.util.floor import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Call
import okhttp3.FormBody
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response 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 { suspend fun fetchFollows(): MetadataMangasPage {
return client.newCall(followsListRequest()) return withIOContext {
.await() val response = client.newCall(followsListRequest(0)).await()
.let { response ->
followsParseMangaPage(response) 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)
}
} }
/** /**
* Parse follows api to manga page * Parse follows api to manga page
* used when multiple follows * used when multiple follows
*/ */
private fun followsParseMangaPage(response: Response, forceHd: Boolean = false): MetadataMangasPage { private fun followsParseMangaPage(response: List<MangaResponse>, statusListResponse: JsonObject): MetadataMangasPage {
val followsPageResult = try { val comparator = compareBy<Pair<MangaInfo, MangaDexSearchMetadata>> { it.second.followStatus }
MdUtil.jsonParser.decodeFromString( .thenBy { it.first.title }
response.body?.string().orEmpty() val result = response.map {
) MdUtil.createMangaEntry(it, lang, useLowQualityCovers) to MangaDexSearchMetadata().apply {
} catch (e: Exception) { followStatus = getFollowStatus(statusListResponse, it.data.id).int
xLogE("error parsing follows", e) }
FollowsPageSerializer(404, emptyList()) }.sortedWith(comparator)
}
if (followsPageResult.data.isNullOrEmpty() || followsPageResult.code != 200) { return MetadataMangasPage(result.map { it.first.toSManga() }, false, result.map { it.second })
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 })
} }
/** /**
* fetch follow status used when fetching status for 1 manga * fetch follow status used when fetching status for 1 manga
*/ */
private fun followStatusParse(response: Response): Track { private fun followStatusParse(response: Response, statusListResponse: JsonObject): Track {
val followsPageResult = try { val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
response.parseAs<FollowsIndividualSerializer>(MdUtil.jsonParser)
} catch (e: Exception) {
xLogE("error parsing follows", e)
throw e
}
val track = Track.create(TrackManager.MDLIST) val track = Track.create(TrackManager.MDLIST)
if (followsPageResult.code == 404) { track.status = getFollowStatus(statusListResponse, mangaResponse.data.id).int
track.status = FollowStatus.UNFOLLOWED.int track.tracking_url = MdUtil.baseUrl + "/manga/" + mangaResponse.data.id
} else { track.title = mangaResponse.data.attributes.title[lang] ?: mangaResponse.data.attributes.title["en"]!!
val follow = followsPageResult.data ?: throw Exception("Invalid response ${followsPageResult.code}")
track.status = follow.followType /* if (follow.chapter.isNotBlank()) {
if (follow.chapter.isNotBlank()) {
track.last_chapter_read = follow.chapter.toFloat().floor() track.last_chapter_read = follow.chapter.toFloat().floor()
} }*/
track.tracking_url = MdUtil.baseUrl + follow.mangaId.toString()
track.title = follow.mangaTitle
}
return track return track
} }
/** /**
* build Request for follows page * build Request for follows page
*/ */
private fun followsListRequest(): Request { private fun followsListRequest(offset: Int): Request {
return GET("${MdUtil.apiUrl}${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK) val tempUrl = MdUtil.userFollows.toHttpUrl().newBuilder()
}
/** tempUrl.apply {
* Parse result element to manga addQueryParameter("limit", MdUtil.mangaLimit.toString())
*/ addQueryParameter("offset", offset.toString())
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
} }
return GET(tempUrl.build().toString(), MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK)
} }
/** /**
* Change the status of a manga * Change the status of a manga
*/ */
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean { suspend fun updateFollowStatus(mangaId: String, followStatus: FollowStatus): Boolean {
return withIOContext { return withIOContext {
if (followStatus == FollowStatus.UNFOLLOWED) { val status = when (followStatus == FollowStatus.UNFOLLOWED) {
client.newCall( true -> null
GET( false -> followStatus.name.toLowerCase(Locale.US)
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_unfollow&id=$mangaID&type=$mangaID", }
headers,
CacheControl.FORCE_NETWORK 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 { ).await()
val status = followStatus.int postResult.isSuccessful
client.newCall(
GET(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_follow&id=$mangaID&type=$status",
headers,
CacheControl.FORCE_NETWORK
)
)
}.succeeded()
} }
} }
suspend fun updateReadingProgress(track: Track): Boolean { suspend fun updateReadingProgress(track: Track): Boolean {
return withIOContext { return true
val mangaID = MdUtil.getMangaId(track.tracking_url) /*return withIOContext {
val mangaID = getMangaId(track.tracking_url)
val formBody = FormBody.Builder() val formBody = FormBody.Builder()
.add("volume", "0") .add("volume", "0")
.add("chapter", track.last_chapter_read.toString()) .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())
client.newCall( val result = runCatching {
POST( client.newCall(
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=edit_progress&id=$mangaID", POST(
headers, "$baseUrl/ajax/actions.ajax.php?function=edit_progress&id=$mangaID",
formBody.build() 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 { suspend fun updateRating(track: Track): Boolean {
return withIOContext { return true
val mangaID = MdUtil.getMangaId(track.tracking_url) /*return withIOContext {
client.newCall( val mangaID = getMangaId(track.tracking_url)
GET( val result = runCatching {
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}", client.newCall(
headers GET(
"$baseUrl/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}",
headers
)
) )
).succeeded() .execute()
} }
}
private suspend fun Call.succeeded() = withIOContext { result.exceptionOrNull()?.let {
try { if (it is EOFException) {
await().body?.string().let { body -> return@withIOContext true
(body != null && body.isEmpty()).also { } else {
if (!it) xLogD(body) XLog.e("error updating rating", it)
return@withIOContext false
} }
} }
} catch (e: EOFException) { result.isSuccess
true }*/
}
} }
/** /**
* fetch all manga from all possible pages * 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 { return withIOContext {
val response = client.newCall(followsListRequest()).await() val metadata: List<MangaDexSearchMetadata>
val mangasPage = followsParseMangaPage(response, forceHd) fetchFollows().also { metadata = it.mangasMetadata.filterIsInstance<MangaDexSearchMetadata>() }.mangas.mapIndexed { index, manga ->
mangasPage.mangas.mapIndexed { index, sManga -> manga to metadata[index]
sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata
} }
} }
} }
@ -206,12 +207,20 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
suspend fun fetchTrackingInfo(url: String): Track { suspend fun fetchTrackingInfo(url: String): Track {
return withIOContext { return withIOContext {
val request = GET( val request = GET(
MdUtil.apiUrl + MdUtil.followsMangaApi + MdUtil.getMangaId(url), MdUtil.mangaUrl + "/" + MdUtil.getMangaId(url),
headers, MdUtil.getAuthHeaders(headers, preferences, mdList),
CacheControl.FORCE_NETWORK CacheControl.FORCE_NETWORK
) )
val response = client.newCall(request).awaitResponse() val response = client.newCall(request).await()
followStatusParse(response) 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 package exh.md.handlers
import eu.kanade.tachiyomi.data.database.models.Track 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.GET
import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.asObservableSuccess
import eu.kanade.tachiyomi.network.await 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.SManga
import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toMangaInfo
import eu.kanade.tachiyomi.source.model.toSChapter 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.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import exh.md.handlers.serializers.ApiCovers import exh.md.handlers.serializers.ChapterListResponse
import exh.md.handlers.serializers.ApiMangaSerializer import exh.md.handlers.serializers.ChapterResponse
import exh.md.handlers.serializers.GroupListResponse
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.MangaDexSearchMetadata
import exh.util.under
import kotlinx.coroutines.async import kotlinx.coroutines.async
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
@ -26,124 +30,179 @@ import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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>> { suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<ChapterInfo>> {
return withIOContext { 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 covers = getCovers(manga, forceLatestCovers)
val parser = ApiMangaParser(lang) val parser = ApiMangaParser(client, lang)
// TODO fix this parser.parseToManga(manga, response, covers, sourceId) to getChapterList(manga)
/*val mangaInfo = parser.parseToManga(manga, response, covers, sourceId)
val chapterList = parser.chapterListParse(apiNetworkManga)
mangaInfo to chapterList*/
manga to emptyList()
} }
} }
private suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> { suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> {
return if (forceLatestCovers) { /* if (forceLatestCovers) {
val covers = client.newCall(coverRequest(manga)).await().parseAs<ApiCovers>(MdUtil.jsonParser) val covers = client.newCall(coverRequest(manga)).await().parseAs<ApiCovers>(MdUtil.jsonParser)
covers.data.map { it.url } return covers.data.map { it.url }
} else { } else {*/
emptyList() return emptyList<String>()
} // }
} }
suspend fun getMangaIdFromChapterId(urlChapterId: String): Int { suspend fun getMangaIdFromChapterId(urlChapterId: String): String {
return withIOContext { 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() val response = client.newCall(request).await()
ApiMangaParser(lang).chapterParseForMangaId(response) ApiMangaParser(client, lang).chapterParseForMangaId(response)
} }
} }
suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long): MangaInfo { suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long): MangaInfo {
return withIOContext { return withIOContext {
val response = client.newCall(apiRequest(manga)).await() val response = client.newCall(mangaRequest(manga)).await()
val covers = getCovers(manga, forceLatestCovers) 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> { fun fetchMangaDetailsObservable(manga: SManga, sourceId: Long): Observable<SManga> {
return client.newCall(apiRequest(manga.toMangaInfo())) return client.newCall(mangaRequest(manga.toMangaInfo()))
.asObservableSuccess() .asObservableSuccess()
.flatMap { response -> .flatMap { response ->
runAsObservable({ runAsObservable({
getCovers(manga.toMangaInfo(), forceLatestCovers) ApiMangaParser(client, lang).parseToManga(manga.toMangaInfo(), response, emptyList(), sourceId).toSManga()
}).map { })
response to it
}
}
.flatMap {
ApiMangaParser(lang).parseToManga(manga, it.first, it.second).andThen(
Observable.just(
manga.apply {
initialized = true
}
)
)
} }
} }
fun fetchChapterListObservable(manga: SManga): Observable<List<SChapter>> { fun fetchChapterListObservable(manga: SManga): Observable<List<SChapter>> {
return client.newCall(apiRequest(manga.toMangaInfo())) return client.newCall(mangaFeedRequest(manga.toMangaInfo(), 0, lang))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .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> { suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
return withIOContext { return withIOContext {
val response = client.newCall(apiRequest(manga)).await() val chapterListResponse = client.newCall(mangaFeedRequest(manga, 0, lang)).await().parseAs<ChapterListResponse>(MdUtil.jsonParser)
ApiMangaParser(lang).chapterListParse(response) 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> { private suspend fun getGroupMap(results: List<ChapterResponse>): Map<String, String> {
return client.newCall(randomMangaRequest()) val groupIds = results.map { chapter -> chapter.relationships }.flatten().filter { it.type == "scanlation_group" }.map { it.id }.distinct()
.asObservableSuccess() val groupMap = runCatching {
.map { response -> groupIds.chunked(100).mapIndexed { index, ids ->
ApiMangaParser(lang).randomMangaIdParse(response) 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 { suspend fun fetchRandomMangaId(): String {
return withIOContext { return withIOContext {
val response = client.newCall(randomMangaRequest()).await() 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 { return withIOContext {
val metadata = async { 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 manga = MangaInfo(mangaUrl, track.title)
val response = client.newCall(apiRequest(manga)).await() val response = client.newCall(mangaRequest(manga)).await()
val metadata = MangaDexSearchMetadata() val metadata = MangaDexSearchMetadata()
ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList()) ApiMangaParser(client, lang).parseIntoMetadata(metadata, response, emptyList())
metadata 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() remoteTrack.await() to metadata.await()
} }
} }
private fun randomMangaRequest(): Request { 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 { private fun mangaRequest(manga: MangaInfo): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK) return GET(MdUtil.mangaUrl + "/" + MdUtil.getMangaId(manga.key), headers, CacheControl.FORCE_NETWORK)
} }
private fun coverRequest(manga: MangaInfo): Request { private fun mangaFeedRequest(manga: MangaInfo, offset: Int, lang: String): Request {
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK) 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 okhttp3.Request
import rx.Observable import rx.Observable
// Unused, kept for reference todo class PageHandler(val client: OkHttpClient, val headers: Headers, private val dataSaver: Boolean) {
class PageHandler(val client: OkHttpClient, val headers: Headers, private val imageServer: String, val dataSaver: String?) {
fun fetchPageList(chapter: SChapter): Observable<List<Page>> { fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
if (chapter.scanlator.equals("MangaPlus")) { 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)) return client.newCall(pageListRequest(chapter))
.asObservableSuccess() .asObservableSuccess()
.map { response -> .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 { private fun pageListRequest(chapter: SChapter): Request {
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix) return GET("${MdUtil.chapterUrl}${MdUtil.getChapterId(chapter.url)}", headers, CacheControl.FORCE_NETWORK)
return GET("${MdUtil.apiUrl}${chpUrl}${MdUtil.apiChapterSuffix}&server=$imageServer&saver=$dataSaver", 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.GET
import eu.kanade.tachiyomi.network.asObservableSuccess 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.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.asJsoup import exh.md.handlers.serializers.MangaListResponse
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import exh.md.utils.setMDUrlWithoutDomain
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
// Unused, kept for reference todo
/** /**
* Returns the latest manga from the updates url since it actually respects the users settings * 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> { fun fetchPopularManga(page: Int): Observable<MangasPage> {
return client.newCall(popularMangaRequest(page)) 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 { 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 { private fun popularMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
val mangas = document.select(popularMangaSelector).map { element -> val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() }
popularMangaFromElement(element) return MangasPage(mangaList, hasMoreResults)
}.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)"
} }
} }

View File

@ -2,198 +2,72 @@ package exh.md.handlers
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.asObservableSuccess 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.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.asJsoup 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.MdUtil
import exh.md.utils.setMDUrlWithoutDomain
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Headers import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import org.jsoup.nodes.Element
import rx.Observable import rx.Observable
// Unused, kept for reference todo class SearchHandler(val client: OkHttpClient, private val headers: Headers, val lang: String, val filterHandler: FilterHandler, private val useLowQualityCovers: Boolean) {
class SearchHandler(val client: OkHttpClient, private val headers: Headers, val lang: String, private val useLowQualityCovers: Boolean) {
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { fun fetchSearchManga(page: Int, query: String, filters: FilterList, sourceId: Long): Observable<MangasPage> {
return when { return if (query.startsWith(PREFIX_ID_SEARCH)) {
query.startsWith(PREFIX_ID_SEARCH) -> { val realQuery = query.removePrefix(PREFIX_ID_SEARCH)
val realQuery = query.removePrefix(PREFIX_ID_SEARCH) client.newCall(searchMangaByIdRequest(realQuery))
client.newCall(searchMangaByIdRequest(realQuery)) .asObservableSuccess()
.asObservableSuccess() .flatMap { response ->
.map { response -> runAsObservable({
val details = SManga.create() val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
details.url = "/manga/$realQuery/" val details = ApiMangaParser(client, lang)
ApiMangaParser(lang).parseToManga(details, response, emptyList()).await() .parseToManga(MdUtil.createMangaEntry(mangaResponse, lang, useLowQualityCovers), response, emptyList(), sourceId).toSManga()
MangasPage(listOf(details), false) MangasPage(listOf(details), false)
} })
} }
query.startsWith(PREFIX_GROUP_SEARCH) -> { } else {
val realQuery = query.removePrefix(PREFIX_GROUP_SEARCH) client.newCall(searchMangaRequest(page, query, filters))
client.newCall(searchMangaByGroupRequest(realQuery)) .asObservableSuccess()
.asObservableSuccess() .map { response ->
.map { response -> searchMangaParse(response)
response.asJsoup().select(groupSelector).firstOrNull()?.attr("abs:href") }
?.let {
searchMangaParse(client.newCall(GET("$it/manga/0", headers)).execute())
}
?: MangasPage(emptyList(), false)
}
}
else -> {
client.newCall(searchMangaRequest(page, query, filters))
.asObservableSuccess()
.map { response ->
searchMangaParse(response)
}
}
} }
} }
private fun searchMangaParse(response: Response): MangasPage { private fun searchMangaParse(response: Response): MangasPage {
val document = response.asJsoup() val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
val mangas = document.select(searchMangaSelector).map { element -> val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() }
searchMangaFromElement(element) return MangasPage(mangaList, hasMoreResults)
}
val hasNextPage = searchMangaNextPageSelector.let { selector ->
document.select(selector).first()
} != null
return MangasPage(mangas, hasNextPage)
} }
private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val tags = mutableListOf<String>() val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder()
val statuses = mutableListOf<String>()
val demographics = mutableListOf<String>()
// Do traditional search tempUrl.apply {
val url = "${MdUtil.baseUrl}/?page=search".toHttpUrl().newBuilder() addQueryParameter("limit", MdUtil.mangaLimit.toString())
.addQueryParameter("p", page.toString()) addQueryParameter("offset", (MdUtil.getMangaListOffset(page)))
.addQueryParameter("title", query.replace(WHITESPACE_REGEX, " ")) val actualQuery = query.replace(WHITESPACE_REGEX, " ")
if (actualQuery.isNotBlank()) {
filters.forEach { filter -> addQueryParameter("title", actualQuery)
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)
}
}
}
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 { return GET(finalUrl, headers, CacheControl.FORCE_NETWORK)
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
} }
private fun searchMangaByIdRequest(id: String): Request { 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 { 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_ID_SEARCH = "id:"
const val PREFIX_GROUP_SEARCH = "group:" const val PREFIX_GROUP_SEARCH = "group:"
val WHITESPACE_REGEX = "\\s".toRegex() 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,36 +5,32 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import exh.md.similar.sql.models.MangaSimilar
import exh.md.similar.sql.models.MangaSimilarImpl import exh.md.similar.sql.models.MangaSimilarImpl
import exh.md.utils.MdUtil import exh.md.utils.MdUtil
import rx.Observable import exh.util.executeOnIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SimilarHandler(val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) { class SimilarHandler(val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) {
/* /**
* fetch our similar mangas * fetch our similar mangas
*/ */
fun fetchSimilar(manga: Manga): Observable<MangasPage> { suspend fun fetchSimilar(manga: Manga): MangasPage {
// Parse the Mangadex id from the URL // Parse the Mangadex id from the URL
return Observable.just(MdUtil.getMangaId(manga.url).toLong()) val mangaId = MdUtil.getMangaId(manga.url).toLong()
.flatMap { mangaId -> val similarMangaDb = Injekt.get<DatabaseHelper>().getSimilar(mangaId).executeOnIO()
Injekt.get<DatabaseHelper>().getSimilar(mangaId).asRxObservable() return if (similarMangaDb != null) {
}.map { similarMangaDb: MangaSimilar? -> val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
if (similarMangaDb != null) { val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER) val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER) SManga.create().apply {
val similarMangas = similarMangaIds.mapIndexed { index, similarId -> title = similarMangaTitles[index]
SManga.create().apply { url = "/manga/$similarId/"
title = similarMangaTitles[index] thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers)
url = "/manga/$similarId/" }
thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers)
}
}
MangasPage(similarMangas, false)
} else MangasPage(mutableListOf(), false)
} }
MangasPage(similarMangas, false)
} 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(5) val nextTimeStamp: Int = 0,
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY, @ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
@ProtoNumber(7) val viewingPeriodDescription: String = "", @ProtoNumber(7) val viewingPeriodDescription: String = "",
@ProtoNumber(8) val nonAppearanceInfo: String = "",
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(), @ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(), @ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
@ProtoNumber(14) val isSimulReleased: Boolean = true, @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.source.online.all.MangaDex
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
import eu.kanade.tachiyomi.util.lang.runAsObservable
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -15,7 +16,7 @@ import rx.schedulers.Schedulers
class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() { class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
override fun requestNext(): Observable<MangasPage> { override fun requestNext(): Observable<MangasPage> {
return source.fetchMangaSimilar(manga) return runAsObservable({ source.fetchMangaSimilar(manga) })
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnNext { .doOnNext {

View File

@ -1,5 +1,7 @@
package exh.md.utils package exh.md.utils
import java.util.Locale
enum class FollowStatus(val int: Int) { enum class FollowStatus(val int: Int) {
UNFOLLOWED(0), UNFOLLOWED(0),
READING(1), READING(1),
@ -10,6 +12,7 @@ enum class FollowStatus(val int: Int) {
RE_READING(6); RE_READING(6);
companion object { 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 package exh.md.utils
enum class MdLang(val lang: String, val dexLang: String, val langId: Int) { enum class MdLang(val lang: String, val prettyPrint: String, val extLang: String = lang) {
English("en", "gb", 1), ENGLISH("en", "English"),
Japanese("ja", "jp", 2), JAPANESE("jp", "Japanese", "ja"),
Polish("pl", "pl", 3), POLISH("pl", "Polish"),
SerboCroatian("sh", "rs", 4), SERBO_CROATIAN("rs", "Serbo-Croatian", "sh"),
Dutch("nl", "nl", 5), DUTCH("nl", "Dutch"),
Italian("it", "it", 6), ITALIAN("it", "IT"),
Russian("ru", "ru", 7), RUSSIAN("ru", "Russian"),
German("de", "de", 8), GERMAN("de", "German"),
Hungarian("hu", "hu", 9), HUNGARIAN("hu", "Hungarian"),
French("fr", "fr", 10), FRENCH("fr", "French"),
Finnish("fi", "fi", 11), FINNISH("fi", "Finnish"),
Vietnamese("vi", "vn", 12), VIETNAMESE("vn", "Vietnamese", "vi"),
Greek("el", "gr", 13), GREEK("gr", "Greek", "el"),
Bulgarian("bg", "bg", 14), BULGARIAN("bg", "BULGARIN"),
Spanish("es", "es", 15), SPANISH_ES("es", "Spanish (Es)"),
PortugeseBrazilian("pt-BR", "br", 16), PORTUGUESE_BR("br", "Portuguese (Br)", "pt-br"),
Portuguese("pt", "pt", 17), PORTUGUESE("pt", "Portuguese (Pt)"),
Swedish("sv", "se", 18), SWEDISH("se", "Swedish", "sv"),
Arabic("ar", "sa", 19), ARABIC("sa", "Arabic", "ar"),
Danish("da", "dk", 20), DANISH("dk", "Danish", "da"),
ChineseSimplifed("zh-Hans", "cn", 21), CHINESE_SIMPLIFIED("cn", "Chinese (Simp)", "zh"),
Bengali("bn", "bd", 22), BENGALI("bd", "Bengali", "bn"),
Romanian("ro", "ro", 23), ROMANIAN("ro", "Romanian"),
Czech("cs", "cz", 24), CZECH("cz", "Czech", "cs"),
Mongolian("mn", "mn", 25), MONGOLIAN("mn", "Mongolian"),
Turkish("tr", "tr", 26), TURKISH("tr", "Turkish"),
Indonesian("id", "id", 27), INDONESIAN("id", "Indonesian"),
Korean("ko", "kr", 28), KOREAN("kr", "Korean", "ko"),
SpanishLTAM("es-419", "mx", 29), SPANISH_LATAM("mx", "Spanish (LATAM)", "es-la"),
Persian("fa", "ir", 30), PERSIAN("ir", "Persian", "fa"),
Malay("ms", "my", 31), MALAY("my", "Malay", "ms"),
Thai("th", "th", 32), THAI("th", "Thai"),
Catalan("ca", "ct", 33), CATALAN("ct", "Catalan", "ca"),
Filipino("fil", "ph", 34), FILIPINO("ph", "Filipino", "fi"),
ChineseTraditional("zh-Hant", "hk", 35), CHINESE_TRAD("hk", "Chinese (Trad)", "zh-hk"),
Ukrainian("uk", "ua", 36), UKRAINIAN("ua", "Ukrainian", "uk"),
Burmese("my", "mm", 37), BURMESE("mm", "Burmese", "my"),
Lithuanian("lt", "il", 38), LINTHUANIAN("lt", "Lithuanian"),
Hebrew("he", "il", 39), HEBREW("il", "Hebrew", "he"),
Hindi("hi", "in", 40), HINDI("in", "Hindi", "hi"),
Norwegian("no", "no", 42) 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,49 +1,89 @@
package exh.md.utils 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.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.SourceManager
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.all.MangaDex 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.source.getMainSource
import exh.util.floor import exh.util.floor
import exh.util.nullIfBlank
import exh.util.nullIfZero import exh.util.nullIfZero
import kotlinx.serialization.SerializationException
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Headers
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import org.jsoup.parser.Parser import org.jsoup.parser.Parser
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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 { class MdUtil {
companion object { companion object {
const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org" const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org"
const val baseUrl = "https://mangadex.org" const val baseUrl = "https://mangadex.org"
const val randMangaPage = "/manga/"
const val apiUrl = "https://api.mangadex.org" const val apiUrl = "https://api.mangadex.org"
const val apiManga = "/v2/manga/" const val apiUrlCdnCache = "https://cdn.statically.io/gh/goldbattle/MangadexRecomendations/master/output/api/"
const val includeChapters = "?include=chapters" const val apiUrlCache = "https://raw.githubusercontent.com/goldbattle/MangadexRecomendations/master/output/api/"
const val oldApiChapter = "/api/chapter/" const val imageUrlCacheNotFound = "https://cdn.statically.io/img/raw.githubusercontent.com/CarlosEsco/Neko/master/.github/manga_cover_not_found.png"
const val newApiChapter = "/v2/chapter/" const val atHomeUrl = "$apiUrl/at-home/server"
const val apiChapterSuffix = "?mark_read=0" 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 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 apiCovers = "/covers"
const val reportUrl = "https://api.mangadex.network/report" const val reportUrl = "https://api.mangadex.network/report"
const val imageUrl = "$baseUrl/data"
val jsonParser = Json { const val mdAtHomeTokenLifespan = 10 * 60 * 1000
isLenient = true const val mangaLimit = 25
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true /**
useArrayPolymorphism = true * Get the manga offset pages are 1 based, so subtract 1
prettyPrint = true */
} fun getMangaListOffset(page: Int): String = (mangaLimit * (page - 1)).toString()
val jsonParser =
Json {
isLenient = true
ignoreUnknownKeys = true
allowSpecialFloatingPointValues = true
useArrayPolymorphism = true
prettyPrint = true
}
private const val scanlatorSeparator = " & " private const val scanlatorSeparator = " & "
@ -164,24 +204,9 @@ class MdUtil {
} }
// Get the ID from the manga url // Get the ID from the manga url
fun getMangaId(url: String): String { fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/")
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 getChapterId(url: String) = url.substringBeforeLast(apiChapterSuffix).substringAfterLast("/") fun getChapterId(url: String) = url.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 cleanString(string: String): String { fun cleanString(string: String): String {
var cleanedString = string var cleanedString = string
@ -222,8 +247,8 @@ class MdUtil {
return baseUrl + attr return baseUrl + attr
} }
fun getScanlators(scanlators: String): List<String> { fun getScanlators(scanlators: String?): List<String> {
if (scanlators.isBlank()) return emptyList() if (scanlators.isNullOrBlank()) return emptyList()
return scanlators.split(scanlatorSeparator).distinct() return scanlators.split(scanlatorSeparator).distinct()
} }
@ -234,7 +259,6 @@ class MdUtil {
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? { fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
if (mangaStatus == SManga.COMPLETED) return null if (mangaStatus == SManga.COMPLETED) return null
// TODO
val remove0ChaptersFromCount = chapters.distinctBy { val remove0ChaptersFromCount = chapters.distinctBy {
/*if (it.chapter_txt.isNotEmpty()) { /*if (it.chapter_txt.isNotEmpty()) {
it.vol + it.chapter_txt it.vol + it.chapter_txt
@ -257,15 +281,63 @@ class MdUtil {
return null 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 -> return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs ->
preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()?.let { preferredMangaDexId -> preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()
mangadexs.firstOrNull { it.id == preferredMangaDexId } ?.let { preferredMangaDexId ->
} ?: mangadexs.firstOrNull() mangadexs.firstOrNull { it.id == preferredMangaDexId }
}
?: 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 languages = preferences.enabledLanguages().get()
val disabledSourceIds = preferences.disabledSources().get() val disabledSourceIds = preferences.disabledSources().get()
@ -275,54 +347,5 @@ class MdUtil {
.filter { it.lang in languages } .filter { it.lang in languages }
.filterNot { it.id.toString() in disabledSourceIds } .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 package exh.metadata.metadata
import android.content.Context import android.content.Context
import androidx.core.net.toUri
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import exh.md.utils.MdUtil
import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.RaisedSearchMetadata
import exh.util.nullIfBlank
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
@Serializable @Serializable
class MangaDexSearchMetadata : RaisedSearchMetadata() { 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 title: String? by titleDelegate(TITLE_TYPE_MAIN)
var altTitles: List<String>? = null
var description: String? = null var description: String? = null
var author: String? = null var authors: List<String>? = null
var artist: String? = null
var lang_flag: String? = null var langFlag: String? = null
var last_chapter_number: Int? = null var lastChapterNumber: Int? = null
var rating: String? = null // var rating: String? = null
var users: String? = null // var users: String? = null
var anilist_id: String? = null var anilistId: String? = null
var kitsu_id: String? = null var kitsuId: String? = null
var my_anime_list_id: String? = null var myAnimeListId: String? = null
var manga_updates_id: String? = null var mangaUpdatesId: String? = null
var anime_planet_id: String? = null var animePlanetId: String? = null
var status: Int? = 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 { override fun createMangaInfo(manga: MangaInfo): MangaInfo {
val key = mdUrl?.let { val key = mdUuid?.let { "/manga/$it" }
try {
val uri = it.toUri()
val out = uri.path!!.removePrefix("/api")
out + if (out.endsWith("/")) "" else "/"
} catch (e: Exception) {
it
}
}
val title = title val title = title
val cover = thumbnail_url val cover = cover ?: manga.cover.nullIfBlank() ?: "https://i.imgur.com/6TrIues.jpg" // cover
val author = author val author = authors?.joinToString()?.let { MdUtil.cleanString(it) }
val artist = artist
val status = status val status = status
@ -72,7 +63,6 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
title = title ?: manga.title, title = title ?: manga.title,
cover = cover ?: manga.cover, cover = cover ?: manga.cover,
author = author ?: manga.author, author = author ?: manga.author,
artist = artist ?: manga.artist,
status = status ?: manga.status, status = status ?: manga.status,
genres = genres, genres = genres,
description = description ?: manga.description description = description ?: manga.description
@ -81,29 +71,30 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
val pairs = mutableListOf<Pair<String, String>>() val pairs = mutableListOf<Pair<String, String>>()
mdId?.let { pairs += context.getString(R.string.id) to it } mdUuid?.let { pairs += context.getString(R.string.id) to it }
mdUrl?.let { pairs += context.getString(R.string.url) to it } // mdUrl?.let { pairs += context.getString(R.string.url) to it }
thumbnail_url?.let { pairs += context.getString(R.string.thumbnail_url) to it } cover?.let { pairs += context.getString(R.string.thumbnail_url) to it }
title?.let { pairs += context.getString(R.string.title) to it } title?.let { pairs += context.getString(R.string.title) to it }
author?.let { pairs += context.getString(R.string.author) to it } authors?.let { pairs += context.getString(R.string.author) to it.joinToString() }
artist?.let { pairs += context.getString(R.string.artist) to it } // artist?.let { pairs += context.getString(R.string.artist) to it }
lang_flag?.let { pairs += context.getString(R.string.language) to it } langFlag?.let { pairs += context.getString(R.string.language) to it }
last_chapter_number?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() } lastChapterNumber?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() }
rating?.let { pairs += context.getString(R.string.average_rating) to it } // rating?.let { pairs += context.getString(R.string.average_rating) to it }
users?.let { pairs += context.getString(R.string.total_ratings) to it } // users?.let { pairs += context.getString(R.string.total_ratings) to it }
status?.let { pairs += context.getString(R.string.status) to it.toString() } status?.let { pairs += context.getString(R.string.status) to it.toString() }
missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it } // 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() } followStatus?.let { pairs += context.getString(R.string.follow_status) to it.toString() }
anilist_id?.let { pairs += context.getString(R.string.anilist_id) to it } anilistId?.let { pairs += context.getString(R.string.anilist_id) to it }
kitsu_id?.let { pairs += context.getString(R.string.kitsu_id) to it } kitsuId?.let { pairs += context.getString(R.string.kitsu_id) to it }
my_anime_list_id?.let { pairs += context.getString(R.string.mal_id) to it } myAnimeListId?.let { pairs += context.getString(R.string.mal_id) to it }
manga_updates_id?.let { pairs += context.getString(R.string.manga_updates_id) to it } mangaUpdatesId?.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 } animePlanetId?.let { pairs += context.getString(R.string.anime_planet_id) to it }
return pairs return pairs
} }
companion object { companion object {
private const val TITLE_TYPE_MAIN = 0 private const val TITLE_TYPE_MAIN = 0
private const val TITLE_TYPE_ALT_TITLE = 1
const val TAG_TYPE_DEFAULT = 0 const val TAG_TYPE_DEFAULT = 0
} }

View File

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

View File

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