From b9b5ef55abf7a62451a5b9fa3f6b14bdd1f1ad73 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Thu, 6 May 2021 21:19:30 -0400 Subject: [PATCH] Rewrite and enable Mangadex delegation for V5 of Mangadex (Thanks Cesco) Co-authored-by: CarlosEsco --- app/build.gradle.kts | 12 +- .../tachiyomi/data/database/DbOpenCallback.kt | 5 +- .../data/database/mappers/MangaTypeMapping.kt | 5 +- .../tachiyomi/data/database/models/Manga.kt | 2 + .../data/database/models/MangaImpl.kt | 2 + .../data/database/queries/MangaQueries.kt | 8 + .../MangaFilteredScanlatorsPutResolver.kt | 32 ++ .../data/database/tables/MangaTable.kt | 8 +- .../data/library/LibraryUpdateService.kt | 4 +- .../tachiyomi/data/track/TrackManager.kt | 2 +- .../tachiyomi/data/track/mdlist/MdList.kt | 13 +- .../kanade/tachiyomi/source/SourceManager.kt | 5 +- .../tachiyomi/source/online/FollowsSource.kt | 2 +- .../tachiyomi/source/online/all/MangaDex.kt | 207 ++++----- .../browse/SourceComfortableGridHolder.kt | 2 +- .../browse/source/browse/SourceGridHolder.kt | 2 +- .../browse/source/browse/SourceListHolder.kt | 2 +- .../tachiyomi/ui/manga/MangaPresenter.kt | 29 +- .../ui/manga/chapter/ChaptersSettingsSheet.kt | 18 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 5 +- app/src/main/java/exh/EXHMigrations.kt | 12 +- .../java/exh/md/handlers/ApiChapterParser.kt | 28 +- .../java/exh/md/handlers/ApiMangaParser.kt | 335 +++++++-------- .../java/exh/md/handlers/FilterHandler.kt | 395 +++++++++++------- .../java/exh/md/handlers/FollowsHandler.kt | 271 ++++++------ .../main/java/exh/md/handlers/MangaHandler.kt | 179 +++++--- .../main/java/exh/md/handlers/PageHandler.kt | 9 +- .../java/exh/md/handlers/PopularHandler.kt | 53 +-- .../java/exh/md/handlers/SearchHandler.kt | 206 ++------- .../java/exh/md/handlers/SimilarHandler.kt | 36 +- .../serializers/ApiChapterSerializer.kt | 24 -- .../serializers/ApiMangaSerializer.kt | 76 ---- .../java/exh/md/handlers/serializers/Auth.kt | 39 ++ .../serializers/CacheApiMangaSerializer.kt | 40 ++ .../handlers/serializers/ChapterSerializer.kt | 67 +++ .../handlers/serializers/CoversSerializer.kt | 14 - .../serializers/FollowsPageSerializer.kt | 24 -- .../serializers/ImageReportSerializer.kt | 10 + .../serializers/MangaPlusSerializer.kt | 1 - .../handlers/serializers/MangaSerializer.kt | 82 ++++ .../handlers/serializers/NetworkFollowed.kt | 13 + .../exh/md/network/MangaDexLoginHelper.kt | 106 +++++ .../java/exh/md/network/NoSessionException.kt | 3 + .../java/exh/md/network/TokenAuthenticator.kt | 73 ++++ .../exh/md/similar/ui/MangaDexSimilarPager.kt | 3 +- .../main/java/exh/md/utils/FollowStatus.kt | 5 +- app/src/main/java/exh/md/utils/MdLang.kt | 97 +++-- app/src/main/java/exh/md/utils/MdUtil.kt | 213 +++++----- .../metadata/MangaDexSearchMetadata.kt | 87 ++-- .../metadata/base/RaisedSearchMetadata.kt | 2 - .../adapters/MangaDexDescriptionAdapter.kt | 11 +- 51 files changed, 1587 insertions(+), 1292 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFilteredScanlatorsPutResolver.kt delete mode 100644 app/src/main/java/exh/md/handlers/serializers/ApiChapterSerializer.kt delete mode 100644 app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt create mode 100644 app/src/main/java/exh/md/handlers/serializers/Auth.kt create mode 100644 app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt create mode 100644 app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt delete mode 100644 app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt delete mode 100644 app/src/main/java/exh/md/handlers/serializers/FollowsPageSerializer.kt create mode 100644 app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt create mode 100644 app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt create mode 100644 app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt create mode 100644 app/src/main/java/exh/md/network/MangaDexLoginHelper.kt create mode 100644 app/src/main/java/exh/md/network/NoSessionException.kt create mode 100644 app/src/main/java/exh/md/network/TokenAuthenticator.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5981074d..2a6b8c3fe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,6 +24,14 @@ if (!gradle.startParameter.taskRequests.toString().contains("Debug")) { shortcutHelper.setFilePath("./shortcuts.xml") +configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "org.jetbrains.kotlin") { + useVersion("1.4.32") + } + } +} + android { compileSdkVersion(AndroidConfig.compileSdk) buildToolsVersion(AndroidConfig.buildTools) @@ -170,7 +178,7 @@ dependencies { implementation("org.conscrypt:conscrypt-android:2.5.1") // JSON - val kotlinSerializationVersion = "1.2.0" + val kotlinSerializationVersion = "1.1.0" implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion") implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion") implementation("com.google.code.gson:gson:2.8.6") @@ -303,7 +311,7 @@ dependencies { // JsonReader for similar manga implementation("com.squareup.moshi:moshi:1.12.0") - implementation("com.mikepenz:fastadapter:5.4.0") + implementation("com.mikepenz:fastadapter:5.4.1") // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 784762063..e9b02ffa1 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -25,7 +25,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { /** * Version of the database. */ - const val DATABASE_VERSION = /* SY --> */ 5 /* SY <-- */ + const val DATABASE_VERSION = /* SY --> */ 6 /* SY <-- */ } override fun onCreate(db: SupportSQLiteDatabase) = with(db) { @@ -78,6 +78,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { db.execSQL(SimilarTable.createTableQuery) db.execSQL(SimilarTable.createMangaIdIndexQuery) } + if (oldVersion < 6) { + db.execSQL(MangaTable.addFilteredScanlators) + } } override fun onConfigure(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt index d15e2fd10..ef4e01878 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/mappers/MangaTypeMapping.kt @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE +import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED @@ -68,7 +69,8 @@ class MangaPutResolver : DefaultPutResolver() { COL_VIEWER to obj.viewer_flags, COL_CHAPTER_FLAGS to obj.chapter_flags, COL_COVER_LAST_MODIFIED to obj.cover_last_modified, - COL_DATE_ADDED to obj.date_added + COL_DATE_ADDED to obj.date_added, + COL_FILTERED_SCANLATORS to obj.filtered_scanlators ) } @@ -91,6 +93,7 @@ interface BaseMangaGetResolver { chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED)) date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED)) + filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 2e78150ac..0b23088f3 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -23,6 +23,8 @@ interface Manga : SManga { var cover_last_modified: Long + var filtered_scanlators: String? + fun setChapterOrder(order: Int) { setChapterFlags(order, CHAPTER_SORT_MASK) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt index e35336a03..5182adc80 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaImpl.kt @@ -62,6 +62,8 @@ open class MangaImpl : Manga { override var cover_last_modified: Long = 0 + override var filtered_scanlators: String? = null + // SY --> lateinit var ogTitle: String private set diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 372f001c8..f62243558 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver +import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver @@ -153,6 +154,13 @@ interface MangaQueries : DbProvider { .withPutResolver(MangaCoverLastModifiedPutResolver()) .prepare() + // SY --> + fun updateMangaFilteredScanlators(manga: Manga) = db.put() + .`object`(manga) + .withPutResolver(MangaFilteredScanlatorsPutResolver()) + .prepare() + // SY <-- + fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFilteredScanlatorsPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFilteredScanlatorsPutResolver.kt new file mode 100644 index 000000000..6cdac014a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFilteredScanlatorsPutResolver.kt @@ -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() { + + 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 + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt index e8d31e4e6..ae073ab75 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt @@ -40,6 +40,8 @@ object MangaTable { // SY ->> const val COL_READ = "read" + + const val COL_FILTERED_SCANLATORS = "filtered_scanlators" // SY <-- const val COL_CATEGORY = "category" @@ -65,7 +67,8 @@ object MangaTable { $COL_VIEWER INTEGER NOT NULL, $COL_CHAPTER_FLAGS INTEGER NOT NULL, $COL_COVER_LAST_MODIFIED LONG NOT NULL, - $COL_DATE_ADDED LONG NOT NULL + $COL_DATE_ADDED LONG NOT NULL, + $COL_FILTERED_SCANLATORS TEXT )""" val createUrlIndexQuery: String @@ -90,4 +93,7 @@ object MangaTable { "FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " + "ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " + "GROUP BY $TABLE.$COL_ID)" + + val addFilteredScanlators: String + get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 9754ee040..cfe953e96 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -561,9 +561,9 @@ class LibraryUpdateService( val syncFollowStatusInts = preferences.mangadexSyncToLibraryIndexes().get().map { it.toInt() } val size: Int - mangaDex.fetchAllFollows(true) + mangaDex.fetchAllFollows() .filter { (_, metadata) -> - syncFollowStatusInts.contains(metadata.follow_status) + syncFollowStatusInts.contains(metadata.followStatus) } .also { size = it.size } .forEach { (networkManga, metadata) -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 89e0fabbb..ad38c3d49 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -18,7 +18,7 @@ class TrackManager(context: Context) { const val BANGUMI = 5 // SY --> Mangadex from Neko - const val MDLIST = 6 + const val MDLIST = 60 // SY <-- // SY --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt index a5795450d..35e171b0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/mdlist/MdList.kt @@ -9,7 +9,6 @@ import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch -import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.util.lang.awaitSingle import eu.kanade.tachiyomi.util.lang.runAsObservable @@ -17,10 +16,12 @@ import eu.kanade.tachiyomi.util.lang.withIOContext import exh.md.utils.FollowStatus import exh.md.utils.MdUtil import tachiyomi.source.model.MangaInfo +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class MdList(private val context: Context, id: Int) : TrackService(id) { - private val mdex by lazy { MdUtil.getEnabledMangaDex() } + private val mdex by lazy { MdUtil.getEnabledMangaDex(Injekt.get()) } @StringRes override fun nameRes(): Int = R.string.mdlist @@ -47,6 +48,7 @@ class MdList(private val context: Context, id: Int) : TrackService(id) { override suspend fun add(track: Track): Track = update(track) override suspend fun update(track: Track): Track { + throw Exception("Mangadex api is read-only") return withIOContext { val mdex = mdex ?: throw MangaDexNotFoundException() @@ -96,9 +98,9 @@ class MdList(private val context: Context, id: Int) : TrackService(id) { val mdex = mdex ?: throw MangaDexNotFoundException() val (remoteTrack, mangaMetadata) = mdex.getTrackingAndMangaInfo(track) track.copyPersonalFrom(remoteTrack) - if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) { + /*if (track.total_chapters == 0 && mangaMetadata.status == SManga.COMPLETED) { track.total_chapters = mangaMetadata.maxChapterNumber ?: 0 - } + }*/ track } } @@ -136,8 +138,5 @@ class MdList(private val context: Context, id: Int) : TrackService(id) { override suspend fun login(username: String, password: String): Unit = throw Exception("not used") - override val isLogged: Boolean - get() = false - class MangaDexNotFoundException : Exception("Mangadex not enabled") } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 6a01bc0f4..4f42d6f78 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.Hitomi +import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.source.online.all.MergedSource import eu.kanade.tachiyomi.source.online.all.NHentai import eu.kanade.tachiyomi.source.online.all.PervEden @@ -203,13 +204,13 @@ open class SourceManager(private val context: Context) { "eu.kanade.tachiyomi.extension.en.tsumino.Tsumino", Tsumino::class ), - /*DelegatedSource( + DelegatedSource( "MangaDex", fillInSourceId, "eu.kanade.tachiyomi.extension.all.mangadex", MangaDex::class, true - ),*/ + ), DelegatedSource( "HBrowse", HBROWSE_SOURCE_ID, diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt index 0a8e3df3a..a860af1f3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/FollowsSource.kt @@ -15,7 +15,7 @@ interface FollowsSource : CatalogueSource { * * @param SManga all smanga found for user */ - suspend fun fetchAllFollows(forceHd: Boolean = false): List> + suspend fun fetchAllFollows(): List> /** * updates the follow status for a manga diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt index 372a8f13a..56a1ec7a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/all/MangaDex.kt @@ -1,17 +1,13 @@ package eu.kanade.tachiyomi.source.online.all import android.content.Context -import android.content.SharedPreferences -import android.net.Uri import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.network.GET -import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.asObservableSuccess -import eu.kanade.tachiyomi.network.await -import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter @@ -21,14 +17,10 @@ import eu.kanade.tachiyomi.source.online.FollowsSource import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.source.online.MetadataSource +import eu.kanade.tachiyomi.source.online.NamespaceSource import eu.kanade.tachiyomi.source.online.RandomMangaSource -import eu.kanade.tachiyomi.source.online.UrlImportableSource import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.util.lang.runAsObservable -import eu.kanade.tachiyomi.util.lang.withIOContext -import exh.GalleryAddEvent -import exh.GalleryAdder import exh.md.MangaDexFabHeaderAdapter import exh.md.handlers.ApiChapterParser import exh.md.handlers.ApiMangaParser @@ -36,25 +28,20 @@ import exh.md.handlers.FollowsHandler import exh.md.handlers.MangaHandler import exh.md.handlers.MangaPlusHandler import exh.md.handlers.SimilarHandler +import exh.md.network.MangaDexLoginHelper +import exh.md.network.NoSessionException +import exh.md.network.TokenAuthenticator import exh.md.utils.FollowStatus import exh.md.utils.MdLang import exh.md.utils.MdUtil import exh.metadata.metadata.MangaDexSearchMetadata import exh.source.DelegatedHttpSource import exh.ui.metadata.adapters.MangaDexDescriptionAdapter -import exh.util.urlImportFetchSearchManga -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.int import okhttp3.CacheControl -import okhttp3.FormBody import okhttp3.Headers -import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import okhttp3.internal.closeQuietly -import okio.EOFException import rx.Observable import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.MangaInfo @@ -67,45 +54,53 @@ import kotlin.reflect.KClass class MangaDex(delegate: HttpSource, val context: Context) : DelegatedHttpSource(delegate), MetadataSource, - UrlImportableSource, + // UrlImportableSource, FollowsSource, LoginSource, BrowseSourceFilterHeader, - RandomMangaSource { + RandomMangaSource, + NamespaceSource { override val lang: String = delegate.lang - override val headers: Headers = super.headers.newBuilder().apply { - add("X-Requested-With", "XMLHttpRequest") - add("Referer", MdUtil.baseUrl) - }.build() - private val mdLang by lazy { - MdLang.values().find { it.lang == lang }?.dexLang ?: lang + MdLang.fromExt(lang) ?: MdLang.ENGLISH } - override val matchingHosts: List = listOf("mangadex.org", "www.mangadex.org") + // override val matchingHosts: List = listOf("mangadex.org", "www.mangadex.org") val preferences: PreferencesHelper by injectLazy() - val trackManager: TrackManager by injectLazy() - - private val sourcePreferences: SharedPreferences by lazy { - context.getSharedPreferences("source_$id", 0x0000) + val mdList: MdList by lazy { + Injekt.get().mdList } - private fun useLowQualityThumbnail() = sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY + /*private val sourcePreferences: SharedPreferences by lazy { + context.getSharedPreferences("source_$id", 0x0000) + }*/ - override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable = + 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 = urlImportFetchSearchManga(context, query) { importIdToMdId(query) { super.fetchSearchManga(page, query, filters) } - } + }*/ - override suspend fun mapUrlToMangaUrl(uri: Uri): String? { + /*override suspend fun mapUrlToMangaUrl(uri: Uri): String? { val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") { - MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt()) + "/manga/" + uri.pathSegments[1] } else { null } @@ -119,44 +114,43 @@ class MangaDex(delegate: HttpSource, val context: Context) : override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? { val id = uri.pathSegments.getOrNull(2) ?: return null - val mangaId = MangaHandler(client, headers, mdLang).getMangaIdFromChapterId(id) + val mangaId = MangaHandler(baseHttpClient, headers, mdLang).getMangaIdFromChapterId(id) return MdUtil.mapMdIdToMangaUrl(mangaId) - } + }*/ override fun fetchMangaDetails(manga: SManga): Observable { - return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga) + return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga, id) } override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { - return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id) + return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id) } override fun fetchChapterList(manga: SManga): Observable> { - 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 { - 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> { return if (chapter.scanlator == "MangaPlus") { - client.newCall(mangaPlusPageListRequest(chapter)) + baseHttpClient.newCall(mangaPlusPageListRequest(chapter)) .asObservableSuccess() .map { response -> val chapterId = ApiChapterParser().externalParse(response) - MangaPlusHandler(client).fetchPageList(chapterId) + MangaPlusHandler(baseHttpClient).fetchPageList(chapterId) } } else super.fetchPageList(chapter) } private fun mangaPlusPageListRequest(chapter: SChapter): Request { - val urlChapterId = MdUtil.getChapterId(chapter.url) - return GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK) + return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK) } override fun fetchImage(page: Page): Observable { - return if (page.imageUrl!!.contains("mangaplus", true)) { + return if (page.imageUrl?.contains("mangaplus", true) == true) { MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers)) .asObservableSuccess() } else super.fetchImage(page) @@ -169,28 +163,27 @@ class MangaDex(delegate: HttpSource, val context: Context) : } override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) { - ApiMangaParser(mdLang).parseIntoMetadata(metadata, input, emptyList()) + ApiMangaParser(baseHttpClient, mdLang.lang).parseIntoMetadata(metadata, input, emptyList()) } override suspend fun fetchFollows(): MangasPage { - return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows() + return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchFollows() } override val requiresLogin: Boolean = true - override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED + override val twoFactorAuth = LoginSource.AuthSupport.NOT_SUPPORTED override fun isLogged(): Boolean { - val httpUrl = MdUtil.baseUrl.toHttpUrl() - return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME } + return mdList.isLogged } override fun getUsername(): String { - return trackManager.mdList.getUsername() + return mdList.getUsername() } override fun getPassword(): String { - return trackManager.mdList.getPassword() + return mdList.getPassword() } override suspend fun login( @@ -198,96 +191,52 @@ class MangaDex(delegate: HttpSource, val context: Context) : password: String, twoFactorCode: String? ): Boolean { - return withIOContext { - val formBody = FormBody.Builder().apply { - add("login_username", username) - add("login_password", password) - add("no_js", "1") - add("remember_me", "1") - add("two_factor", twoFactorCode ?: "") - } - - runCatching { - client.newCall( - POST( - "${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login", - headers, - formBody.build() - ) - ).await().closeQuietly() - } - - val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await() - - withIOContext { response.body?.string() }.let { jsonData -> - if (jsonData != null) { - MdUtil.jsonParser.decodeFromString(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200 - } else { - throw Exception("Json data was null") - } - }.also { - preferences.setTrackCredentials(trackManager.mdList, username, password) - } - } + val result = loginHelper.login(username, password) + return if (result is MangaDexLoginHelper.LoginResult.Success) { + MdUtil.updateLoginToken(result.token, preferences, mdList) + mdList.saveCredentials(username, password) + true + } else false } override suspend fun logout(): Boolean { - return withIOContext { - // https://mangadex.org/ajax/actions.ajax.php?function=logout - val httpUrl = MdUtil.baseUrl.toHttpUrl() - val listOfDexCookies = network.cookieManager.get(httpUrl) - val cookie = listOfDexCookies.find { it.name == REMEMBER_ME } - val token = cookie?.value - if (token.isNullOrEmpty()) { - return@withIOContext true - } - try { - val result = client.newCall( - POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build() - ).await() - val resultStr = withIOContext { result.body?.string() } - if (resultStr?.contains("success", true) == true) { - network.cookieManager.remove(httpUrl) - trackManager.mdList.logout() - return@withIOContext true - } - } catch (e: EOFException) { - network.cookieManager.remove(httpUrl) - trackManager.mdList.logout() - return@withIOContext true - } - - false + val result = try { + loginHelper.logout(MdUtil.getAuthHeaders(Headers.Builder().build(), preferences, mdList)) + } catch (e: NoSessionException) { + true } + + return if (result) { + mdList.logout() + true + } else false } - override suspend fun fetchAllFollows(forceHd: Boolean): List> { - return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchAllFollows(forceHd) } + override suspend fun fetchAllFollows(): List> { + return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchAllFollows() } suspend fun updateReadingProgress(track: Track): Boolean { - return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateReadingProgress(track) } + return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateReadingProgress(track) } suspend fun updateRating(track: Track): Boolean { - return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateRating(track) } + return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateRating(track) } override suspend fun fetchTrackingInfo(url: String): Track { - return withIOContext { - if (!isLogged()) { - throw Exception("Not Logged in") - } - FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchTrackingInfo(url) + if (!isLogged()) { + throw Exception("Not Logged in") } + return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchTrackingInfo(url) } suspend fun getTrackingAndMangaInfo(track: Track): Pair { - return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail()) + return MangaHandler(baseHttpClient, headers, mdLang.lang).getTrackingInfo(track, useLowQualityThumbnail(), mdList) } override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean { - return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateFollowStatus(mangaID, followStatus) } + return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateFollowStatus(mangaID, followStatus) } override fun getFilterHeader(controller: BaseController<*>): MangaDexFabHeaderAdapter { @@ -295,14 +244,14 @@ class MangaDex(delegate: HttpSource, val context: Context) : } override suspend fun fetchRandomMangaUrl(): String { - return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() } + return MangaHandler(baseHttpClient, headers, mdLang.lang).fetchRandomMangaId() } - fun fetchMangaSimilar(manga: Manga): Observable { + suspend fun fetchMangaSimilar(manga: Manga): MangasPage { return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga) } - private fun importIdToMdId(query: String, fail: () -> Observable): Observable = + /*private fun importIdToMdId(query: String, fail: () -> Observable): Observable = when { query.toIntOrNull() != null -> { runAsObservable({ @@ -320,11 +269,11 @@ class MangaDex(delegate: HttpSource, val context: Context) : } } else -> fail() - } + }*/ - companion object { + /*companion object { private const val REMEMBER_ME = "mangadex_rememberme_token" private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault" private const val LOW_QUALITY = 1 - } + }*/ } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt index 088204872..0cd46ed7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt @@ -51,7 +51,7 @@ class SourceComfortableGridHolder(private val view: View, private val adapter: F // SY --> override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { if (metadata is MangaDexSearchMetadata) { - metadata.follow_status?.let { + metadata.followStatus?.let { binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] binding.localText.isVisible = true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt index e6ef6d62e..b414bf473 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceGridHolder.kt @@ -48,7 +48,7 @@ open class SourceGridHolder(private val view: View, private val adapter: Flexibl // SY --> override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { if (metadata is MangaDexSearchMetadata) { - metadata.follow_status?.let { + metadata.followStatus?.let { binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] binding.localText.isVisible = true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt index c9dca5e83..9e97533c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt @@ -50,7 +50,7 @@ class SourceListHolder(private val view: View, adapter: FlexibleAdapter<*>) : // SY --> override fun onSetMetadataValues(manga: Manga, metadata: RaisedSearchMetadata) { if (metadata is MangaDexSearchMetadata) { - metadata.follow_status?.let { + metadata.followStatus?.let { binding.localText.text = itemView.context.resources.getStringArray(R.array.md_follows_options).asList()[it] binding.localText.isVisible = true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 810bf1805..7131dc135 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -46,14 +46,13 @@ import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.Stat import exh.debug.DebugToggles import exh.eh.EHentaiUpdateHelper import exh.log.xLogD +import exh.log.xLogE import exh.md.utils.FollowStatus import exh.md.utils.MdUtil -import exh.md.utils.scanlatorList import exh.merged.sql.models.MergedMangaReference import exh.metadata.metadata.base.FlatMetadata import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.getFlatMetadataForManga -import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource import exh.source.isEhBasedSource @@ -184,11 +183,6 @@ class MangaPresenter( .subscribeLatestCache({ view, (manga, flatMetadata) -> flatMetadata?.let { metadata -> view.onNextMetaInfo(metadata) - meta?.let { - it.filteredScanlators?.let { - if (chapters.isNotEmpty()) chaptersRelay.call(chapters) - } - } } // SY <-- view.onNextMangaInfo(manga, source) @@ -219,7 +213,7 @@ class MangaPresenter( // Find downloaded chapters setDownloadedChapters(chapters) - allChapterScanlators = chapters.flatMap { it.chapter.scanlatorList() }.toSet() + allChapterScanlators = chapters.flatMap { MdUtil.getScanlators(it.chapter.scanlator) }.toSet() // Store the last emission this.chapters = chapters @@ -307,6 +301,7 @@ class MangaPresenter( withUIContext { view?.onFetchMangaInfoDone() } } catch (e: Throwable) { + xLogE("Error getting manga details", e) withUIContext { view?.onFetchMangaInfoError(e) } } } @@ -840,11 +835,9 @@ class MangaPresenter( } // SY --> - meta?.let { metadata -> - metadata.filteredScanlators?.let { filteredScanlatorString -> - val filteredScanlators = MdUtil.getScanlators(filteredScanlatorString) - observable = observable.filter { it.scanlatorList().any { group -> filteredScanlators.contains(group) } } - } + manga.filtered_scanlators?.let { filteredScanlatorString -> + val filteredScanlators = MdUtil.getScanlators(filteredScanlatorString) + observable = observable.filter { MdUtil.getScanlators(it.scanlator).any { group -> filteredScanlators.contains(group) } } } // SY <-- @@ -1043,12 +1036,10 @@ class MangaPresenter( } // SY --> - suspend fun setScanlatorFilter(filteredScanlators: Set) { - val meta = meta ?: return - meta.filteredScanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators) - meta.flatten().let { - db.insertFlatMetadataAsync(it).await() - } + fun setScanlatorFilter(filteredScanlators: Set) { + val manga = manga + manga.filtered_scanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators) + db.updateMangaFilteredScanlators(manga).executeAsBlocking() refreshChapters() } // SY <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt index 67536e6de..6c3266402 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt @@ -9,18 +9,14 @@ import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.bluelinelabs.conductor.Router import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.source.online.MetadataSource import eu.kanade.tachiyomi.ui.manga.MangaPresenter import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext -import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.popupMenu import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.sheet.TabbedBottomSheetDialog import exh.md.utils.MdUtil -import exh.metadata.metadata.MangaDexSearchMetadata -import exh.source.getMainSource import kotlinx.coroutines.supervisorScope class ChaptersSettingsSheet( @@ -88,7 +84,7 @@ class ChaptersSettingsSheet( * Returns true if there's at least one filter from [FilterGroup] active. */ fun hasActiveFilters(): Boolean { - return filterGroup.items.any { it.state != State.IGNORE.value } || (presenter.meta?.let { it is MangaDexSearchMetadata && it.filteredScanlators != null } ?: false) + return filterGroup.items.any { it.state != State.IGNORE.value } || presenter.manga.filtered_scanlators != null } inner class FilterGroup : Group { @@ -100,7 +96,7 @@ class ChaptersSettingsSheet( private val scanlatorFilters = Item.DrawableSelection(0, this, R.string.scanlator, R.drawable.ic_outline_people_alt_24dp) override val header = null - override val items = listOf(downloaded, unread, bookmarked) + if (presenter.source.getMainSource() is MetadataSource<*, *>) listOf(scanlatorFilters) else emptyList() + override val items = listOf(downloaded, unread, bookmarked, scanlatorFilters) override val footer = null override fun initModels() { @@ -116,16 +112,8 @@ class ChaptersSettingsSheet( override fun onItemClicked(item: Item) { if (item is Item.DrawableSelection) { - val meta = presenter.meta - if (meta == null) { - context.toast(R.string.metadata_corrupted) - return - } else if (presenter.allChapterScanlators.isEmpty()) { - context.toast(R.string.no_scanlators) - return - } val scanlators = presenter.allChapterScanlators.toList() - val filteredScanlators = meta.filteredScanlators?.let { MdUtil.getScanlators(it) } + val filteredScanlators = presenter.manga.filtered_scanlators?.let { MdUtil.getScanlators(it) } val preselected = if (filteredScanlators.isNullOrEmpty()) scanlators.mapIndexed { index, _ -> index }.toIntArray() else filteredScanlators.map { scanlators.indexOf(it) }.toIntArray() MaterialDialog(context) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 93ce37bc2..2b9b394bf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.updateCoverLastModified import exh.md.utils.FollowStatus import exh.md.utils.MdUtil -import exh.md.utils.scanlatorList import exh.metadata.metadata.base.RaisedSearchMetadata import exh.metadata.metadata.base.getFlatMetadataForManga import exh.source.MERGED_SOURCE_ID @@ -116,7 +115,7 @@ class ReaderPresenter( private val chapterList by lazy { val manga = manga!! // SY --> - val filteredScanlators = meta?.filteredScanlators?.let { MdUtil.getScanlators(it) } + val filteredScanlators = manga.filtered_scanlators?.let { MdUtil.getScanlators(it) } // SY <-- val dbChapters = /* SY --> */ if (manga.source == MERGED_SOURCE_ID) { (sourceManager.get(MERGED_SOURCE_ID) as MergedSource) @@ -142,7 +141,7 @@ class ReaderPresenter( ) || (manga.bookmarkedFilter == Manga.CHAPTER_SHOW_BOOKMARKED && !it.bookmark) || // SY --> - (filteredScanlators != null && it.scanlatorList().none { group -> filteredScanlators.contains(group) }) + (filteredScanlators != null && MdUtil.getScanlators(it.scanlator).none { group -> filteredScanlators.contains(group) }) // SY <-- ) { return@filter false diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index 4d8983322..463187f80 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -3,6 +3,7 @@ package exh import android.content.Context import androidx.core.content.edit import androidx.preference.PreferenceManager +import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.BuildConfig @@ -12,6 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.resolvers.MangaUrlPutResolver import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.MangaTable +import eu.kanade.tachiyomi.data.database.tables.TrackTable import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.preference.PreferenceKeys import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -242,7 +244,6 @@ object EXHMigrations { // UpdaterJob.cancelTask(context) // } } - if (oldVersion under 17) { // Migrate Rotation and Viewer values to default values for viewer_flags val prefs = PreferenceManager.getDefaultSharedPreferences(context) @@ -264,6 +265,15 @@ object EXHMigrations { putInt("pref_default_reading_mode_key", newReadingMode) remove("pref_default_viewer_key") } + + // Delete old mangadex trackers + db.db.lowLevel().delete( + DeleteQuery.builder() + .table(TrackTable.TABLE) + .where("${TrackTable.COL_SYNC_ID} = ?") + .whereArgs(6) + .build() + ) } // if (oldVersion under 1) { } (1 is current release version) diff --git a/app/src/main/java/exh/md/handlers/ApiChapterParser.kt b/app/src/main/java/exh/md/handlers/ApiChapterParser.kt index 2ce30b3de..d0c1536bc 100644 --- a/app/src/main/java/exh/md/handlers/ApiChapterParser.kt +++ b/app/src/main/java/exh/md/handlers/ApiChapterParser.kt @@ -2,7 +2,7 @@ package exh.md.handlers import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.model.Page -import exh.md.handlers.serializers.ApiChapterSerializer +import exh.md.handlers.serializers.ChapterResponse import exh.md.utils.MdUtil import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.jsonObject @@ -10,18 +10,26 @@ import kotlinx.serialization.json.jsonPrimitive import okhttp3.Response class ApiChapterParser { - // Only used in [PageHandler], which means its currently unused, kept for reference - fun pageListParse(response: Response): List { - val networkApiChapter = response.parseAs(MdUtil.jsonParser) + fun pageListParse(response: Response, host: String, dataSaver: Boolean): List { + val networkApiChapter = response.parseAs(MdUtil.jsonParser) - val hash = networkApiChapter.data.hash - val pageArray = networkApiChapter.data.pages - val server = networkApiChapter.data.server + val pages = mutableListOf() - return pageArray.mapIndexed { index, page -> - val url = "$hash/$page" - Page(index, "$server,${response.request.url},${System.currentTimeMillis()}", url) + val atHomeRequestUrl = response.request.url.toUrl().toString() + + val hash = networkApiChapter.data.attributes.hash + val pageArray = if (dataSaver) { + networkApiChapter.data.attributes.dataSaver.map { "/data-saver/$hash/$it" } + } else { + networkApiChapter.data.attributes.data.map { "/data/$hash/$it" } } + val now = System.currentTimeMillis() + pageArray.forEach { imgUrl -> + val mdAtHomeUrl = "$host,$atHomeRequestUrl,$now" + pages += Page(pages.size, mdAtHomeUrl, imgUrl) + } + + return pages } fun externalParse(response: Response): String { diff --git a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt index aabdaf6b3..b3e341d3f 100644 --- a/app/src/main/java/exh/md/handlers/ApiMangaParser.kt +++ b/app/src/main/java/exh/md/handlers/ApiMangaParser.kt @@ -1,34 +1,32 @@ package exh.md.handlers +import com.elvishew.xlog.XLog import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.parseAs +import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.asJsoup import exh.log.xLogE -import exh.md.handlers.serializers.ApiChapterSerializer -import exh.md.handlers.serializers.ApiMangaSerializer -import exh.md.handlers.serializers.ChapterSerializer -import exh.md.utils.MdLang +import exh.md.handlers.serializers.AuthorResponseList +import exh.md.handlers.serializers.ChapterResponse +import exh.md.handlers.serializers.MangaResponse import exh.md.utils.MdUtil import exh.metadata.metadata.MangaDexSearchMetadata import exh.metadata.metadata.base.RaisedTag import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.insertFlatMetadata -import exh.metadata.metadata.base.insertFlatMetadataCompletable import exh.util.executeOnIO import exh.util.floor -import exh.util.nullIfZero +import okhttp3.OkHttpClient import okhttp3.Response -import rx.Completable -import rx.Single import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.MangaInfo -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy +import java.util.Date +import java.util.Locale -class ApiMangaParser(private val lang: String) { - val db: DatabaseHelper get() = Injekt.get() +class ApiMangaParser(val client: OkHttpClient, private val lang: String) { + val db: DatabaseHelper by injectLazy() val metaClass = MangaDexSearchMetadata::class @@ -40,44 +38,18 @@ class ApiMangaParser(private val lang: String) { }?.call() ?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!") - /** - * Parses metadata from the input and then copies it into the manga - * - * Will also save the metadata to the DB if possible - */ - fun parseToManga(manga: SManga, input: Response, coverUrls: List): 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, sourceId: Long): MangaInfo { + return parseToManga(manga, input.parseAs(MdUtil.jsonParser), coverUrls, sourceId) } - suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List, sourceId: Long): MangaInfo { + suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, coverUrls: List, sourceId: Long): MangaInfo { val mangaId = db.getManga(manga.key, sourceId).executeOnIO()?.id val metadata = if (mangaId != null) { val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO() flatMetadata?.raise(metaClass) ?: newMetaInstance() } else newMetaInstance() - parseInfoIntoMetadata(metadata, input, coverUrls) + parseIntoMetadata(metadata, input, coverUrls) if (mangaId != null) { metadata.mangaId = mangaId db.insertFlatMetadata(metadata.flatten()) @@ -86,69 +58,82 @@ class ApiMangaParser(private val lang: String) { return metadata.createMangaInfo(manga) } - fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List) = parseIntoMetadata(metadata, input, coverUrls) - + /** + * Parse the manga details json into metadata object + */ fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List) { + parseIntoMetadata(metadata, input.parseAs(MdUtil.jsonParser), coverUrls) + } + + fun parseIntoMetadata(metadata: MangaDexSearchMetadata, networkApiManga: MangaResponse, coverUrls: List) { with(metadata) { try { - val networkApiManga = input.parseAs(MdUtil.jsonParser) - val networkManga = networkApiManga.data.manga - mdId = MdUtil.getMangaId(input.request.url.toString()) - mdUrl = input.request.url.toString() - title = MdUtil.cleanString(networkManga.title) - thumbnail_url = if (coverUrls.isNotEmpty()) { - coverUrls.last() - } else { - 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() + val networkManga = networkApiManga.data.attributes + mdUuid = networkApiManga.data.id + title = MdUtil.cleanString(networkManga.title[lang] ?: networkManga.title["en"]!!) + altTitles = networkManga.altTitles.mapNotNull { it[lang] } + cover = + if (coverUrls.isNotEmpty()) { + coverUrls.last() + } else { + null + // networkManga.mainCover + } - networkManga.rating?.let { - rating = it.bayesian ?: it.mean - users = it.users - } - networkManga.links?.let { links -> - links.al?.let { anilist_id = it } - links.kt?.let { kitsu_id = it } - links.mal?.let { my_anime_list_id = it } - links.mu?.let { manga_updates_id = it } - links.ap?.let { anime_planet_id = it } - } - val filteredChapters = filterChapterForChecking(networkApiManga) + description = MdUtil.cleanDescription(networkManga.description["en"]!!) - val tempStatus = parseStatus(networkManga.publication!!.status) + val authorIds = networkApiManga.relationships.filter { it.type.equals("author", true) }.distinct() + + authors = runCatching { + val ids = authorIds.joinToString("&ids[]=", "?ids[]=") + val response = client.newCall(GET("${MdUtil.authorUrl}$ids")).execute() + val json = response.parseAs(MdUtil.jsonParser) + json.results.map { MdUtil.cleanString(it.data.attributes.name) }.takeUnless { it.isEmpty() } + }.getOrNull() + + langFlag = networkManga.originalLanguage + val lastChapter = networkManga.lastChapter.toFloatOrNull() + lastChapterNumber = lastChapter?.floor() + + /*networkManga.rating?.let { + manga.rating = it.bayesian ?: it.mean + manga.users = it.users + }*/ + + networkManga.links?.let { + it["al"]?.let { anilistId = it } + it["kt"]?.let { kitsuId = it } + it["mal"]?.let { myAnimeListId = it } + it["mu"]?.let { mangaUpdatesId = it } + it["ap"]?.let { animePlanetId = it } + } + // val filteredChapters = filterChapterForChecking(networkApiManga) + + val tempStatus = parseStatus(networkManga.status ?: "") val publishedOrCancelled = tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED - if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) { - status = SManga.COMPLETED - missing_chapters = null - maxChapterNumber = networkApiManga.data.manga.lastChapter?.toDoubleOrNull()?.floor() - } else { - status = tempStatus - } + /*if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) { + manga.status = SManga.COMPLETED + manga.missing_chapters = null + } else {*/ + status = tempStatus + // } - val genres = - networkManga.tags.mapNotNull { FilterHandler.allTypes[it.toString()] } - .toMutableList() + // things that will go with the genre tags but aren't actually genre + val nonGenres = listOfNotNull( + networkManga.publicationDemographic?.let { RaisedTag("Demographic", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) }, + networkManga.contentRating?.let { RaisedTag("Content Rating", it.capitalize(Locale.US), MangaDexSearchMetadata.TAG_TYPE_DEFAULT) }, + ) - networkManga.publication.demographic?.let { demographicInt -> - val demographic = FilterHandler.demographics().firstOrNull { it.id.toInt() == demographicInt } - - if (demographic != null) { - genres.add(0, demographic.name) + val genres = nonGenres + networkManga.tags + .mapNotNull { dexTag -> + dexTag.attributes.name[lang] ?: dexTag.attributes.name["en"] + }.map { + RaisedTag("Tags", it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT) } - } - - if (networkManga.isHentai) { - genres.add("Hentai") - } if (tags.isNotEmpty()) tags.clear() - tags += genres.map { RaisedTag(null, it, MangaDexSearchMetadata.TAG_TYPE_DEFAULT) } + tags += genres } catch (e: Exception) { xLogE("Parse into metadata error", e) throw e @@ -160,15 +145,14 @@ class ApiMangaParser(private val lang: String) { * If chapter title is oneshot or a chapter exists which matches the last chapter in the required language * return manga is complete */ - private fun isMangaCompleted( + /*private fun isMangaCompleted( serializer: ApiMangaSerializer, filteredChapters: List ): Boolean { - val finalChapterNumber = serializer.data.manga.lastChapter - if (filteredChapters.isEmpty() || finalChapterNumber.isNullOrEmpty()) { + if (filteredChapters.isEmpty() || serializer.data.manga.lastChapter.isNullOrEmpty()) { return false } - // just to fix the stupid lint + val finalChapterNumber = serializer.data.manga.lastChapter!! if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) { filteredChapters.firstOrNull()?.let { if (isOneShot(it, finalChapterNumber)) { @@ -177,36 +161,39 @@ class ApiMangaParser(private val lang: String) { } } val removeOneshots = filteredChapters.asSequence() - .map { it.chapter?.toDoubleOrNull()?.floor()?.nullIfZero() } - .filterNotNull() + .map { it.chapter!!.toDoubleOrNull() } + .filter { it != null } + .map { floor(it!!).toInt() } + .filter { it != 0 } .toList().distinctBy { it } - return removeOneshots.toList().size == finalChapterNumber.toDouble().floor() - } + return removeOneshots.toList().size == floor(finalChapterNumber.toDouble()).toInt() + }*/ - private fun filterChapterForChecking(serializer: ApiMangaSerializer): List { - return serializer.data.chapters.asSequence() - .filter { lang == it.language } - .filter { - it.chapter?.let { chapterNumber -> - if (chapterNumber.toDoubleOrNull() == null) { - return@filter false - } - return@filter true - } - return@filter false - }.toList() - } + /* private fun filterChapterForChecking(serializer: ApiMangaSerializer): List { + serializer.data.chapters ?: return emptyList() + return serializer.data.chapters.asSequence() + .filter { langs.contains(it.language) } + .filter { + it.chapter?.let { chapterNumber -> + if (chapterNumber.toDoubleOrNull() == null) { + return@filter false + } + return@filter true + } + return@filter false + }.toList() + }*/ - private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean { + /*private fun isOneShot(chapter: ChapterSerializer, finalChapterNumber: String): Boolean { return chapter.title.equals("oneshot", true) || ((chapter.chapter.isNullOrEmpty() || chapter.chapter == "0") && MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) - } + }*/ - private fun parseStatus(status: Int) = when (status) { - 1 -> SManga.ONGOING - 2 -> SManga.PUBLICATION_COMPLETE - 3 -> SManga.CANCELLED - 4 -> SManga.HIATUS + private fun parseStatus(status: String) = when (status) { + "ongoing" -> SManga.ONGOING + "complete" -> SManga.PUBLICATION_COMPLETE + "abandoned" -> SManga.CANCELLED + "hiatus" -> SManga.HIATUS else -> SManga.UNKNOWN } @@ -214,88 +201,69 @@ class ApiMangaParser(private val lang: String) { * Parse for the random manga id from the [MdUtil.randMangaPage] response. */ fun randomMangaIdParse(response: Response): String { - val randMangaUrl = response.asJsoup() - .select("link[rel=canonical]") - .attr("href") - return MdUtil.getMangaId(randMangaUrl) + return response.parseAs(MdUtil.jsonParser).data.id } - fun chapterListParse(response: Response): List { - return chapterListParse(response.parseAs(MdUtil.jsonParser)) + fun chapterListParse(chapterListResponse: List, groupMap: Map): List { + val now = Date().time + + return chapterListResponse.asSequence() + .map { + mapChapter(it, groupMap) + }.filter { + it.dateUpload <= now && "MangaPlus" != it.scanlator + }.toList() } - fun chapterListParse(networkApiManga: ApiMangaSerializer): List { - val now = System.currentTimeMillis() - val networkManga = networkApiManga.data.manga - val networkChapters = networkApiManga.data.chapters - val groups = networkApiManga.data.groups.mapNotNull { - if (it.name == null) { - null - } else { - it.id to it.name - } - }.toMap() - - val status = networkManga.publication!!.status - - val finalChapterNumber = networkManga.lastChapter - - // Skip chapters that don't match the desired language, or are future releases - - val chapLang = MdLang.values().firstOrNull { lang == it.dexLang } - return networkChapters.asSequence() - .filter { lang == it.language && (it.timestamp * 1000) <= now } - .map { mapChapter(it, finalChapterNumber, status, chapLang, networkChapters.size, groups) }.toList() - } - - fun chapterParseForMangaId(response: Response): Int { + fun chapterParseForMangaId(response: Response): String { try { - return response.parseAs().data.mangaId + return response.parseAs(MdUtil.jsonParser) + .relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found") } catch (e: Exception) { - xLogE("Parse for manga id error", e) + XLog.e(e) throw e } } private fun mapChapter( - networkChapter: ChapterSerializer, - finalChapterNumber: String?, - status: Int, - chapLang: MdLang?, - totalChapterCount: Int, - groups: Map + networkChapter: ChapterResponse, + groups: Map, ): ChapterInfo { - val key = MdUtil.oldApiChapter + networkChapter.id - - // Build chapter name + val chapter = SChapter.create() + val attributes = networkChapter.data.attributes + val key = MdUtil.chapterSuffix + networkChapter.data.id val chapterName = mutableListOf() + // Build chapter name - if (!networkChapter.volume.isNullOrBlank()) { - val vol = "Vol." + networkChapter.volume + if (attributes.volume != null) { + val vol = "Vol." + attributes.volume chapterName.add(vol) // todo // chapter.vol = vol } - if (!networkChapter.chapter.isNullOrBlank()) { - val chp = "Ch." + networkChapter.chapter - chapterName.add(chp) - // chapter.chapter_txt = chp - } - if (!networkChapter.title.isNullOrBlank()) { + if (attributes.chapter.isNullOrBlank().not()) { if (chapterName.isNotEmpty()) { chapterName.add("-") } - // todo - chapterName.add(networkChapter.title) - // chapter.chapter_title = MdUtil.cleanString(networkChapter.title) + val chp = "Ch.${attributes.chapter}" + chapterName.add(chp) + // chapter.chapter_txt = chp + } + + if (attributes.title.isNullOrBlank().not()) { + if (chapterName.isNotEmpty()) { + chapterName.add("-") + } + chapterName.add(attributes.title!!) + chapter.name = MdUtil.cleanString(attributes.title) } // if volume, chapter and title is empty its a oneshot if (chapterName.isEmpty()) { chapterName.add("Oneshot") } - if ((status == 2 || status == 3)) { + /*if ((status == 2 || status == 3)) { if (finalChapterNumber != null) { if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) || networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0 @@ -303,26 +271,25 @@ class ApiMangaParser(private val lang: String) { chapterName.add("[END]") } } - } + }*/ val name = MdUtil.cleanString(chapterName.joinToString(" ")) // Convert from unix time - val dateUpload = networkChapter.timestamp * 1000 - val scanlatorName = mutableSetOf() + val dateUpload = MdUtil.parseDate(attributes.publishAt) - networkChapter.groups.mapNotNull { groups[it] }.forEach { scanlatorName.add(it) } + val scanlatorName = networkChapter.relationships.filter { it.type == "scanlation_group" }.mapNotNull { groups[it.id] }.toSet() val scanlator = MdUtil.cleanString(MdUtil.getScanlatorString(scanlatorName)) - // val mangadexChapterId = MdUtil.getChapterId(chapter.url) + // chapter.mangadex_chapter_id = MdUtil.getChapterId(chapter.url) - // val language = chapLang?.name + // chapter.language = MdLang.fromIsoCode(attributes.translatedLanguage)?.prettyPrint ?: "" return ChapterInfo( key = key, name = name, + scanlator = scanlator, dateUpload = dateUpload, - scanlator = scanlator ) } } diff --git a/app/src/main/java/exh/md/handlers/FilterHandler.kt b/app/src/main/java/exh/md/handlers/FilterHandler.kt index 1ed7f65c6..92974e3f3 100644 --- a/app/src/main/java/exh/md/handlers/FilterHandler.kt +++ b/app/src/main/java/exh/md/handlers/FilterHandler.kt @@ -1,180 +1,261 @@ package exh.md.handlers +import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList +import okhttp3.HttpUrl +import java.util.Locale -class FilterHandler { +class FilterHandler(private val preferencesHelper: PreferencesHelper) { - class TextField(name: String, val key: String) : Filter.Text(name) - class Tag(val id: String, name: String) : Filter.TriState(name) - class Switch(val id: String, name: String) : Filter.CheckBox(name) - class ContentList(contents: List) : Filter.Group("Content", contents) - class FormatList(formats: List) : Filter.Group("Format", formats) - class GenreList(genres: List) : Filter.Group("Genres", genres) - class PublicationStatusList(statuses: List) : Filter.Group("Publication Status", statuses) - class DemographicList(demographics: List) : Filter.Group("Demographic", demographics) + internal fun getMDFilterList(): FilterList { + val filters = mutableListOf( + OriginalLanguageList(getOriginalLanguage()), + DemographicList(getDemographics()), + StatusList(getStatus()), + SortFilter(sortableList.map { it.first }.toTypedArray()), + TagList(getTags()), + TagInclusionMode(), + TagExclusionMode() + ).toMutableList() - class R18 : Filter.Select("R18+", arrayOf("Default", "Show all", "Show only", "Show none")) - class ThemeList(themes: List) : Filter.Group("Themes", themes) - class TagInclusionMode : Filter.Select("Tag inclusion", arrayOf("All (and)", "Any (or)"), 0) - class TagExclusionMode : Filter.Select("Tag exclusion", arrayOf("All (and)", "Any (or)"), 1) + if (true) { // preferencesHelper.showR18Filter()) { + filters.add(2, ContentRatingList(getContentRating())) + } - class SortFilter : Filter.Sort( - "Sort", - sortables().map { it.first }.toTypedArray(), - Selection(0, false) + return FilterList(list = filters.toList()) + } + + private class Demographic(name: String) : Filter.CheckBox(name) + private class DemographicList(demographics: List) : + Filter.Group("Publication Demographic", demographics) + + private fun getDemographics() = listOf( + Demographic("None"), + Demographic("Shounen"), + Demographic("Shoujo"), + Demographic("Seinen"), + Demographic("Josei") ) - class OriginalLanguage : Filter.Select("Original Language", sourceLang().map { it.first }.toTypedArray()) + private class Status(name: String) : Filter.CheckBox(name) + private class StatusList(status: List) : + Filter.Group("Status", status) - fun getFilterList() = FilterList( - TextField("Author", "author"), - TextField("Artist", "artist"), - R18(), - SortFilter(), - DemographicList(demographics()), - PublicationStatusList(publicationStatus()), - OriginalLanguage(), - ContentList(contentType()), - FormatList(formats()), - GenreList(genre()), - ThemeList(themes()), - TagInclusionMode(), - TagExclusionMode() + private fun getStatus() = listOf( + Status("Onging"), + Status("Completed"), + Status("Hiatus"), + Status("Abandoned"), ) - companion object { - fun demographics() = listOf( - Switch("1", "Shounen"), - Switch("2", "Shoujo"), - Switch("3", "Seinen"), - Switch("4", "Josei") - ) + private class ContentRating(name: String) : Filter.CheckBox(name) + private class ContentRatingList(contentRating: List) : + Filter.Group("Content Rating", contentRating) - fun publicationStatus() = listOf( - Switch("1", "Ongoing"), - Switch("2", "Completed"), - Switch("3", "Cancelled"), - Switch("4", "Hiatus") - ) + private fun getContentRating() = listOf( + ContentRating("Safe"), + ContentRating("Suggestive"), + ContentRating("Erotica"), + ContentRating("Pornographic") + ) - fun sortables() = listOf( - Triple("Update date", 1, 0), - Triple("Alphabetically", 2, 3), - Triple("Number of comments", 4, 5), - Triple("Rating", 6, 7), - Triple("Views", 8, 9), - Triple("Follows", 10, 11) - ) + private class OriginalLanguage(name: String, val isoCode: String) : Filter.CheckBox(name) + private class OriginalLanguageList(originalLanguage: List) : + Filter.Group("Original language", originalLanguage) - fun sourceLang() = listOf( - Pair("All", "0"), - Pair("Japanese", "2"), - Pair("English", "1"), - Pair("Polish", "3"), - Pair("German", "8"), - Pair("French", "10"), - Pair("Vietnamese", "12"), - Pair("Chinese", "21"), - Pair("Indonesian", "27"), - Pair("Korean", "28"), - Pair("Spanish (LATAM)", "29"), - Pair("Thai", "32"), - Pair("Filipino", "34") - ) + private fun getOriginalLanguage() = listOf( + OriginalLanguage("Japanese (Manga)", "jp"), + OriginalLanguage("Chinese (Manhua)", "cn"), + OriginalLanguage("Korean (Manhwa)", "kr"), + ) - fun contentType() = listOf( - Tag("9", "Ecchi"), - Tag("32", "Smut"), - Tag("49", "Gore"), - Tag("50", "Sexual Violence") - ).sortedWith(compareBy { it.name }) + internal class Tag(val id: String, name: String) : Filter.TriState(name) + private class TagList(tags: List) : Filter.Group("Tags", tags) - fun formats() = listOf( - Tag("1", "4-koma"), - Tag("4", "Award Winning"), - Tag("7", "Doujinshi"), - Tag("21", "Oneshot"), - Tag("36", "Long Strip"), - Tag("42", "Adaptation"), - Tag("43", "Anthology"), - Tag("44", "Web Comic"), - Tag("45", "Full Color"), - Tag("46", "User Created"), - Tag("47", "Official Colored"), - Tag("48", "Fan Colored") - ).sortedWith(compareBy { it.name }) + internal fun getTags() = listOf( + Tag("391b0423-d847-456f-aff0-8b0cfc03066b", "Action"), + Tag("f4122d1c-3b44-44d0-9936-ff7502c39ad3", "Adaptation"), + Tag("87cc87cd-a395-47af-b27a-93258283bbc6", "Adventure"), + Tag("e64f6742-c834-471d-8d72-dd51fc02b835", "Aliens"), + Tag("3de8c75d-8ee3-48ff-98ee-e20a65c86451", "Animals"), + Tag("51d83883-4103-437c-b4b1-731cb73d786c", "Anthology"), + Tag("0a39b5a1-b235-4886-a747-1d05d216532d", "Award Winning"), + Tag("5920b825-4181-4a17-beeb-9918b0ff7a30", "Boy Love"), + Tag("4d32cc48-9f00-4cca-9b5a-a839f0764984", "Comedy"), + Tag("ea2bc92d-1c26-4930-9b7c-d5c0dc1b6869", "Cooking"), + Tag("5ca48985-9a9d-4bd8-be29-80dc0303db72", "Crime"), + Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Crossdressing"), + Tag("da2d50ca-3018-4cc0-ac7a-6b7d472a29ea", "Delinquents"), + Tag("39730448-9a5f-48a2-85b0-a70db87b1233", "Demons"), + Tag("b13b2a48-c720-44a9-9c77-39c9979373fb", "Doujinshi"), + Tag("b9af3a63-f058-46de-a9a0-e0c13906197a", "Drama"), + Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Ecchi"), + Tag("7b2ce280-79ef-4c09-9b58-12b7c23a9b78", "Fan Colored"), + Tag("cdc58593-87dd-415e-bbc0-2ec27bf404cc", "Fantasy"), + Tag("b11fda93-8f1d-4bef-b2ed-8803d3733170", "4-koma"), + Tag("f5ba408b-0e7a-484d-8d49-4e9125ac96de", "Full Color"), + Tag("2bd2e8d0-f146-434a-9b51-fc9ff2c5fe6a", "Genderswap"), + Tag("3bb26d85-09d5-4d2e-880c-c34b974339e9", "Ghosts"), + Tag("a3c67850-4684-404e-9b7f-c69850ee5da6", "Girl Love"), + Tag("b29d6a3d-1569-4e7a-8caf-7557bc92cd5d", "Gore"), + Tag("fad12b5e-68ba-460e-b933-9ae8318f5b65", "Gyaru"), + Tag("aafb99c1-7f60-43fa-b75f-fc9502ce29c7", "Harem"), + Tag("33771934-028e-4cb3-8744-691e866a923e", "Historical"), + Tag("cdad7e68-1419-41dd-bdce-27753074a640", "Horror"), + Tag("5bd0e105-4481-44ca-b6e7-7544da56b1a3", "Incest"), + Tag("ace04997-f6bd-436e-b261-779182193d3d", "Isekai"), + Tag("2d1f5d56-a1e5-4d0d-a961-2193588b08ec", "Loli"), + Tag("3e2b8dae-350e-4ab8-a8ce-016e844b9f0d", "Long Strip"), + Tag("85daba54-a71c-4554-8a28-9901a8b0afad", "Mafia"), + Tag("a1f53773-c69a-4ce5-8cab-fffcd90b1565", "Magic"), + Tag("81c836c9-914a-4eca-981a-560dad663e73", "Magical Girls"), + Tag("799c202e-7daa-44eb-9cf7-8a3c0441531e", "Martial Arts"), + Tag("50880a9d-5440-4732-9afb-8f457127e836", "Mecha"), + Tag("c8cbe35b-1b2b-4a3f-9c37-db84c4514856", "Medical"), + Tag("ac72833b-c4e9-4878-b9db-6c8a4a99444a", "Military"), + Tag("dd1f77c5-dea9-4e2b-97ae-224af09caf99", "Monster Girls"), + Tag("t36fd93ea-e8b8-445e-b836-358f02b3d33d", "Monsters"), + Tag("f42fbf9e-188a-447b-9fdc-f19dc1e4d685", "Music"), + Tag("ee968100-4191-4968-93d3-f82d72be7e46", "Mystery"), + Tag("489dd859-9b61-4c37-af75-5b18e88daafc", "Ninja"), + Tag("92d6d951-ca5e-429c-ac78-451071cbf064", "Office Workers"), + Tag("320831a8-4026-470b-94f6-8353740e6f04", "Official Colored"), + Tag("0234a31e-a729-4e28-9d6a-3f87c4966b9e", "Oneshot"), + Tag("b1e97889-25b4-4258-b28b-cd7f4d28ea9b", "Philosophical"), + Tag("df33b754-73a3-4c54-80e6-1a74a8058539", "Police"), + Tag("9467335a-1b83-4497-9231-765337a00b96", "Post-Apocalyptic"), + Tag("3b60b75c-a2d7-4860-ab56-05f391bb889c", "Psychological"), + Tag("0bc90acb-ccc1-44ca-a34a-b9f3a73259d0", "Reincarnation"), + Tag("65761a2a-415e-47f3-bef2-a9dababba7a6", "Reverse Harem"), + Tag("423e2eae-a7a2-4a8b-ac03-a8351462d71d", "Romance"), + Tag("81183756-1453-4c81-aa9e-f6e1b63be016", "Samurai"), + Tag("caaa44eb-cd40-4177-b930-79d3ef2afe87", "School Life"), + Tag("256c8bd9-4904-4360-bf4f-508a76d67183", "Sci-Fi"), + Tag("97893a4c-12af-4dac-b6be-0dffb353568e", "Sexual Violence"), + Tag("ddefd648-5140-4e5f-ba18-4eca4071d19b", "Shota"), + Tag("e5301a23-ebd9-49dd-a0cb-2add944c7fe9", "Slice of Life"), + Tag("69964a64-2f90-4d33-beeb-f3ed2875eb4c", "Sports"), + Tag("7064a261-a137-4d3a-8848-2d385de3a99c", "Superhero"), + Tag("eabc5b4c-6aff-42f3-b657-3e90cbd00b75", "Supernatural"), + Tag("5fff9cde-849c-4d78-aab0-0d52b2ee1d25", "Survival"), + Tag("07251805-a27e-4d59-b488-f0bfbec15168", "Thriller"), + Tag("292e862b-2d17-4062-90a2-0356caa4ae27", "Time Travel"), + Tag("f8f62932-27da-4fe4-8ee1-6779a8c5edba", "Tragedy"), + Tag("31932a7e-5b8e-49a6-9f12-2afa39dc544c", "Traditional Games"), + Tag("891cf039-b895-47f0-9229-bef4c96eccd4", "User Created"), + Tag("d7d1730f-6eb0-4ba6-9437-602cac38664c", "Vampires"), + Tag("9438db5a-7e2a-4ac0-b39e-e0d95a34b8a8", "Video Games"), + Tag("d14322ac-4d6f-4e9b-afd9-629d5f4d8a41", "Villainess"), + Tag("8c86611e-fab7-4986-9dec-d1a2f44acdd5", "Virtual Reality"), + Tag("e197df38-d0e7-43b5-9b09-2842d0c326dd", "Web Comic"), + Tag("acc803a4-c95a-4c22-86fc-eb6b582d82a2", "Wuxia"), + Tag("631ef465-9aba-4afb-b0fc-ea10efe274a8", "Zombies") + ) - fun genre() = listOf( - Tag("2", "Action"), - Tag("3", "Adventure"), - Tag("5", "Comedy"), - Tag("8", "Drama"), - Tag("10", "Fantasy"), - Tag("13", "Historical"), - Tag("14", "Horror"), - Tag("17", "Mecha"), - Tag("18", "Medical"), - Tag("20", "Mystery"), - Tag("22", "Psychological"), - Tag("23", "Romance"), - Tag("25", "Sci-Fi"), - Tag("28", "Shoujo Ai"), - Tag("30", "Shounen Ai"), - Tag("31", "Slice of Life"), - Tag("33", "Sports"), - Tag("35", "Tragedy"), - Tag("37", "Yaoi"), - Tag("38", "Yuri"), - Tag("41", "Isekai"), - Tag("51", "Crime"), - Tag("52", "Magical Girls"), - Tag("53", "Philosophical"), - Tag("54", "Superhero"), - Tag("55", "Thriller"), - Tag("56", "Wuxia") - ).sortedWith(compareBy { it.name }) + private class TagInclusionMode : + Filter.Select("Included tags mode", arrayOf("And", "Or"), 0) - fun themes() = listOf( - Tag("6", "Cooking"), - Tag("11", "Gyaru"), - Tag("12", "Harem"), - Tag("16", "Martial Arts"), - Tag("19", "Music"), - Tag("24", "School Life"), - Tag("34", "Supernatural"), - Tag("40", "Video Games"), - Tag("57", "Aliens"), - Tag("58", "Animals"), - Tag("59", "Crossdressing"), - Tag("60", "Demons"), - Tag("61", "Delinquents"), - Tag("62", "Genderswap"), - Tag("63", "Ghosts"), - Tag("64", "Monster Girls"), - Tag("65", "Loli"), - Tag("66", "Magic"), - Tag("67", "Military"), - Tag("68", "Monsters"), - Tag("69", "Ninja"), - Tag("70", "Office Workers"), - Tag("71", "Police"), - Tag("72", "Post-Apocalyptic"), - Tag("73", "Reincarnation"), - Tag("74", "Reverse Harem"), - Tag("75", "Samurai"), - Tag("76", "Shota"), - Tag("77", "Survival"), - Tag("78", "Time Travel"), - Tag("79", "Vampires"), - Tag("80", "Traditional Games"), - Tag("81", "Virtual Reality"), - Tag("82", "Zombies"), - Tag("83", "Incest"), - Tag("84", "Mafia"), - Tag("85", "Villainess") - ).sortedWith(compareBy { it.name }) + private class TagExclusionMode : + Filter.Select("Excluded tags mode", arrayOf("And", "Or"), 1) - 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) : 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() } } diff --git a/app/src/main/java/exh/md/handlers/FollowsHandler.kt b/app/src/main/java/exh/md/handlers/FollowsHandler.kt index 4f0bd5490..c40342359 100644 --- a/app/src/main/java/exh/md/handlers/FollowsHandler.kt +++ b/app/src/main/java/exh/md/handlers/FollowsHandler.kt @@ -3,202 +3,203 @@ package exh.md.handlers import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.POST import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.parseAs -import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.MetadataMangasPage import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.util.lang.withIOContext -import exh.log.xLogD -import exh.log.xLogE -import exh.md.handlers.serializers.FollowPage -import exh.md.handlers.serializers.FollowsIndividualSerializer -import exh.md.handlers.serializers.FollowsPageSerializer +import exh.md.handlers.serializers.MangaListResponse +import exh.md.handlers.serializers.MangaResponse +import exh.md.handlers.serializers.UpdateReadingStatus import exh.md.utils.FollowStatus import exh.md.utils.MdUtil import exh.metadata.metadata.MangaDexSearchMetadata -import exh.util.awaitResponse -import exh.util.floor -import kotlinx.serialization.decodeFromString +import exh.util.under +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import okhttp3.CacheControl -import okhttp3.Call -import okhttp3.FormBody import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response -import okio.EOFException +import tachiyomi.source.model.MangaInfo +import java.util.Locale -class FollowsHandler(val client: OkHttpClient, val headers: Headers, val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) { +class FollowsHandler( + val client: OkHttpClient, + val headers: Headers, + val preferences: PreferencesHelper, + private val lang: String, + private val useLowQualityCovers: Boolean, + private val mdList: MdList +) { /** - * fetch follows by page + * fetch all follows */ - suspend fun fetchFollows(): MangasPage { - return client.newCall(followsListRequest()) - .await() - .let { response -> - followsParseMangaPage(response) + suspend fun fetchFollows(): MetadataMangasPage { + return withIOContext { + val response = client.newCall(followsListRequest(0)).await() + + val mangaListResponse = response.parseAs(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(MdUtil.jsonParser) + results.addAll(newMangaListResponse.results) + hasMoreResults = newMangaListResponse.limit + newMangaListResponse.offset under newMangaListResponse.total + lastOffset = newMangaListResponse.offset } + val statusListResponse = client.newCall(statusListRequest()).await().parseAs() + followsParseMangaPage(results, statusListResponse) + } } /** * Parse follows api to manga page * used when multiple follows */ - private fun followsParseMangaPage(response: Response, forceHd: Boolean = false): MetadataMangasPage { - val followsPageResult = try { - MdUtil.jsonParser.decodeFromString( - response.body?.string().orEmpty() - ) - } catch (e: Exception) { - xLogE("error parsing follows", e) - FollowsPageSerializer(404, emptyList()) - } + private fun followsParseMangaPage(response: List, statusListResponse: JsonObject): MetadataMangasPage { + val comparator = compareBy> { it.second.followStatus } + .thenBy { it.first.title } + val result = response.map { + MdUtil.createMangaEntry(it, lang, useLowQualityCovers) to MangaDexSearchMetadata().apply { + followStatus = getFollowStatus(statusListResponse, it.data.id).int + } + }.sortedWith(comparator) - if (followsPageResult.data.isNullOrEmpty() || followsPageResult.code != 200) { - return MetadataMangasPage(emptyList(), false, emptyList()) - } - val lowQualityCovers = if (forceHd) false else useLowQualityCovers - - val follows = followsPageResult.data.map { - followFromElement(it, lowQualityCovers) - } - - val comparator = compareBy> { it.second.follow_status }.thenBy { it.first.title } - - val result = follows.sortedWith(comparator) - - return MetadataMangasPage(result.map { it.first }, false, result.map { it.second }) + return MetadataMangasPage(result.map { it.first.toSManga() }, false, result.map { it.second }) } /** * fetch follow status used when fetching status for 1 manga */ - private fun followStatusParse(response: Response): Track { - val followsPageResult = try { - response.parseAs(MdUtil.jsonParser) - } catch (e: Exception) { - xLogE("error parsing follows", e) - throw e - } - + private fun followStatusParse(response: Response, statusListResponse: JsonObject): Track { + val mangaResponse = response.parseAs(MdUtil.jsonParser) val track = Track.create(TrackManager.MDLIST) - if (followsPageResult.code == 404) { - track.status = FollowStatus.UNFOLLOWED.int - } else { - val follow = followsPageResult.data ?: throw Exception("Invalid response ${followsPageResult.code}") - track.status = follow.followType - if (follow.chapter.isNotBlank()) { + track.status = getFollowStatus(statusListResponse, mangaResponse.data.id).int + track.tracking_url = MdUtil.baseUrl + "/manga/" + mangaResponse.data.id + track.title = mangaResponse.data.attributes.title[lang] ?: mangaResponse.data.attributes.title["en"]!! + + /* if (follow.chapter.isNotBlank()) { track.last_chapter_read = follow.chapter.toFloat().floor() - } - track.tracking_url = MdUtil.baseUrl + follow.mangaId.toString() - track.title = follow.mangaTitle - } + }*/ return track } /** * build Request for follows page */ - private fun followsListRequest(): Request { - return GET("${MdUtil.apiUrl}${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK) - } + private fun followsListRequest(offset: Int): Request { + val tempUrl = MdUtil.userFollows.toHttpUrl().newBuilder() - /** - * Parse result element to manga - */ - private fun followFromElement(result: FollowPage, lowQualityCovers: Boolean): Pair { - val manga = SManga.create() - manga.title = MdUtil.cleanString(result.mangaTitle) - manga.url = "/manga/${result.mangaId}/" - manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers) - return manga to MangaDexSearchMetadata().apply { - title = manga.title - mdUrl = manga.url - thumbnail_url = manga.thumbnail_url - follow_status = FollowStatus.fromInt(result.followType).int + tempUrl.apply { + addQueryParameter("limit", MdUtil.mangaLimit.toString()) + addQueryParameter("offset", offset.toString()) } + return GET(tempUrl.build().toString(), MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK) } /** * Change the status of a manga */ - suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean { + suspend fun updateFollowStatus(mangaId: String, followStatus: FollowStatus): Boolean { return withIOContext { - if (followStatus == FollowStatus.UNFOLLOWED) { - client.newCall( - GET( - "${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_unfollow&id=$mangaID&type=$mangaID", - headers, - CacheControl.FORCE_NETWORK - ) + val status = when (followStatus == FollowStatus.UNFOLLOWED) { + true -> null + false -> followStatus.name.toLowerCase(Locale.US) + } + + val jsonString = MdUtil.jsonParser.encodeToString(UpdateReadingStatus(status)) + + val postResult = client.newCall( + POST( + MdUtil.updateReadingStatusUrl(mangaId), + MdUtil.getAuthHeaders(headers, preferences, mdList), + jsonString.toRequestBody("application/json".toMediaType()) ) - } else { - val status = followStatus.int - client.newCall( - GET( - "${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_follow&id=$mangaID&type=$status", - headers, - CacheControl.FORCE_NETWORK - ) - ) - }.succeeded() + ).await() + postResult.isSuccessful } } suspend fun updateReadingProgress(track: Track): Boolean { - return withIOContext { - val mangaID = MdUtil.getMangaId(track.tracking_url) + return true + /*return withIOContext { + val mangaID = getMangaId(track.tracking_url) val formBody = FormBody.Builder() .add("volume", "0") .add("chapter", track.last_chapter_read.toString()) - xLogD("chapter to update %s", track.last_chapter_read.toString()) - client.newCall( - POST( - "${MdUtil.baseUrl}/ajax/actions.ajax.php?function=edit_progress&id=$mangaID", - headers, - formBody.build() - ) - ).succeeded() - } + XLog.d("chapter to update %s", track.last_chapter_read.toString()) + val result = runCatching { + client.newCall( + POST( + "$baseUrl/ajax/actions.ajax.php?function=edit_progress&id=$mangaID", + headers, + formBody.build() + ) + ).execute() + } + result.exceptionOrNull()?.let { + if (it is EOFException) { + return@withIOContext true + } else { + XLog.e("error updating reading progress", it) + return@withIOContext false + } + } + result.isSuccess + }*/ } suspend fun updateRating(track: Track): Boolean { - return withIOContext { - val mangaID = MdUtil.getMangaId(track.tracking_url) - client.newCall( - GET( - "${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}", - headers + return true + /*return withIOContext { + val mangaID = getMangaId(track.tracking_url) + val result = runCatching { + client.newCall( + GET( + "$baseUrl/ajax/actions.ajax.php?function=manga_rating&id=$mangaID&rating=${track.score.toInt()}", + headers + ) ) - ).succeeded() - } - } + .execute() + } - private suspend fun Call.succeeded() = withIOContext { - try { - await().body?.string().let { body -> - (body != null && body.isEmpty()).also { - if (!it) xLogD(body) + result.exceptionOrNull()?.let { + if (it is EOFException) { + return@withIOContext true + } else { + XLog.e("error updating rating", it) + return@withIOContext false } } - } catch (e: EOFException) { - true - } + result.isSuccess + }*/ } /** * fetch all manga from all possible pages */ - suspend fun fetchAllFollows(forceHd: Boolean): List> { + suspend fun fetchAllFollows(): List> { return withIOContext { - val response = client.newCall(followsListRequest()).await() - val mangasPage = followsParseMangaPage(response, forceHd) - mangasPage.mangas.mapIndexed { index, sManga -> - sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata + val metadata: List + fetchFollows().also { metadata = it.mangasMetadata.filterIsInstance() }.mangas.mapIndexed { index, manga -> + manga to metadata[index] } } } @@ -206,12 +207,20 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere suspend fun fetchTrackingInfo(url: String): Track { return withIOContext { val request = GET( - MdUtil.apiUrl + MdUtil.followsMangaApi + MdUtil.getMangaId(url), - headers, + MdUtil.mangaUrl + "/" + MdUtil.getMangaId(url), + MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK ) - val response = client.newCall(request).awaitResponse() - followStatusParse(response) + val response = client.newCall(request).await() + val statusListResponse = client.newCall(statusListRequest()).await().parseAs(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) + } } diff --git a/app/src/main/java/exh/md/handlers/MangaHandler.kt b/app/src/main/java/exh/md/handlers/MangaHandler.kt index 936b9440d..ca96e2a8b 100644 --- a/app/src/main/java/exh/md/handlers/MangaHandler.kt +++ b/app/src/main/java/exh/md/handlers/MangaHandler.kt @@ -1,6 +1,7 @@ package exh.md.handlers import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.mdlist.MdList import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess import eu.kanade.tachiyomi.network.await @@ -9,12 +10,15 @@ import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.toMangaInfo import eu.kanade.tachiyomi.source.model.toSChapter +import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.withIOContext -import exh.md.handlers.serializers.ApiCovers -import exh.md.handlers.serializers.ApiMangaSerializer +import exh.md.handlers.serializers.ChapterListResponse +import exh.md.handlers.serializers.ChapterResponse +import exh.md.handlers.serializers.GroupListResponse import exh.md.utils.MdUtil import exh.metadata.metadata.MangaDexSearchMetadata +import exh.util.under import kotlinx.coroutines.async import okhttp3.CacheControl import okhttp3.Headers @@ -26,124 +30,179 @@ import tachiyomi.source.model.MangaInfo import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -class MangaHandler(val client: OkHttpClient, val headers: Headers, val lang: String, val forceLatestCovers: Boolean = false) { +class MangaHandler(val client: OkHttpClient, val headers: Headers, private val lang: String, private val forceLatestCovers: Boolean = false) { - // TODO make use of this suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair> { return withIOContext { - val apiNetworkManga = client.newCall(apiRequest(manga)).await().parseAs(MdUtil.jsonParser) + val response = client.newCall(mangaRequest(manga)).await() val covers = getCovers(manga, forceLatestCovers) - val parser = ApiMangaParser(lang) + val parser = ApiMangaParser(client, lang) - // TODO fix this - /*val mangaInfo = parser.parseToManga(manga, response, covers, sourceId) - val chapterList = parser.chapterListParse(apiNetworkManga) - - mangaInfo to chapterList*/ - manga to emptyList() + parser.parseToManga(manga, response, covers, sourceId) to getChapterList(manga) } } - private suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List { - return if (forceLatestCovers) { - val covers = client.newCall(coverRequest(manga)).await().parseAs(MdUtil.jsonParser) - covers.data.map { it.url } - } else { - emptyList() - } + suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List { + /* if (forceLatestCovers) { + val covers = client.newCall(coverRequest(manga)).await().parseAs(MdUtil.jsonParser) + return covers.data.map { it.url } + } else {*/ + return emptyList() + // } } - suspend fun getMangaIdFromChapterId(urlChapterId: String): Int { + suspend fun getMangaIdFromChapterId(urlChapterId: String): String { return withIOContext { - val request = GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK) + val request = GET(MdUtil.chapterUrl + urlChapterId) val response = client.newCall(request).await() - ApiMangaParser(lang).chapterParseForMangaId(response) + ApiMangaParser(client, lang).chapterParseForMangaId(response) } } suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long): MangaInfo { return withIOContext { - val response = client.newCall(apiRequest(manga)).await() + val response = client.newCall(mangaRequest(manga)).await() val covers = getCovers(manga, forceLatestCovers) - ApiMangaParser(lang).parseToManga(manga, response, covers, sourceId) + ApiMangaParser(client, lang).parseToManga(manga, response, covers, sourceId) } } - fun fetchMangaDetailsObservable(manga: SManga): Observable { - return client.newCall(apiRequest(manga.toMangaInfo())) + fun fetchMangaDetailsObservable(manga: SManga, sourceId: Long): Observable { + return client.newCall(mangaRequest(manga.toMangaInfo())) .asObservableSuccess() .flatMap { response -> runAsObservable({ - getCovers(manga.toMangaInfo(), forceLatestCovers) - }).map { - response to it - } - } - .flatMap { - ApiMangaParser(lang).parseToManga(manga, it.first, it.second).andThen( - Observable.just( - manga.apply { - initialized = true - } - ) - ) + ApiMangaParser(client, lang).parseToManga(manga.toMangaInfo(), response, emptyList(), sourceId).toSManga() + }) } } fun fetchChapterListObservable(manga: SManga): Observable> { - return client.newCall(apiRequest(manga.toMangaInfo())) + return client.newCall(mangaFeedRequest(manga.toMangaInfo(), 0, lang)) .asObservableSuccess() .map { response -> - ApiMangaParser(lang).chapterListParse(response).map { it.toSChapter() } + val chapterListResponse = response.parseAs(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(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(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 { return withIOContext { - val response = client.newCall(apiRequest(manga)).await() - ApiMangaParser(lang).chapterListParse(response) + val chapterListResponse = client.newCall(mangaFeedRequest(manga, 0, lang)).await().parseAs(MdUtil.jsonParser) + val results = chapterListResponse.results + + var hasMoreResults = chapterListResponse.limit + chapterListResponse.offset under chapterListResponse.total + var lastOffset = chapterListResponse.offset + + while (hasMoreResults) { + val offset = lastOffset + chapterListResponse.limit + val newChapterListResponse = client.newCall(mangaFeedRequest(manga, offset, lang)).await() + .parseAs(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 { - return client.newCall(randomMangaRequest()) - .asObservableSuccess() - .map { response -> - ApiMangaParser(lang).randomMangaIdParse(response) - } + private suspend fun getGroupMap(results: List): Map { + val groupIds = results.map { chapter -> chapter.relationships }.flatten().filter { it.type == "scanlation_group" }.map { it.id }.distinct() + val groupMap = runCatching { + groupIds.chunked(100).mapIndexed { index, ids -> + client.newCall(groupIdRequest(ids, 100 * index)).await() + .parseAs(MdUtil.jsonParser) + .results.map { group -> Pair(group.data.id, group.data.attributes.name) } + }.flatten().toMap() + }.getOrNull() ?: emptyMap() + + return groupMap } suspend fun fetchRandomMangaId(): String { return withIOContext { val response = client.newCall(randomMangaRequest()).await() - ApiMangaParser(lang).randomMangaIdParse(response) + ApiMangaParser(client, lang).randomMangaIdParse(response) } } - suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean): Pair { + suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean, mdList: MdList): Pair { return withIOContext { val metadata = async { - val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt()) + val mangaUrl = "/manga/" + MdUtil.getMangaId(track.tracking_url) val manga = MangaInfo(mangaUrl, track.title) - val response = client.newCall(apiRequest(manga)).await() + val response = client.newCall(mangaRequest(manga)).await() val metadata = MangaDexSearchMetadata() - ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList()) + ApiMangaParser(client, lang).parseIntoMetadata(metadata, response, emptyList()) metadata } - val remoteTrack = async { FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url) } + val remoteTrack = async { + FollowsHandler( + client, + headers, + Injekt.get(), + lang, + useLowQualityCovers, + mdList + ).fetchTrackingInfo(track.tracking_url) + } remoteTrack.await() to metadata.await() } } private fun randomMangaRequest(): Request { - return GET(MdUtil.baseUrl + MdUtil.randMangaPage, cache = CacheControl.FORCE_NETWORK) + return GET(MdUtil.randomMangaUrl, cache = CacheControl.FORCE_NETWORK) } - private fun apiRequest(manga: MangaInfo): Request { - return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK) + private fun mangaRequest(manga: MangaInfo): Request { + return GET(MdUtil.mangaUrl + "/" + MdUtil.getMangaId(manga.key), headers, CacheControl.FORCE_NETWORK) } - private fun coverRequest(manga: MangaInfo): Request { - return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK) + private fun mangaFeedRequest(manga: MangaInfo, offset: Int, lang: String): Request { + return GET(MdUtil.mangaFeedUrl(MdUtil.getMangaId(manga.key), offset, lang), headers, CacheControl.FORCE_NETWORK) + } + + private fun groupIdRequest(id: List, offset: Int): Request { + val urlSuffix = id.joinToString("&ids[]=", "?limit=100&offset=$offset&ids[]=") + return GET(MdUtil.groupUrl + urlSuffix, headers) + } + + /* private fun coverRequest(manga: SManga): Request { + return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK) + }*/ + + companion object { } } diff --git a/app/src/main/java/exh/md/handlers/PageHandler.kt b/app/src/main/java/exh/md/handlers/PageHandler.kt index ff6b43caf..a86873cdc 100644 --- a/app/src/main/java/exh/md/handlers/PageHandler.kt +++ b/app/src/main/java/exh/md/handlers/PageHandler.kt @@ -11,8 +11,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import rx.Observable -// Unused, kept for reference todo -class PageHandler(val client: OkHttpClient, val headers: Headers, private val imageServer: String, val dataSaver: String?) { +class PageHandler(val client: OkHttpClient, val headers: Headers, private val dataSaver: Boolean) { fun fetchPageList(chapter: SChapter): Observable> { if (chapter.scanlator.equals("MangaPlus")) { @@ -26,12 +25,12 @@ class PageHandler(val client: OkHttpClient, val headers: Headers, private val im return client.newCall(pageListRequest(chapter)) .asObservableSuccess() .map { response -> - ApiChapterParser().pageListParse(response) + val host = MdUtil.atHomeUrlHostUrl("${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}", client) + ApiChapterParser().pageListParse(response, host, dataSaver) } } private fun pageListRequest(chapter: SChapter): Request { - val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix) - return GET("${MdUtil.apiUrl}${chpUrl}${MdUtil.apiChapterSuffix}&server=$imageServer&saver=$dataSaver", headers, CacheControl.FORCE_NETWORK) + return GET("${MdUtil.chapterUrl}${MdUtil.getChapterId(chapter.url)}", headers, CacheControl.FORCE_NETWORK) } } diff --git a/app/src/main/java/exh/md/handlers/PopularHandler.kt b/app/src/main/java/exh/md/handlers/PopularHandler.kt index 205c5f27c..b6ae49893 100644 --- a/app/src/main/java/exh/md/handlers/PopularHandler.kt +++ b/app/src/main/java/exh/md/handlers/PopularHandler.kt @@ -2,24 +2,23 @@ package exh.md.handlers import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.source.model.toSManga +import exh.md.handlers.serializers.MangaListResponse import exh.md.utils.MdUtil -import exh.md.utils.setMDUrlWithoutDomain import okhttp3.CacheControl import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import org.jsoup.nodes.Element import rx.Observable -// Unused, kept for reference todo /** * Returns the latest manga from the updates url since it actually respects the users settings */ -class PopularHandler(val client: OkHttpClient, private val headers: Headers, private val useLowQualityCovers: Boolean) { +class PopularHandler(val client: OkHttpClient, private val headers: Headers, private val lang: String, private val useLowQualityCovers: Boolean) { fun fetchPopularManga(page: Int): Observable { return client.newCall(popularMangaRequest(page)) @@ -30,38 +29,20 @@ class PopularHandler(val client: OkHttpClient, private val headers: Headers, pri } private fun popularMangaRequest(page: Int): Request { - return GET("${MdUtil.baseUrl}/updates/$page/", headers, CacheControl.FORCE_NETWORK) + val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder() + + tempUrl.apply { + addQueryParameter("limit", MdUtil.mangaLimit.toString()) + addQueryParameter("offset", (MdUtil.getMangaListOffset(page))) + } + + return GET(tempUrl.build().toString(), headers, CacheControl.FORCE_NETWORK) } private fun popularMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val mangas = document.select(popularMangaSelector).map { element -> - popularMangaFromElement(element) - }.distinctBy { it.url } - - val hasNextPage = popularMangaNextPageSelector.let { selector -> - document.select(selector).first() - } != null - - return MangasPage(mangas, hasNextPage) - } - - private fun popularMangaFromElement(element: Element): SManga { - val manga = SManga.create() - element.select("a.manga_title").first().let { - val url = MdUtil.modifyMangaUrl(it.attr("href")) - manga.setMDUrlWithoutDomain(url) - manga.title = it.text().trim() - } - - manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, useLowQualityCovers) - - return manga - } - - companion object { - const val popularMangaSelector = "tr a.manga_title" - const val popularMangaNextPageSelector = ".pagination li:not(.disabled) span[title*=last page]:not(disabled)" + val mlResponse = response.parseAs(MdUtil.jsonParser) + val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total + val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() } + return MangasPage(mangaList, hasMoreResults) } } diff --git a/app/src/main/java/exh/md/handlers/SearchHandler.kt b/app/src/main/java/exh/md/handlers/SearchHandler.kt index e2a5a16d9..6fc6bd487 100644 --- a/app/src/main/java/exh/md/handlers/SearchHandler.kt +++ b/app/src/main/java/exh/md/handlers/SearchHandler.kt @@ -2,198 +2,72 @@ package exh.md.handlers import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.asObservableSuccess +import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage -import eu.kanade.tachiyomi.source.model.SManga -import eu.kanade.tachiyomi.util.asJsoup +import eu.kanade.tachiyomi.source.model.toSManga +import eu.kanade.tachiyomi.util.lang.runAsObservable +import exh.md.handlers.serializers.MangaListResponse +import exh.md.handlers.serializers.MangaResponse import exh.md.utils.MdUtil -import exh.md.utils.setMDUrlWithoutDomain import okhttp3.CacheControl import okhttp3.Headers import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import org.jsoup.nodes.Element import rx.Observable -// Unused, kept for reference todo -class SearchHandler(val client: OkHttpClient, private val headers: Headers, val lang: String, private val useLowQualityCovers: Boolean) { +class SearchHandler(val client: OkHttpClient, private val headers: Headers, val lang: String, val filterHandler: FilterHandler, private val useLowQualityCovers: Boolean) { - fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable { - return when { - query.startsWith(PREFIX_ID_SEARCH) -> { - val realQuery = query.removePrefix(PREFIX_ID_SEARCH) - client.newCall(searchMangaByIdRequest(realQuery)) - .asObservableSuccess() - .map { response -> - val details = SManga.create() - details.url = "/manga/$realQuery/" - ApiMangaParser(lang).parseToManga(details, response, emptyList()).await() + fun fetchSearchManga(page: Int, query: String, filters: FilterList, sourceId: Long): Observable { + return if (query.startsWith(PREFIX_ID_SEARCH)) { + val realQuery = query.removePrefix(PREFIX_ID_SEARCH) + client.newCall(searchMangaByIdRequest(realQuery)) + .asObservableSuccess() + .flatMap { response -> + runAsObservable({ + val mangaResponse = response.parseAs(MdUtil.jsonParser) + val details = ApiMangaParser(client, lang) + .parseToManga(MdUtil.createMangaEntry(mangaResponse, lang, useLowQualityCovers), response, emptyList(), sourceId).toSManga() MangasPage(listOf(details), false) - } - } - query.startsWith(PREFIX_GROUP_SEARCH) -> { - val realQuery = query.removePrefix(PREFIX_GROUP_SEARCH) - client.newCall(searchMangaByGroupRequest(realQuery)) - .asObservableSuccess() - .map { response -> - response.asJsoup().select(groupSelector).firstOrNull()?.attr("abs:href") - ?.let { - searchMangaParse(client.newCall(GET("$it/manga/0", headers)).execute()) - } - ?: MangasPage(emptyList(), false) - } - } - else -> { - client.newCall(searchMangaRequest(page, query, filters)) - .asObservableSuccess() - .map { response -> - searchMangaParse(response) - } - } + }) + } + } else { + client.newCall(searchMangaRequest(page, query, filters)) + .asObservableSuccess() + .map { response -> + searchMangaParse(response) + } } } private fun searchMangaParse(response: Response): MangasPage { - val document = response.asJsoup() - - val mangas = document.select(searchMangaSelector).map { element -> - searchMangaFromElement(element) - } - - val hasNextPage = searchMangaNextPageSelector.let { selector -> - document.select(selector).first() - } != null - - return MangasPage(mangas, hasNextPage) + val mlResponse = response.parseAs(MdUtil.jsonParser) + val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total + val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() } + return MangasPage(mangaList, hasMoreResults) } private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { - val tags = mutableListOf() - val statuses = mutableListOf() - val demographics = mutableListOf() + val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder() - // Do traditional search - val url = "${MdUtil.baseUrl}/?page=search".toHttpUrl().newBuilder() - .addQueryParameter("p", page.toString()) - .addQueryParameter("title", query.replace(WHITESPACE_REGEX, " ")) - - filters.forEach { filter -> - when (filter) { - is FilterHandler.TextField -> url.addQueryParameter(filter.key, filter.state) - is FilterHandler.DemographicList -> { - filter.state.forEach { demographic -> - if (demographic.state) { - demographics.add(demographic.id) - } - } - } - 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() - ) - } - } - } + tempUrl.apply { + addQueryParameter("limit", MdUtil.mangaLimit.toString()) + addQueryParameter("offset", (MdUtil.getMangaListOffset(page))) + val actualQuery = query.replace(WHITESPACE_REGEX, " ") + if (actualQuery.isNotBlank()) { + addQueryParameter("title", actualQuery) } } - // Manually append genres list to avoid commas being encoded - var urlToUse = url.toString() - if (demographics.isNotEmpty()) { - urlToUse += "&demos=" + demographics.joinToString(",") - } - if (statuses.isNotEmpty()) { - urlToUse += "&statuses=" + statuses.joinToString(",") - } - if (tags.isNotEmpty()) { - urlToUse += "&tags=" + tags.joinToString(",") - } - return GET(urlToUse, headers, CacheControl.FORCE_NETWORK) - } + val finalUrl = filterHandler.addFiltersToUrl(tempUrl, filters) - private fun searchMangaFromElement(element: Element): SManga { - val manga = SManga.create() - element.select("a.manga_title").first().let { - val url = MdUtil.modifyMangaUrl(it.attr("href")) - manga.setMDUrlWithoutDomain(url) - manga.title = it.text().trim() - } - - manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, useLowQualityCovers) - - return manga + return GET(finalUrl, headers, CacheControl.FORCE_NETWORK) } private fun searchMangaByIdRequest(id: String): Request { - return GET(MdUtil.apiUrl + MdUtil.apiManga + id + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK) + return GET(MdUtil.mangaUrl + "/" + id, headers, CacheControl.FORCE_NETWORK) } private fun searchMangaByGroupRequest(group: String): Request { @@ -204,9 +78,5 @@ class SearchHandler(val client: OkHttpClient, private val headers: Headers, val const val PREFIX_ID_SEARCH = "id:" const val PREFIX_GROUP_SEARCH = "group:" val WHITESPACE_REGEX = "\\s".toRegex() - const val searchMangaNextPageSelector = - ".pagination li:not(.disabled) span[title*=last page]:not(disabled)" - const val searchMangaSelector = "div.manga-entry" - const val groupSelector = ".table > tbody:nth-child(2) > tr:nth-child(1) > td:nth-child(2) > a" } } diff --git a/app/src/main/java/exh/md/handlers/SimilarHandler.kt b/app/src/main/java/exh/md/handlers/SimilarHandler.kt index f166d822b..35986a711 100644 --- a/app/src/main/java/exh/md/handlers/SimilarHandler.kt +++ b/app/src/main/java/exh/md/handlers/SimilarHandler.kt @@ -5,36 +5,32 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.model.SManga -import exh.md.similar.sql.models.MangaSimilar import exh.md.similar.sql.models.MangaSimilarImpl import exh.md.utils.MdUtil -import rx.Observable +import exh.util.executeOnIO import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SimilarHandler(val preferences: PreferencesHelper, private val useLowQualityCovers: Boolean) { - /* + /** * fetch our similar mangas */ - fun fetchSimilar(manga: Manga): Observable { + suspend fun fetchSimilar(manga: Manga): MangasPage { // Parse the Mangadex id from the URL - return Observable.just(MdUtil.getMangaId(manga.url).toLong()) - .flatMap { mangaId -> - Injekt.get().getSimilar(mangaId).asRxObservable() - }.map { similarMangaDb: MangaSimilar? -> - if (similarMangaDb != null) { - val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER) - val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER) - val similarMangas = similarMangaIds.mapIndexed { index, similarId -> - SManga.create().apply { - title = similarMangaTitles[index] - url = "/manga/$similarId/" - thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers) - } - } - MangasPage(similarMangas, false) - } else MangasPage(mutableListOf(), false) + val mangaId = MdUtil.getMangaId(manga.url).toLong() + val similarMangaDb = Injekt.get().getSimilar(mangaId).executeOnIO() + return if (similarMangaDb != null) { + val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER) + val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER) + val similarMangas = similarMangaIds.mapIndexed { index, similarId -> + SManga.create().apply { + title = similarMangaTitles[index] + url = "/manga/$similarId/" + thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers) + } } + MangasPage(similarMangas, false) + } else MangasPage(mutableListOf(), false) } } diff --git a/app/src/main/java/exh/md/handlers/serializers/ApiChapterSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ApiChapterSerializer.kt deleted file mode 100644 index cf1579d54..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/ApiChapterSerializer.kt +++ /dev/null @@ -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, - val server: String, - val mangaId: Int -) diff --git a/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt deleted file mode 100644 index 35368630e..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/ApiMangaSerializer.kt +++ /dev/null @@ -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, - val groups: List, - -) - -@Serializable -data class MangaSerializer( - val artist: List, - val author: List, - val mainCover: String, - val description: String, - val tags: List, - 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, - val timestamp: Long -) - -@Serializable -data class GroupSerializer( - val id: Long, - val name: String? = null -) diff --git a/app/src/main/java/exh/md/handlers/serializers/Auth.kt b/app/src/main/java/exh/md/handlers/serializers/Auth.kt new file mode 100644 index 000000000..c4807b509 --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/Auth.kt @@ -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) diff --git a/app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt new file mode 100644 index 000000000..685f83ad8 --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/CacheApiMangaSerializer.kt @@ -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, + val content: List, + val format: List, + val genre: List, + val theme: List, + val languages: List, + val related: List, + val external: MutableMap, + val last_updated: String, + val matches: List, +) + +@Serializable +data class CacheRelatedSerializer( + val id: Long, + val title: String, + val type: String, + val r18: Boolean, +) + +@Serializable +data class CacheSimilarMatchesSerializer( + val id: Long, + val title: String, + val score: Float, + val r18: Boolean, + val languages: List, +) diff --git a/app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt new file mode 100644 index 000000000..80a5fd14a --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/ChapterSerializer.kt @@ -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 +) + +@Serializable +data class ChapterResponse( + val result: String, + val data: NetworkChapter, + val relationships: List +) + +@Serializable +data class NetworkChapter( + val id: String, + val type: String, + val attributes: ChapterAttributes, +) + +@Serializable +data class ChapterAttributes( + val title: String?, + val volume: Int?, + val chapter: String?, + val translatedLanguage: String, + val publishAt: String, + val data: List, + val dataSaver: List, + val hash: String, +) + +@Serializable +data class AtHomeResponse( + val baseUrl: String +) + +@Serializable +data class GroupListResponse( + val limit: Int, + val offset: Int, + val total: Int, + val results: List +) + +@Serializable +data class GroupResponse( + val result: String, + val data: GroupData, +) + +@Serializable +data class GroupData( + val id: String, + val attributes: GroupAttributes, +) + +@Serializable +data class GroupAttributes( + val name: String, +) diff --git a/app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt deleted file mode 100644 index 7db2edc64..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/CoversSerializer.kt +++ /dev/null @@ -1,14 +0,0 @@ -package exh.md.handlers.serializers - -import kotlinx.serialization.Serializable - -@Serializable -data class ApiCovers( - val data: List, -) - -@Serializable -data class CoversResult( - val volume: String, - val url: String -) diff --git a/app/src/main/java/exh/md/handlers/serializers/FollowsPageSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/FollowsPageSerializer.kt deleted file mode 100644 index 6c4509c33..000000000 --- a/app/src/main/java/exh/md/handlers/serializers/FollowsPageSerializer.kt +++ /dev/null @@ -1,24 +0,0 @@ -package exh.md.handlers.serializers - -import kotlinx.serialization.Serializable - -@Serializable -data class FollowsPageSerializer( - val code: Int, - val data: List? -) - -@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 -) diff --git a/app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt new file mode 100644 index 000000000..4ce0f1cae --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/ImageReportSerializer.kt @@ -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? +) diff --git a/app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt index c9bd43df1..ca5d8533e 100644 --- a/app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt +++ b/app/src/main/java/exh/md/handlers/serializers/MangaPlusSerializer.kt @@ -56,7 +56,6 @@ data class TitleDetailView( @ProtoNumber(5) val nextTimeStamp: Int = 0, @ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY, @ProtoNumber(7) val viewingPeriodDescription: String = "", - @ProtoNumber(8) val nonAppearanceInfo: String = "", @ProtoNumber(9) val firstChapterList: List = emptyList(), @ProtoNumber(10) val lastChapterList: List = emptyList(), @ProtoNumber(14) val isSimulReleased: Boolean = true, diff --git a/app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt b/app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt new file mode 100644 index 000000000..fe9840042 --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/MangaSerializer.kt @@ -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 +) + +@Serializable +data class MangaResponse( + val result: String, + val data: NetworkManga, + val relationships: List +) + +@Serializable +data class NetworkManga(val id: String, val type: String, val attributes: NetworkMangaAttributes) + +@Serializable +data class NetworkMangaAttributes( + val title: Map, + val altTitles: List>, + val description: Map, + val links: Map?, + val originalLanguage: String, + val lastVolume: Int?, + val lastChapter: String, + val contentRating: String?, + val publicationDemographic: String?, + val status: String?, + val year: Int?, + val tags: List + // val readingStatus: String? = null, +) + +@Serializable +data class TagsSerializer( + val id: String, + val attributes: TagAttributes +) + +@Serializable +data class TagAttributes( + val name: Map +) + +@Serializable +data class Relationships( + val id: String, + val type: String, +) + +@Serializable +data class AuthorResponseList( + val results: List, +) + +@Serializable +data class AuthorResponse( + val result: String, + val data: NetworkAuthor, +) + +@Serializable +data class NetworkAuthor( + val id: String, + val attributes: AuthorAttributes, +) + +@Serializable +data class AuthorAttributes( + val name: String, +) + +@Serializable +data class UpdateReadingStatus( + val id: String? +) diff --git a/app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt b/app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt new file mode 100644 index 000000000..222b6f6d7 --- /dev/null +++ b/app/src/main/java/exh/md/handlers/serializers/NetworkFollowed.kt @@ -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? = null +) + +@Serializable +data class FollowedSerializer(val mangaId: String, val mangaTitle: String, val followType: Int) diff --git a/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt b/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt new file mode 100644 index 000000000..02018d163 --- /dev/null +++ b/app/src/main/java/exh/md/network/MangaDexLoginHelper.kt @@ -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(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(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(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(MdUtil.jsonParser) + return body.result == "ok" + } +} diff --git a/app/src/main/java/exh/md/network/NoSessionException.kt b/app/src/main/java/exh/md/network/NoSessionException.kt new file mode 100644 index 000000000..fad50935b --- /dev/null +++ b/app/src/main/java/exh/md/network/NoSessionException.kt @@ -0,0 +1,3 @@ +package exh.md.network + +class NoSessionException : IllegalArgumentException("Session token does not exist") diff --git a/app/src/main/java/exh/md/network/TokenAuthenticator.kt b/app/src/main/java/exh/md/network/TokenAuthenticator.kt new file mode 100644 index 000000000..004fd0e1c --- /dev/null +++ b/app/src/main/java/exh/md/network/TokenAuthenticator.kt @@ -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 -> "" + } + } +} diff --git a/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt index 190d2e0bf..372c151e3 100644 --- a/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt +++ b/app/src/main/java/exh/md/similar/ui/MangaDexSimilarPager.kt @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage import eu.kanade.tachiyomi.source.online.all.MangaDex import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException import eu.kanade.tachiyomi.ui.browse.source.browse.Pager +import eu.kanade.tachiyomi.util.lang.runAsObservable import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers @@ -15,7 +16,7 @@ import rx.schedulers.Schedulers class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() { override fun requestNext(): Observable { - return source.fetchMangaSimilar(manga) + return runAsObservable({ source.fetchMangaSimilar(manga) }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnNext { diff --git a/app/src/main/java/exh/md/utils/FollowStatus.kt b/app/src/main/java/exh/md/utils/FollowStatus.kt index a6d2be3d0..41c89a221 100644 --- a/app/src/main/java/exh/md/utils/FollowStatus.kt +++ b/app/src/main/java/exh/md/utils/FollowStatus.kt @@ -1,5 +1,7 @@ package exh.md.utils +import java.util.Locale + enum class FollowStatus(val int: Int) { UNFOLLOWED(0), READING(1), @@ -10,6 +12,7 @@ enum class FollowStatus(val int: Int) { RE_READING(6); companion object { - fun fromInt(value: Int): FollowStatus = values().find { it.int == value } ?: UNFOLLOWED + fun fromDex(value: String?): FollowStatus = values().firstOrNull { it.name.toLowerCase(Locale.US) == value } ?: UNFOLLOWED + fun fromInt(value: Int): FollowStatus = values().firstOrNull { it.int == value } ?: UNFOLLOWED } } diff --git a/app/src/main/java/exh/md/utils/MdLang.kt b/app/src/main/java/exh/md/utils/MdLang.kt index 3e6c768f1..854d1aa6a 100644 --- a/app/src/main/java/exh/md/utils/MdLang.kt +++ b/app/src/main/java/exh/md/utils/MdLang.kt @@ -1,45 +1,58 @@ package exh.md.utils -enum class MdLang(val lang: String, val dexLang: String, val langId: Int) { - English("en", "gb", 1), - Japanese("ja", "jp", 2), - Polish("pl", "pl", 3), - SerboCroatian("sh", "rs", 4), - Dutch("nl", "nl", 5), - Italian("it", "it", 6), - Russian("ru", "ru", 7), - German("de", "de", 8), - Hungarian("hu", "hu", 9), - French("fr", "fr", 10), - Finnish("fi", "fi", 11), - Vietnamese("vi", "vn", 12), - Greek("el", "gr", 13), - Bulgarian("bg", "bg", 14), - Spanish("es", "es", 15), - PortugeseBrazilian("pt-BR", "br", 16), - Portuguese("pt", "pt", 17), - Swedish("sv", "se", 18), - Arabic("ar", "sa", 19), - Danish("da", "dk", 20), - ChineseSimplifed("zh-Hans", "cn", 21), - Bengali("bn", "bd", 22), - Romanian("ro", "ro", 23), - Czech("cs", "cz", 24), - Mongolian("mn", "mn", 25), - Turkish("tr", "tr", 26), - Indonesian("id", "id", 27), - Korean("ko", "kr", 28), - SpanishLTAM("es-419", "mx", 29), - Persian("fa", "ir", 30), - Malay("ms", "my", 31), - Thai("th", "th", 32), - Catalan("ca", "ct", 33), - Filipino("fil", "ph", 34), - ChineseTraditional("zh-Hant", "hk", 35), - Ukrainian("uk", "ua", 36), - Burmese("my", "mm", 37), - Lithuanian("lt", "il", 38), - Hebrew("he", "il", 39), - Hindi("hi", "in", 40), - Norwegian("no", "no", 42) +enum class MdLang(val lang: String, val prettyPrint: String, val extLang: String = lang) { + ENGLISH("en", "English"), + JAPANESE("jp", "Japanese", "ja"), + POLISH("pl", "Polish"), + SERBO_CROATIAN("rs", "Serbo-Croatian", "sh"), + DUTCH("nl", "Dutch"), + ITALIAN("it", "IT"), + RUSSIAN("ru", "Russian"), + GERMAN("de", "German"), + HUNGARIAN("hu", "Hungarian"), + FRENCH("fr", "French"), + FINNISH("fi", "Finnish"), + VIETNAMESE("vn", "Vietnamese", "vi"), + GREEK("gr", "Greek", "el"), + BULGARIAN("bg", "BULGARIN"), + SPANISH_ES("es", "Spanish (Es)"), + PORTUGUESE_BR("br", "Portuguese (Br)", "pt-br"), + PORTUGUESE("pt", "Portuguese (Pt)"), + SWEDISH("se", "Swedish", "sv"), + ARABIC("sa", "Arabic", "ar"), + DANISH("dk", "Danish", "da"), + CHINESE_SIMPLIFIED("cn", "Chinese (Simp)", "zh"), + BENGALI("bd", "Bengali", "bn"), + ROMANIAN("ro", "Romanian"), + CZECH("cz", "Czech", "cs"), + MONGOLIAN("mn", "Mongolian"), + TURKISH("tr", "Turkish"), + INDONESIAN("id", "Indonesian"), + KOREAN("kr", "Korean", "ko"), + SPANISH_LATAM("mx", "Spanish (LATAM)", "es-la"), + PERSIAN("ir", "Persian", "fa"), + MALAY("my", "Malay", "ms"), + THAI("th", "Thai"), + CATALAN("ct", "Catalan", "ca"), + FILIPINO("ph", "Filipino", "fi"), + CHINESE_TRAD("hk", "Chinese (Trad)", "zh-hk"), + UKRAINIAN("ua", "Ukrainian", "uk"), + BURMESE("mm", "Burmese", "my"), + LINTHUANIAN("lt", "Lithuanian"), + HEBREW("il", "Hebrew", "he"), + HINDI("in", "Hindi", "hi"), + NORWEGIAN("no", "Norwegian") + ; + + companion object { + fun fromIsoCode(isoCode: String): MdLang? = + values().firstOrNull { + it.lang == isoCode + } + + fun fromExt(extLang: String): MdLang? = + values().firstOrNull { + it.extLang == extLang + } + } } diff --git a/app/src/main/java/exh/md/utils/MdUtil.kt b/app/src/main/java/exh/md/utils/MdUtil.kt index ae0d1337d..3389f97e0 100644 --- a/app/src/main/java/exh/md/utils/MdUtil.kt +++ b/app/src/main/java/exh/md/utils/MdUtil.kt @@ -1,49 +1,89 @@ package exh.md.utils -import androidx.core.net.toUri -import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.mdlist.MdList +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.online.all.MangaDex +import exh.log.xLogD +import exh.md.handlers.serializers.AtHomeResponse +import exh.md.handlers.serializers.LoginBodyToken +import exh.md.handlers.serializers.MangaResponse +import exh.md.network.NoSessionException import exh.source.getMainSource import exh.util.floor +import exh.util.nullIfBlank import exh.util.nullIfZero +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json +import okhttp3.Headers +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient import org.jsoup.parser.Parser +import tachiyomi.source.model.MangaInfo import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.net.URISyntaxException +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone -@Suppress("unused") class MdUtil { companion object { const val cdnUrl = "https://mangadex.org" // "https://s0.mangadex.org" const val baseUrl = "https://mangadex.org" - const val randMangaPage = "/manga/" const val apiUrl = "https://api.mangadex.org" - const val apiManga = "/v2/manga/" - const val includeChapters = "?include=chapters" - const val oldApiChapter = "/api/chapter/" - const val newApiChapter = "/v2/chapter/" - const val apiChapterSuffix = "?mark_read=0" + const val apiUrlCdnCache = "https://cdn.statically.io/gh/goldbattle/MangadexRecomendations/master/output/api/" + const val apiUrlCache = "https://raw.githubusercontent.com/goldbattle/MangadexRecomendations/master/output/api/" + const val imageUrlCacheNotFound = "https://cdn.statically.io/img/raw.githubusercontent.com/CarlosEsco/Neko/master/.github/manga_cover_not_found.png" + const val atHomeUrl = "$apiUrl/at-home/server" + const val chapterUrl = "$apiUrl/chapter/" + const val chapterSuffix = "/chapter/" + const val checkTokenUrl = "$apiUrl/auth/check" + const val refreshTokenUrl = "$apiUrl/auth/refresh" + const val loginUrl = "$apiUrl/auth/login" + const val logoutUrl = "$apiUrl/auth/logout" + const val groupUrl = "$apiUrl/group" + const val authorUrl = "$apiUrl/author" + const val randomMangaUrl = "$apiUrl/manga/random" + const val mangaUrl = "$apiUrl/manga" + const val mangaStatus = "$apiUrl/manga/status" + const val userFollows = "$apiUrl/user/follows/manga" + fun updateReadingStatusUrl(id: String) = "$apiUrl/manga/$id/status" + + fun mangaFeedUrl(id: String, offset: Int, language: String): String { + return "$mangaUrl/$id/feed".toHttpUrl().newBuilder().apply { + addQueryParameter("limit", "500") + addQueryParameter("offset", offset.toString()) + addQueryParameter("locales[]", language) + }.build().toString() + } + const val groupSearchUrl = "$baseUrl/groups/0/1/" - const val followsAllApi = "/v2/user/me/followed-manga" - const val isLoggedInApi = "/v2/user/me" - const val followsMangaApi = "/v2/user/me/manga/" const val apiCovers = "/covers" const val reportUrl = "https://api.mangadex.network/report" - const val imageUrl = "$baseUrl/data" - val jsonParser = Json { - isLenient = true - ignoreUnknownKeys = true - allowSpecialFloatingPointValues = true - useArrayPolymorphism = true - prettyPrint = true - } + const val mdAtHomeTokenLifespan = 10 * 60 * 1000 + const val mangaLimit = 25 + + /** + * Get the manga offset pages are 1 based, so subtract 1 + */ + fun getMangaListOffset(page: Int): String = (mangaLimit * (page - 1)).toString() + + val jsonParser = + Json { + isLenient = true + ignoreUnknownKeys = true + allowSpecialFloatingPointValues = true + useArrayPolymorphism = true + prettyPrint = true + } private const val scanlatorSeparator = " & " @@ -164,24 +204,9 @@ class MdUtil { } // Get the ID from the manga url - fun getMangaId(url: String): String { - val lastSection = url.trimEnd('/').substringAfterLast("/") - return if (lastSection.toIntOrNull() != null) { - lastSection - } else { - // this occurs if person has manga from before that had the id/name/ - url.trimEnd('/').substringBeforeLast("/").substringAfterLast("/") - } - } + fun getMangaId(url: String): String = url.trimEnd('/').substringAfterLast("/") - fun getChapterId(url: String) = url.substringBeforeLast(apiChapterSuffix).substringAfterLast("/") - - // creates the manga url from the browse for the api - fun modifyMangaUrl(url: String): String = - url.replace("/title/", "/manga/").substringBeforeLast("/") + "/" - - // Removes the ?timestamp from image urls - fun removeTimeParamUrl(url: String): String = url.substringBeforeLast("?") + fun getChapterId(url: String) = url.substringAfterLast("/") fun cleanString(string: String): String { var cleanedString = string @@ -222,8 +247,8 @@ class MdUtil { return baseUrl + attr } - fun getScanlators(scanlators: String): List { - if (scanlators.isBlank()) return emptyList() + fun getScanlators(scanlators: String?): List { + if (scanlators.isNullOrBlank()) return emptyList() return scanlators.split(scanlatorSeparator).distinct() } @@ -234,7 +259,6 @@ class MdUtil { fun getMissingChapterCount(chapters: List, mangaStatus: Int): String? { if (mangaStatus == SManga.COMPLETED) return null - // TODO val remove0ChaptersFromCount = chapters.distinctBy { /*if (it.chapter_txt.isNotEmpty()) { it.vol + it.chapter_txt @@ -257,15 +281,63 @@ class MdUtil { return null } - fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? { + fun atHomeUrlHostUrl(requestUrl: String, client: OkHttpClient): String { + val atHomeRequest = GET(requestUrl) + val atHomeResponse = client.newCall(atHomeRequest).execute() + return atHomeResponse.parseAs(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(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(it) + } catch (e: SerializationException) { + xLogD("Unable to load session token") + null + } + }?.refresh + + fun updateLoginToken(token: LoginBodyToken, preferences: PreferencesHelper, mdList: MdList) { + preferences.trackToken(mdList).set(jsonParser.encodeToString(token)) + } + + fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) = + headers.newBuilder().add("Authorization", "Bearer ${sessionToken(preferences, mdList) ?: throw NoSessionException()}").build() + + fun getEnabledMangaDex(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): MangaDex? { return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs -> - preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()?.let { preferredMangaDexId -> - mangadexs.firstOrNull { it.id == preferredMangaDexId } - } ?: mangadexs.firstOrNull() + preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero() + ?.let { preferredMangaDexId -> + mangadexs.firstOrNull { it.id == preferredMangaDexId } + } + ?: mangadexs.firstOrNull() } } - fun getEnabledMangaDexs(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): List { + fun getEnabledMangaDexs(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): List { val languages = preferences.enabledLanguages().get() val disabledSourceIds = preferences.disabledSources().get() @@ -275,54 +347,5 @@ class MdUtil { .filter { it.lang in languages } .filterNot { it.id.toString() in disabledSourceIds } } - - fun mapMdIdToMangaUrl(id: Int) = "/manga/$id/" } } - -/** - * Assigns the url of the chapter without the scheme and domain. It saves some redundancy from - * database and the urls could still work after a domain change. - * - * @param url the full url to the chapter. - */ -fun SChapter.setMDUrlWithoutDomain(url: String) { - this.url = getMDUrlWithoutDomain(url) -} - -/** - * Assigns the url of the manga without the scheme and domain. It saves some redundancy from - * database and the urls could still work after a domain change. - * - * @param url the full url to the manga. - */ -fun SManga.setMDUrlWithoutDomain(url: String) { - this.url = getMDUrlWithoutDomain(url) -} - -/** - * Returns the url of the given string without the scheme and domain. - * - * @param orig the full url. - */ -private fun getMDUrlWithoutDomain(orig: String): String { - return try { - val uri = orig.toUri() - var out = uri.path.orEmpty() - if (uri.query != null) { - out += "?" + uri.query - } - if (uri.fragment != null) { - out += "#" + uri.fragment - } - out - } catch (e: URISyntaxException) { - orig - } -} - -fun Chapter.scanlatorList(): List { - return this.scanlator?.let { - MdUtil.getScanlators(it) - } ?: listOf("No scanlator") -} diff --git a/app/src/main/java/exh/metadata/metadata/MangaDexSearchMetadata.kt b/app/src/main/java/exh/metadata/metadata/MangaDexSearchMetadata.kt index eb00996d1..540e74dff 100644 --- a/app/src/main/java/exh/metadata/metadata/MangaDexSearchMetadata.kt +++ b/app/src/main/java/exh/metadata/metadata/MangaDexSearchMetadata.kt @@ -1,65 +1,56 @@ package exh.metadata.metadata import android.content.Context -import androidx.core.net.toUri import eu.kanade.tachiyomi.R +import exh.md.utils.MdUtil import exh.metadata.metadata.base.RaisedSearchMetadata +import exh.util.nullIfBlank import kotlinx.serialization.Serializable import tachiyomi.source.model.MangaInfo @Serializable class MangaDexSearchMetadata : RaisedSearchMetadata() { - var mdId: String? = null + var mdUuid: String? = null - var mdUrl: String? = null + // var mdUrl: String? = null - var thumbnail_url: String? = null + var cover: String? = null var title: String? by titleDelegate(TITLE_TYPE_MAIN) + var altTitles: List? = null var description: String? = null - var author: String? = null - var artist: String? = null + var authors: List? = null - var lang_flag: String? = null + var langFlag: String? = null - var last_chapter_number: Int? = null - var rating: String? = null - var users: String? = null + var lastChapterNumber: Int? = null + // var rating: String? = null + // var users: String? = null - var anilist_id: String? = null - var kitsu_id: String? = null - var my_anime_list_id: String? = null - var manga_updates_id: String? = null - var anime_planet_id: String? = null + var anilistId: String? = null + var kitsuId: String? = null + var myAnimeListId: String? = null + var mangaUpdatesId: String? = null + var animePlanetId: String? = null var status: Int? = null - var missing_chapters: String? = null + // var missing_chapters: String? = null - var follow_status: Int? = null + var followStatus: Int? = null - var maxChapterNumber: Int? = null + // var maxChapterNumber: Int? = null override fun createMangaInfo(manga: MangaInfo): MangaInfo { - val key = mdUrl?.let { - try { - val uri = it.toUri() - val out = uri.path!!.removePrefix("/api") - out + if (out.endsWith("/")) "" else "/" - } catch (e: Exception) { - it - } - } + val key = mdUuid?.let { "/manga/$it" } val title = title - val cover = thumbnail_url + val cover = cover ?: manga.cover.nullIfBlank() ?: "https://i.imgur.com/6TrIues.jpg" // cover - val author = author - - val artist = artist + val author = authors?.joinToString()?.let { MdUtil.cleanString(it) } val status = status @@ -72,7 +63,6 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() { title = title ?: manga.title, cover = cover ?: manga.cover, author = author ?: manga.author, - artist = artist ?: manga.artist, status = status ?: manga.status, genres = genres, description = description ?: manga.description @@ -81,29 +71,30 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() { override fun getExtraInfoPairs(context: Context): List> { val pairs = mutableListOf>() - mdId?.let { pairs += context.getString(R.string.id) to it } - mdUrl?.let { pairs += context.getString(R.string.url) to it } - thumbnail_url?.let { pairs += context.getString(R.string.thumbnail_url) to it } + mdUuid?.let { pairs += context.getString(R.string.id) to it } + // mdUrl?.let { pairs += context.getString(R.string.url) to it } + cover?.let { pairs += context.getString(R.string.thumbnail_url) to it } title?.let { pairs += context.getString(R.string.title) to it } - author?.let { pairs += context.getString(R.string.author) to it } - artist?.let { pairs += context.getString(R.string.artist) to it } - lang_flag?.let { pairs += context.getString(R.string.language) to it } - last_chapter_number?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() } - rating?.let { pairs += context.getString(R.string.average_rating) to it } - users?.let { pairs += context.getString(R.string.total_ratings) to it } + authors?.let { pairs += context.getString(R.string.author) to it.joinToString() } + // artist?.let { pairs += context.getString(R.string.artist) to it } + langFlag?.let { pairs += context.getString(R.string.language) to it } + lastChapterNumber?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() } + // rating?.let { pairs += context.getString(R.string.average_rating) to it } + // users?.let { pairs += context.getString(R.string.total_ratings) to it } status?.let { pairs += context.getString(R.string.status) to it.toString() } - missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it } - follow_status?.let { pairs += context.getString(R.string.follow_status) to it.toString() } - anilist_id?.let { pairs += context.getString(R.string.anilist_id) to it } - kitsu_id?.let { pairs += context.getString(R.string.kitsu_id) to it } - my_anime_list_id?.let { pairs += context.getString(R.string.mal_id) to it } - manga_updates_id?.let { pairs += context.getString(R.string.manga_updates_id) to it } - anime_planet_id?.let { pairs += context.getString(R.string.anime_planet_id) to it } + // missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it } + followStatus?.let { pairs += context.getString(R.string.follow_status) to it.toString() } + anilistId?.let { pairs += context.getString(R.string.anilist_id) to it } + kitsuId?.let { pairs += context.getString(R.string.kitsu_id) to it } + myAnimeListId?.let { pairs += context.getString(R.string.mal_id) to it } + mangaUpdatesId?.let { pairs += context.getString(R.string.manga_updates_id) to it } + animePlanetId?.let { pairs += context.getString(R.string.anime_planet_id) to it } return pairs } companion object { private const val TITLE_TYPE_MAIN = 0 + private const val TITLE_TYPE_ALT_TITLE = 1 const val TAG_TYPE_DEFAULT = 0 } diff --git a/app/src/main/java/exh/metadata/metadata/base/RaisedSearchMetadata.kt b/app/src/main/java/exh/metadata/metadata/base/RaisedSearchMetadata.kt index b7e648771..6c522c433 100644 --- a/app/src/main/java/exh/metadata/metadata/base/RaisedSearchMetadata.kt +++ b/app/src/main/java/exh/metadata/metadata/base/RaisedSearchMetadata.kt @@ -48,8 +48,6 @@ abstract class RaisedSearchMetadata { @Transient val titles = mutableListOf() - var filteredScanlators: String? = null - fun getTitleOfType(type: Int): String? = titles.find { it.type == type }?.title fun replaceTitleOfType(type: Int, newTitle: String?) { diff --git a/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt b/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt index 251a38a06..599e8edc1 100644 --- a/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt +++ b/app/src/main/java/exh/ui/metadata/adapters/MangaDexDescriptionAdapter.kt @@ -1,20 +1,18 @@ package exh.ui.metadata.adapters -import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.databinding.DescriptionAdapterMdBinding import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.util.system.copyToClipboard -import exh.metadata.MetadataUtil.getRatingString import exh.metadata.bindDrawable import exh.metadata.metadata.MangaDexSearchMetadata import exh.ui.metadata.MetadataViewController -import kotlin.math.round class MangaDexDescriptionAdapter( private val controller: MangaController @@ -40,10 +38,13 @@ class MangaDexDescriptionAdapter( val meta = controller.presenter.meta if (meta == null || meta !is MangaDexSearchMetadata) return - val ratingFloat = meta.rating?.toFloatOrNull() + // todo + /*val ratingFloat = meta.rating?.toFloatOrNull() binding.ratingBar.rating = ratingFloat?.div(2F) ?: 0F @SuppressLint("SetTextI18n") - binding.rating.text = (round((meta.rating?.toFloatOrNull() ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(itemView.context, ratingFloat) + binding.rating.text = (round((meta.rating?.toFloatOrNull() ?: 0F) * 100.0) / 100.0).toString() + " - " + getRatingString(itemView.context, ratingFloat)*/ + binding.rating.isVisible = false + binding.ratingBar.isVisible = false binding.moreInfo.bindDrawable(itemView.context, R.drawable.ic_info_24dp)