Rewrite and enable Mangadex delegation for V5 of Mangadex (Thanks Cesco)
Co-authored-by: CarlosEsco <CarlosEsco@users.noreply.github.com>
This commit is contained in:
parent
8686fecb1f
commit
b9b5ef55ab
@ -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 <--
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFI
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FILTERED_SCANLATORS
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
|
||||
@ -68,7 +69,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
|
||||
COL_VIEWER to obj.viewer_flags,
|
||||
COL_CHAPTER_FLAGS to obj.chapter_flags,
|
||||
COL_COVER_LAST_MODIFIED to obj.cover_last_modified,
|
||||
COL_DATE_ADDED to obj.date_added
|
||||
COL_DATE_ADDED to obj.date_added,
|
||||
COL_FILTERED_SCANLATORS to obj.filtered_scanlators
|
||||
)
|
||||
}
|
||||
|
||||
@ -91,6 +93,7 @@ interface BaseMangaGetResolver {
|
||||
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
|
||||
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
|
||||
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
|
||||
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFilteredScanlatorsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaInfoPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
@ -153,6 +154,13 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaCoverLastModifiedPutResolver())
|
||||
.prepare()
|
||||
|
||||
// SY -->
|
||||
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaFilteredScanlatorsPutResolver())
|
||||
.prepare()
|
||||
// SY <--
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import androidx.core.content.contentValuesOf
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
// [EXH]
|
||||
class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_FILTERED_SCANLATORS} = ?")
|
||||
.whereArgs(manga.filtered_scanlators)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = contentValuesOf(
|
||||
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators
|
||||
)
|
||||
}
|
@ -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"
|
||||
}
|
||||
|
@ -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) ->
|
||||
|
@ -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 -->
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -15,7 +15,7 @@ interface FollowsSource : CatalogueSource {
|
||||
*
|
||||
* @param SManga all smanga found for user
|
||||
*/
|
||||
suspend fun fetchAllFollows(forceHd: Boolean = false): List<Pair<SManga, RaisedSearchMetadata>>
|
||||
suspend fun fetchAllFollows(): List<Pair<SManga, RaisedSearchMetadata>>
|
||||
|
||||
/**
|
||||
* updates the follow status for a manga
|
||||
|
@ -1,17 +1,13 @@
|
||||
package eu.kanade.tachiyomi.source.online.all
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
@ -21,14 +17,10 @@ import eu.kanade.tachiyomi.source.online.FollowsSource
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.source.online.LoginSource
|
||||
import eu.kanade.tachiyomi.source.online.MetadataSource
|
||||
import eu.kanade.tachiyomi.source.online.NamespaceSource
|
||||
import eu.kanade.tachiyomi.source.online.RandomMangaSource
|
||||
import eu.kanade.tachiyomi.source.online.UrlImportableSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.GalleryAddEvent
|
||||
import exh.GalleryAdder
|
||||
import exh.md.MangaDexFabHeaderAdapter
|
||||
import exh.md.handlers.ApiChapterParser
|
||||
import exh.md.handlers.ApiMangaParser
|
||||
@ -36,25 +28,20 @@ import exh.md.handlers.FollowsHandler
|
||||
import exh.md.handlers.MangaHandler
|
||||
import exh.md.handlers.MangaPlusHandler
|
||||
import exh.md.handlers.SimilarHandler
|
||||
import exh.md.network.MangaDexLoginHelper
|
||||
import exh.md.network.NoSessionException
|
||||
import exh.md.network.TokenAuthenticator
|
||||
import exh.md.utils.FollowStatus
|
||||
import exh.md.utils.MdLang
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.source.DelegatedHttpSource
|
||||
import exh.ui.metadata.adapters.MangaDexDescriptionAdapter
|
||||
import exh.util.urlImportFetchSearchManga
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.int
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import okio.EOFException
|
||||
import rx.Observable
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
@ -67,45 +54,53 @@ import kotlin.reflect.KClass
|
||||
class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
DelegatedHttpSource(delegate),
|
||||
MetadataSource<MangaDexSearchMetadata, Response>,
|
||||
UrlImportableSource,
|
||||
// UrlImportableSource,
|
||||
FollowsSource,
|
||||
LoginSource,
|
||||
BrowseSourceFilterHeader,
|
||||
RandomMangaSource {
|
||||
RandomMangaSource,
|
||||
NamespaceSource {
|
||||
override val lang: String = delegate.lang
|
||||
|
||||
override val headers: Headers = super.headers.newBuilder().apply {
|
||||
add("X-Requested-With", "XMLHttpRequest")
|
||||
add("Referer", MdUtil.baseUrl)
|
||||
}.build()
|
||||
|
||||
private val mdLang by lazy {
|
||||
MdLang.values().find { it.lang == lang }?.dexLang ?: lang
|
||||
MdLang.fromExt(lang) ?: MdLang.ENGLISH
|
||||
}
|
||||
|
||||
override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||
// override val matchingHosts: List<String> = listOf("mangadex.org", "www.mangadex.org")
|
||||
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
val trackManager: TrackManager by injectLazy()
|
||||
|
||||
private val sourcePreferences: SharedPreferences by lazy {
|
||||
context.getSharedPreferences("source_$id", 0x0000)
|
||||
val mdList: MdList by lazy {
|
||||
Injekt.get<TrackManager>().mdList
|
||||
}
|
||||
|
||||
private fun useLowQualityThumbnail() = sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
|
||||
/*private val sourcePreferences: SharedPreferences by lazy {
|
||||
context.getSharedPreferences("source_$id", 0x0000)
|
||||
}*/
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
private val loginHelper by lazy {
|
||||
MangaDexLoginHelper(networkHttpClient, preferences, mdList)
|
||||
}
|
||||
|
||||
override val baseHttpClient: OkHttpClient = super.client.newBuilder()
|
||||
.authenticator(
|
||||
TokenAuthenticator(loginHelper)
|
||||
)
|
||||
.build()
|
||||
|
||||
private fun useLowQualityThumbnail() = false // sourcePreferences.getInt(SHOW_THUMBNAIL_PREF, 0) == LOW_QUALITY
|
||||
|
||||
/*override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
urlImportFetchSearchManga(context, query) {
|
||||
importIdToMdId(query) {
|
||||
super.fetchSearchManga(page, query, filters)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
/*override suspend fun mapUrlToMangaUrl(uri: Uri): String? {
|
||||
val lcFirstPathSegment = uri.pathSegments.firstOrNull()?.toLowerCase() ?: return null
|
||||
|
||||
return if (lcFirstPathSegment == "title" || lcFirstPathSegment == "manga") {
|
||||
MdUtil.mapMdIdToMangaUrl(uri.pathSegments[1].toInt())
|
||||
"/manga/" + uri.pathSegments[1]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -119,44 +114,43 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
|
||||
override suspend fun mapChapterUrlToMangaUrl(uri: Uri): String? {
|
||||
val id = uri.pathSegments.getOrNull(2) ?: return null
|
||||
val mangaId = MangaHandler(client, headers, mdLang).getMangaIdFromChapterId(id)
|
||||
val mangaId = MangaHandler(baseHttpClient, headers, mdLang).getMangaIdFromChapterId(id)
|
||||
return MdUtil.mapMdIdToMangaUrl(mangaId)
|
||||
}
|
||||
}*/
|
||||
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga)
|
||||
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).fetchMangaDetailsObservable(manga, id)
|
||||
}
|
||||
|
||||
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
|
||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id)
|
||||
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).getMangaDetails(manga, id)
|
||||
}
|
||||
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
|
||||
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).fetchChapterListObservable(manga)
|
||||
}
|
||||
|
||||
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||
return MangaHandler(client, headers, mdLang, preferences.mangaDexForceLatestCovers().get()).getChapterList(manga)
|
||||
return MangaHandler(baseHttpClient, headers, mdLang.lang, preferences.mangaDexForceLatestCovers().get()).getChapterList(manga)
|
||||
}
|
||||
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return if (chapter.scanlator == "MangaPlus") {
|
||||
client.newCall(mangaPlusPageListRequest(chapter))
|
||||
baseHttpClient.newCall(mangaPlusPageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
val chapterId = ApiChapterParser().externalParse(response)
|
||||
MangaPlusHandler(client).fetchPageList(chapterId)
|
||||
MangaPlusHandler(baseHttpClient).fetchPageList(chapterId)
|
||||
}
|
||||
} else super.fetchPageList(chapter)
|
||||
}
|
||||
|
||||
private fun mangaPlusPageListRequest(chapter: SChapter): Request {
|
||||
val urlChapterId = MdUtil.getChapterId(chapter.url)
|
||||
return GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
|
||||
return GET(MdUtil.chapterUrl + MdUtil.getChapterId(chapter.url), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
override fun fetchImage(page: Page): Observable<Response> {
|
||||
return if (page.imageUrl!!.contains("mangaplus", true)) {
|
||||
return if (page.imageUrl?.contains("mangaplus", true) == true) {
|
||||
MangaPlusHandler(network.client).client.newCall(GET(page.imageUrl!!, headers))
|
||||
.asObservableSuccess()
|
||||
} else super.fetchImage(page)
|
||||
@ -169,28 +163,27 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
|
||||
override fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response) {
|
||||
ApiMangaParser(mdLang).parseIntoMetadata(metadata, input, emptyList())
|
||||
ApiMangaParser(baseHttpClient, mdLang.lang).parseIntoMetadata(metadata, input, emptyList())
|
||||
}
|
||||
|
||||
override suspend fun fetchFollows(): MangasPage {
|
||||
return FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchFollows()
|
||||
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchFollows()
|
||||
}
|
||||
|
||||
override val requiresLogin: Boolean = true
|
||||
|
||||
override val twoFactorAuth = LoginSource.AuthSupport.SUPPORTED
|
||||
override val twoFactorAuth = LoginSource.AuthSupport.NOT_SUPPORTED
|
||||
|
||||
override fun isLogged(): Boolean {
|
||||
val httpUrl = MdUtil.baseUrl.toHttpUrl()
|
||||
return trackManager.mdList.isLogged && network.cookieManager.get(httpUrl).any { it.name == REMEMBER_ME }
|
||||
return mdList.isLogged
|
||||
}
|
||||
|
||||
override fun getUsername(): String {
|
||||
return trackManager.mdList.getUsername()
|
||||
return mdList.getUsername()
|
||||
}
|
||||
|
||||
override fun getPassword(): String {
|
||||
return trackManager.mdList.getPassword()
|
||||
return mdList.getPassword()
|
||||
}
|
||||
|
||||
override suspend fun login(
|
||||
@ -198,96 +191,52 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
password: String,
|
||||
twoFactorCode: String?
|
||||
): Boolean {
|
||||
return withIOContext {
|
||||
val formBody = FormBody.Builder().apply {
|
||||
add("login_username", username)
|
||||
add("login_password", password)
|
||||
add("no_js", "1")
|
||||
add("remember_me", "1")
|
||||
add("two_factor", twoFactorCode ?: "")
|
||||
}
|
||||
|
||||
runCatching {
|
||||
client.newCall(
|
||||
POST(
|
||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=login",
|
||||
headers,
|
||||
formBody.build()
|
||||
)
|
||||
).await().closeQuietly()
|
||||
}
|
||||
|
||||
val response = client.newCall(GET(MdUtil.apiUrl + MdUtil.isLoggedInApi, headers)).await()
|
||||
|
||||
withIOContext { response.body?.string() }.let { jsonData ->
|
||||
if (jsonData != null) {
|
||||
MdUtil.jsonParser.decodeFromString<JsonObject>(jsonData)["code"]?.let { it as? JsonPrimitive }?.int == 200
|
||||
} else {
|
||||
throw Exception("Json data was null")
|
||||
}
|
||||
}.also {
|
||||
preferences.setTrackCredentials(trackManager.mdList, username, password)
|
||||
}
|
||||
}
|
||||
val result = loginHelper.login(username, password)
|
||||
return if (result is MangaDexLoginHelper.LoginResult.Success) {
|
||||
MdUtil.updateLoginToken(result.token, preferences, mdList)
|
||||
mdList.saveCredentials(username, password)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
||||
override suspend fun logout(): Boolean {
|
||||
return withIOContext {
|
||||
// https://mangadex.org/ajax/actions.ajax.php?function=logout
|
||||
val httpUrl = MdUtil.baseUrl.toHttpUrl()
|
||||
val listOfDexCookies = network.cookieManager.get(httpUrl)
|
||||
val cookie = listOfDexCookies.find { it.name == REMEMBER_ME }
|
||||
val token = cookie?.value
|
||||
if (token.isNullOrEmpty()) {
|
||||
return@withIOContext true
|
||||
}
|
||||
try {
|
||||
val result = client.newCall(
|
||||
POST("${MdUtil.baseUrl}/ajax/actions.ajax.php?function=logout", headers).newBuilder().addHeader(REMEMBER_ME, token).build()
|
||||
).await()
|
||||
val resultStr = withIOContext { result.body?.string() }
|
||||
if (resultStr?.contains("success", true) == true) {
|
||||
network.cookieManager.remove(httpUrl)
|
||||
trackManager.mdList.logout()
|
||||
return@withIOContext true
|
||||
}
|
||||
} catch (e: EOFException) {
|
||||
network.cookieManager.remove(httpUrl)
|
||||
trackManager.mdList.logout()
|
||||
return@withIOContext true
|
||||
}
|
||||
|
||||
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<Pair<SManga, MangaDexSearchMetadata>> {
|
||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchAllFollows(forceHd) }
|
||||
override suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
|
||||
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).fetchAllFollows()
|
||||
}
|
||||
|
||||
suspend fun updateReadingProgress(track: Track): Boolean {
|
||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateReadingProgress(track) }
|
||||
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateReadingProgress(track)
|
||||
}
|
||||
|
||||
suspend fun updateRating(track: Track): Boolean {
|
||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateRating(track) }
|
||||
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateRating(track)
|
||||
}
|
||||
|
||||
override suspend fun fetchTrackingInfo(url: String): Track {
|
||||
return withIOContext {
|
||||
if (!isLogged()) {
|
||||
throw Exception("Not Logged in")
|
||||
}
|
||||
FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).fetchTrackingInfo(url)
|
||||
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<Track, MangaDexSearchMetadata> {
|
||||
return MangaHandler(client, headers, mdLang).getTrackingInfo(track, useLowQualityThumbnail())
|
||||
return MangaHandler(baseHttpClient, headers, mdLang.lang).getTrackingInfo(track, useLowQualityThumbnail(), mdList)
|
||||
}
|
||||
|
||||
override suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||
return withIOContext { FollowsHandler(client, headers, Injekt.get(), useLowQualityThumbnail()).updateFollowStatus(mangaID, followStatus) }
|
||||
return FollowsHandler(baseHttpClient, headers, Injekt.get(), mdLang.lang, useLowQualityThumbnail(), mdList).updateFollowStatus(mangaID, followStatus)
|
||||
}
|
||||
|
||||
override fun getFilterHeader(controller: BaseController<*>): MangaDexFabHeaderAdapter {
|
||||
@ -295,14 +244,14 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
|
||||
override suspend fun fetchRandomMangaUrl(): String {
|
||||
return withIOContext { MangaHandler(client, headers, mdLang).fetchRandomMangaId() }
|
||||
return MangaHandler(baseHttpClient, headers, mdLang.lang).fetchRandomMangaId()
|
||||
}
|
||||
|
||||
fun fetchMangaSimilar(manga: Manga): Observable<MangasPage> {
|
||||
suspend fun fetchMangaSimilar(manga: Manga): MangasPage {
|
||||
return SimilarHandler(preferences, useLowQualityThumbnail()).fetchSimilar(manga)
|
||||
}
|
||||
|
||||
private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
||||
/*private fun importIdToMdId(query: String, fail: () -> Observable<MangasPage>): Observable<MangasPage> =
|
||||
when {
|
||||
query.toIntOrNull() != null -> {
|
||||
runAsObservable({
|
||||
@ -320,11 +269,11 @@ class MangaDex(delegate: HttpSource, val context: Context) :
|
||||
}
|
||||
}
|
||||
else -> fail()
|
||||
}
|
||||
}*/
|
||||
|
||||
companion object {
|
||||
/*companion object {
|
||||
private const val REMEMBER_ME = "mangadex_rememberme_token"
|
||||
private const val SHOW_THUMBNAIL_PREF = "showThumbnailDefault"
|
||||
private const val LOW_QUALITY = 1
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<String>) {
|
||||
val meta = meta ?: return
|
||||
meta.filteredScanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators)
|
||||
meta.flatten().let {
|
||||
db.insertFlatMetadataAsync(it).await()
|
||||
}
|
||||
fun setScanlatorFilter(filteredScanlators: Set<String>) {
|
||||
val manga = manga
|
||||
manga.filtered_scanlators = if (filteredScanlators.size == allChapterScanlators.size) null else MdUtil.getScanlatorString(filteredScanlators)
|
||||
db.updateMangaFilteredScanlators(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
// SY <--
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -2,7 +2,7 @@ package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import exh.md.handlers.serializers.ApiChapterSerializer
|
||||
import exh.md.handlers.serializers.ChapterResponse
|
||||
import exh.md.utils.MdUtil
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
@ -10,18 +10,26 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import okhttp3.Response
|
||||
|
||||
class ApiChapterParser {
|
||||
// Only used in [PageHandler], which means its currently unused, kept for reference
|
||||
fun pageListParse(response: Response): List<Page> {
|
||||
val networkApiChapter = response.parseAs<ApiChapterSerializer>(MdUtil.jsonParser)
|
||||
fun pageListParse(response: Response, host: String, dataSaver: Boolean): List<Page> {
|
||||
val networkApiChapter = response.parseAs<ChapterResponse>(MdUtil.jsonParser)
|
||||
|
||||
val hash = networkApiChapter.data.hash
|
||||
val pageArray = networkApiChapter.data.pages
|
||||
val server = networkApiChapter.data.server
|
||||
val pages = mutableListOf<Page>()
|
||||
|
||||
return pageArray.mapIndexed { index, page ->
|
||||
val url = "$hash/$page"
|
||||
Page(index, "$server,${response.request.url},${System.currentTimeMillis()}", url)
|
||||
val atHomeRequestUrl = response.request.url.toUrl().toString()
|
||||
|
||||
val hash = networkApiChapter.data.attributes.hash
|
||||
val pageArray = if (dataSaver) {
|
||||
networkApiChapter.data.attributes.dataSaver.map { "/data-saver/$hash/$it" }
|
||||
} else {
|
||||
networkApiChapter.data.attributes.data.map { "/data/$hash/$it" }
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
pageArray.forEach { imgUrl ->
|
||||
val mdAtHomeUrl = "$host,$atHomeRequestUrl,$now"
|
||||
pages += Page(pages.size, mdAtHomeUrl, imgUrl)
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
fun externalParse(response: Response): String {
|
||||
|
@ -1,34 +1,32 @@
|
||||
package exh.md.handlers
|
||||
|
||||
import com.elvishew.xlog.XLog
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import exh.log.xLogE
|
||||
import exh.md.handlers.serializers.ApiChapterSerializer
|
||||
import exh.md.handlers.serializers.ApiMangaSerializer
|
||||
import exh.md.handlers.serializers.ChapterSerializer
|
||||
import exh.md.utils.MdLang
|
||||
import exh.md.handlers.serializers.AuthorResponseList
|
||||
import exh.md.handlers.serializers.ChapterResponse
|
||||
import exh.md.handlers.serializers.MangaResponse
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.metadata.metadata.base.RaisedTag
|
||||
import exh.metadata.metadata.base.getFlatMetadataForManga
|
||||
import exh.metadata.metadata.base.insertFlatMetadata
|
||||
import exh.metadata.metadata.base.insertFlatMetadataCompletable
|
||||
import exh.util.executeOnIO
|
||||
import exh.util.floor
|
||||
import exh.util.nullIfZero
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import rx.Completable
|
||||
import rx.Single
|
||||
import tachiyomi.source.model.ChapterInfo
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class ApiMangaParser(private val lang: String) {
|
||||
val db: DatabaseHelper get() = Injekt.get()
|
||||
class ApiMangaParser(val client: OkHttpClient, private val lang: String) {
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
|
||||
val metaClass = MangaDexSearchMetadata::class
|
||||
|
||||
@ -40,44 +38,18 @@ class ApiMangaParser(private val lang: String) {
|
||||
}?.call()
|
||||
?: error("Could not find no-args constructor for meta class: ${metaClass.qualifiedName}!")
|
||||
|
||||
/**
|
||||
* Parses metadata from the input and then copies it into the manga
|
||||
*
|
||||
* Will also save the metadata to the DB if possible
|
||||
*/
|
||||
fun parseToManga(manga: SManga, input: Response, coverUrls: List<String>): Completable {
|
||||
val mangaId = (manga as? Manga)?.id
|
||||
val metaObservable = if (mangaId != null) {
|
||||
// We have to use fromCallable because StorIO messes up the thread scheduling if we use their rx functions
|
||||
Single.fromCallable {
|
||||
db.getFlatMetadataForManga(mangaId).executeAsBlocking()
|
||||
}.map {
|
||||
it?.raise(metaClass) ?: newMetaInstance()
|
||||
}
|
||||
} else {
|
||||
Single.just(newMetaInstance())
|
||||
}
|
||||
|
||||
return metaObservable.map {
|
||||
parseIntoMetadata(it, input, coverUrls)
|
||||
it.copyTo(manga)
|
||||
it
|
||||
}.flatMapCompletable {
|
||||
if (mangaId != null) {
|
||||
it.mangaId = mangaId
|
||||
db.insertFlatMetadataCompletable(it.flatten())
|
||||
} else Completable.complete()
|
||||
}
|
||||
suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List<String>, sourceId: Long): MangaInfo {
|
||||
return parseToManga(manga, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls, sourceId)
|
||||
}
|
||||
|
||||
suspend fun parseToManga(manga: MangaInfo, input: Response, coverUrls: List<String>, sourceId: Long): MangaInfo {
|
||||
suspend fun parseToManga(manga: MangaInfo, input: MangaResponse, coverUrls: List<String>, sourceId: Long): MangaInfo {
|
||||
val mangaId = db.getManga(manga.key, sourceId).executeOnIO()?.id
|
||||
val metadata = if (mangaId != null) {
|
||||
val flatMetadata = db.getFlatMetadataForManga(mangaId).executeOnIO()
|
||||
flatMetadata?.raise(metaClass) ?: newMetaInstance()
|
||||
} else newMetaInstance()
|
||||
|
||||
parseInfoIntoMetadata(metadata, input, coverUrls)
|
||||
parseIntoMetadata(metadata, input, coverUrls)
|
||||
if (mangaId != null) {
|
||||
metadata.mangaId = mangaId
|
||||
db.insertFlatMetadata(metadata.flatten())
|
||||
@ -86,69 +58,82 @@ class ApiMangaParser(private val lang: String) {
|
||||
return metadata.createMangaInfo(manga)
|
||||
}
|
||||
|
||||
fun parseInfoIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) = parseIntoMetadata(metadata, input, coverUrls)
|
||||
|
||||
/**
|
||||
* Parse the manga details json into metadata object
|
||||
*/
|
||||
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, input: Response, coverUrls: List<String>) {
|
||||
parseIntoMetadata(metadata, input.parseAs<MangaResponse>(MdUtil.jsonParser), coverUrls)
|
||||
}
|
||||
|
||||
fun parseIntoMetadata(metadata: MangaDexSearchMetadata, networkApiManga: MangaResponse, coverUrls: List<String>) {
|
||||
with(metadata) {
|
||||
try {
|
||||
val networkApiManga = input.parseAs<ApiMangaSerializer>(MdUtil.jsonParser)
|
||||
val networkManga = networkApiManga.data.manga
|
||||
mdId = MdUtil.getMangaId(input.request.url.toString())
|
||||
mdUrl = input.request.url.toString()
|
||||
title = MdUtil.cleanString(networkManga.title)
|
||||
thumbnail_url = if (coverUrls.isNotEmpty()) {
|
||||
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<AuthorResponseList>(MdUtil.jsonParser)
|
||||
json.results.map { MdUtil.cleanString(it.data.attributes.name) }.takeUnless { it.isEmpty() }
|
||||
}.getOrNull()
|
||||
|
||||
langFlag = networkManga.originalLanguage
|
||||
val lastChapter = networkManga.lastChapter.toFloatOrNull()
|
||||
lastChapterNumber = lastChapter?.floor()
|
||||
|
||||
/*networkManga.rating?.let {
|
||||
manga.rating = it.bayesian ?: it.mean
|
||||
manga.users = it.users
|
||||
}*/
|
||||
|
||||
networkManga.links?.let {
|
||||
it["al"]?.let { anilistId = it }
|
||||
it["kt"]?.let { kitsuId = it }
|
||||
it["mal"]?.let { myAnimeListId = it }
|
||||
it["mu"]?.let { mangaUpdatesId = it }
|
||||
it["ap"]?.let { animePlanetId = it }
|
||||
}
|
||||
// val filteredChapters = filterChapterForChecking(networkApiManga)
|
||||
|
||||
val tempStatus = parseStatus(networkManga.status ?: "")
|
||||
val publishedOrCancelled =
|
||||
tempStatus == SManga.PUBLICATION_COMPLETE || tempStatus == SManga.CANCELLED
|
||||
if (publishedOrCancelled && isMangaCompleted(networkApiManga, filteredChapters)) {
|
||||
status = SManga.COMPLETED
|
||||
missing_chapters = null
|
||||
maxChapterNumber = networkApiManga.data.manga.lastChapter?.toDoubleOrNull()?.floor()
|
||||
} else {
|
||||
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<ChapterSerializer>
|
||||
): Boolean {
|
||||
val finalChapterNumber = serializer.data.manga.lastChapter
|
||||
if (filteredChapters.isEmpty() || finalChapterNumber.isNullOrEmpty()) {
|
||||
if (filteredChapters.isEmpty() || serializer.data.manga.lastChapter.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
// just to fix the stupid lint
|
||||
val finalChapterNumber = serializer.data.manga.lastChapter!!
|
||||
if (MdUtil.validOneShotFinalChapters.contains(finalChapterNumber)) {
|
||||
filteredChapters.firstOrNull()?.let {
|
||||
if (isOneShot(it, finalChapterNumber)) {
|
||||
@ -177,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<ChapterSerializer> {
|
||||
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<ChapterSerializer> {
|
||||
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<MangaResponse>(MdUtil.jsonParser).data.id
|
||||
}
|
||||
|
||||
fun chapterListParse(response: Response): List<ChapterInfo> {
|
||||
return chapterListParse(response.parseAs<ApiMangaSerializer>(MdUtil.jsonParser))
|
||||
fun chapterListParse(chapterListResponse: List<ChapterResponse>, groupMap: Map<String, String>): List<ChapterInfo> {
|
||||
val now = Date().time
|
||||
|
||||
return chapterListResponse.asSequence()
|
||||
.map {
|
||||
mapChapter(it, groupMap)
|
||||
}.filter {
|
||||
it.dateUpload <= now && "MangaPlus" != it.scanlator
|
||||
}.toList()
|
||||
}
|
||||
|
||||
fun chapterListParse(networkApiManga: ApiMangaSerializer): List<ChapterInfo> {
|
||||
val now = System.currentTimeMillis()
|
||||
val networkManga = networkApiManga.data.manga
|
||||
val networkChapters = networkApiManga.data.chapters
|
||||
val groups = networkApiManga.data.groups.mapNotNull {
|
||||
if (it.name == null) {
|
||||
null
|
||||
} else {
|
||||
it.id to it.name
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
val status = networkManga.publication!!.status
|
||||
|
||||
val finalChapterNumber = networkManga.lastChapter
|
||||
|
||||
// Skip chapters that don't match the desired language, or are future releases
|
||||
|
||||
val chapLang = MdLang.values().firstOrNull { lang == it.dexLang }
|
||||
return networkChapters.asSequence()
|
||||
.filter { lang == it.language && (it.timestamp * 1000) <= now }
|
||||
.map { mapChapter(it, finalChapterNumber, status, chapLang, networkChapters.size, groups) }.toList()
|
||||
}
|
||||
|
||||
fun chapterParseForMangaId(response: Response): Int {
|
||||
fun chapterParseForMangaId(response: Response): String {
|
||||
try {
|
||||
return response.parseAs<ApiChapterSerializer>().data.mangaId
|
||||
return response.parseAs<ChapterResponse>(MdUtil.jsonParser)
|
||||
.relationships.firstOrNull { it.type.equals("manga", true) }?.id ?: throw Exception("Not found")
|
||||
} catch (e: Exception) {
|
||||
xLogE("Parse for manga id error", e)
|
||||
XLog.e(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapChapter(
|
||||
networkChapter: ChapterSerializer,
|
||||
finalChapterNumber: String?,
|
||||
status: Int,
|
||||
chapLang: MdLang?,
|
||||
totalChapterCount: Int,
|
||||
groups: Map<Long, String>
|
||||
networkChapter: ChapterResponse,
|
||||
groups: Map<String, String>,
|
||||
): ChapterInfo {
|
||||
val key = MdUtil.oldApiChapter + networkChapter.id
|
||||
|
||||
// Build chapter name
|
||||
val chapter = SChapter.create()
|
||||
val attributes = networkChapter.data.attributes
|
||||
val key = MdUtil.chapterSuffix + networkChapter.data.id
|
||||
val chapterName = mutableListOf<String>()
|
||||
// Build chapter name
|
||||
|
||||
if (!networkChapter.volume.isNullOrBlank()) {
|
||||
val vol = "Vol." + networkChapter.volume
|
||||
if (attributes.volume != null) {
|
||||
val vol = "Vol." + attributes.volume
|
||||
chapterName.add(vol)
|
||||
// todo
|
||||
// chapter.vol = vol
|
||||
}
|
||||
|
||||
if (!networkChapter.chapter.isNullOrBlank()) {
|
||||
val chp = "Ch." + networkChapter.chapter
|
||||
chapterName.add(chp)
|
||||
// chapter.chapter_txt = chp
|
||||
}
|
||||
if (!networkChapter.title.isNullOrBlank()) {
|
||||
if (attributes.chapter.isNullOrBlank().not()) {
|
||||
if (chapterName.isNotEmpty()) {
|
||||
chapterName.add("-")
|
||||
}
|
||||
// todo
|
||||
chapterName.add(networkChapter.title)
|
||||
// chapter.chapter_title = MdUtil.cleanString(networkChapter.title)
|
||||
val chp = "Ch.${attributes.chapter}"
|
||||
chapterName.add(chp)
|
||||
// chapter.chapter_txt = chp
|
||||
}
|
||||
|
||||
if (attributes.title.isNullOrBlank().not()) {
|
||||
if (chapterName.isNotEmpty()) {
|
||||
chapterName.add("-")
|
||||
}
|
||||
chapterName.add(attributes.title!!)
|
||||
chapter.name = MdUtil.cleanString(attributes.title)
|
||||
}
|
||||
|
||||
// if volume, chapter and title is empty its a oneshot
|
||||
if (chapterName.isEmpty()) {
|
||||
chapterName.add("Oneshot")
|
||||
}
|
||||
if ((status == 2 || status == 3)) {
|
||||
/*if ((status == 2 || status == 3)) {
|
||||
if (finalChapterNumber != null) {
|
||||
if ((isOneShot(networkChapter, finalChapterNumber) && totalChapterCount == 1) ||
|
||||
networkChapter.chapter == finalChapterNumber && finalChapterNumber.toIntOrNull() != 0
|
||||
@ -303,26 +271,25 @@ class ApiMangaParser(private val lang: String) {
|
||||
chapterName.add("[END]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
val name = MdUtil.cleanString(chapterName.joinToString(" "))
|
||||
// Convert from unix time
|
||||
val dateUpload = networkChapter.timestamp * 1000
|
||||
val scanlatorName = mutableSetOf<String>()
|
||||
val dateUpload = MdUtil.parseDate(attributes.publishAt)
|
||||
|
||||
networkChapter.groups.mapNotNull { groups[it] }.forEach { scanlatorName.add(it) }
|
||||
val scanlatorName = networkChapter.relationships.filter { it.type == "scanlation_group" }.mapNotNull { groups[it.id] }.toSet()
|
||||
|
||||
val scanlator = MdUtil.cleanString(MdUtil.getScanlatorString(scanlatorName))
|
||||
|
||||
// val mangadexChapterId = MdUtil.getChapterId(chapter.url)
|
||||
// chapter.mangadex_chapter_id = MdUtil.getChapterId(chapter.url)
|
||||
|
||||
// val language = chapLang?.name
|
||||
// chapter.language = MdLang.fromIsoCode(attributes.translatedLanguage)?.prettyPrint ?: ""
|
||||
|
||||
return ChapterInfo(
|
||||
key = key,
|
||||
name = name,
|
||||
scanlator = scanlator,
|
||||
dateUpload = dateUpload,
|
||||
scanlator = scanlator
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,180 +1,261 @@
|
||||
package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.model.Filter
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import okhttp3.HttpUrl
|
||||
import java.util.Locale
|
||||
|
||||
class FilterHandler {
|
||||
class FilterHandler(private val preferencesHelper: PreferencesHelper) {
|
||||
|
||||
class TextField(name: String, val key: String) : Filter.Text(name)
|
||||
class Tag(val id: String, name: String) : Filter.TriState(name)
|
||||
class Switch(val id: String, name: String) : Filter.CheckBox(name)
|
||||
class ContentList(contents: List<Tag>) : Filter.Group<Tag>("Content", contents)
|
||||
class FormatList(formats: List<Tag>) : Filter.Group<Tag>("Format", formats)
|
||||
class GenreList(genres: List<Tag>) : Filter.Group<Tag>("Genres", genres)
|
||||
class PublicationStatusList(statuses: List<Switch>) : Filter.Group<Switch>("Publication Status", statuses)
|
||||
class DemographicList(demographics: List<Switch>) : Filter.Group<Switch>("Demographic", demographics)
|
||||
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<String>("R18+", arrayOf("Default", "Show all", "Show only", "Show none"))
|
||||
class ThemeList(themes: List<Tag>) : Filter.Group<Tag>("Themes", themes)
|
||||
class TagInclusionMode : Filter.Select<String>("Tag inclusion", arrayOf("All (and)", "Any (or)"), 0)
|
||||
class TagExclusionMode : Filter.Select<String>("Tag exclusion", arrayOf("All (and)", "Any (or)"), 1)
|
||||
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<Demographic>) :
|
||||
Filter.Group<Demographic>("Publication Demographic", demographics)
|
||||
|
||||
private fun getDemographics() = listOf(
|
||||
Demographic("None"),
|
||||
Demographic("Shounen"),
|
||||
Demographic("Shoujo"),
|
||||
Demographic("Seinen"),
|
||||
Demographic("Josei")
|
||||
)
|
||||
|
||||
class OriginalLanguage : Filter.Select<String>("Original Language", sourceLang().map { it.first }.toTypedArray())
|
||||
private class Status(name: String) : Filter.CheckBox(name)
|
||||
private class StatusList(status: List<Status>) :
|
||||
Filter.Group<Status>("Status", status)
|
||||
|
||||
fun getFilterList() = FilterList(
|
||||
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<ContentRating>) :
|
||||
Filter.Group<ContentRating>("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<OriginalLanguage>) :
|
||||
Filter.Group<OriginalLanguage>("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<Tag>) : Filter.Group<Tag>("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<String>("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<String>("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<String>) : Filter.Sort("Sort", sortables, Selection(0, false))
|
||||
|
||||
fun addFiltersToUrl(url: HttpUrl.Builder, filters: FilterList): String {
|
||||
url.apply {
|
||||
// add filters
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is OriginalLanguageList -> {
|
||||
filter.state.forEach { lang ->
|
||||
if (lang.state) {
|
||||
addQueryParameter(
|
||||
"originalLanguage[]",
|
||||
lang.isoCode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is ContentRatingList -> {
|
||||
filter.state.forEach { rating ->
|
||||
if (rating.state) {
|
||||
addQueryParameter(
|
||||
"contentRating[]",
|
||||
rating.name.toLowerCase(Locale.US)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is DemographicList -> {
|
||||
filter.state.forEach { demographic ->
|
||||
if (demographic.state) {
|
||||
addQueryParameter(
|
||||
"publicationDemographic[]",
|
||||
demographic.name.toLowerCase(
|
||||
Locale.US
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is StatusList -> {
|
||||
filter.state.forEach { status ->
|
||||
if (status.state) {
|
||||
addQueryParameter(
|
||||
"status[]",
|
||||
status.name.toLowerCase(
|
||||
Locale.US
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is SortFilter -> {
|
||||
if (filter.state != null) {
|
||||
if (filter.state!!.index != 0) {
|
||||
val query = sortableList[filter.state!!.index].second
|
||||
val value = when (filter.state!!.ascending) {
|
||||
true -> "asc"
|
||||
false -> "desc"
|
||||
}
|
||||
addQueryParameter("order[$query]", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TagList -> {
|
||||
filter.state.forEach { tag ->
|
||||
if (tag.isIncluded()) {
|
||||
addQueryParameter("includedTags[]", tag.id)
|
||||
} else if (tag.isExcluded()) {
|
||||
addQueryParameter("excludedTags[]", tag.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is TagInclusionMode -> {
|
||||
addQueryParameter(
|
||||
"includedTagsMode",
|
||||
filter.values[filter.state].toUpperCase(Locale.US)
|
||||
)
|
||||
}
|
||||
is TagExclusionMode -> {
|
||||
addQueryParameter(
|
||||
"excludedTagsMode",
|
||||
filter.values[filter.state].toUpperCase(Locale.US)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (false) { // preferencesHelper.showR18Filter().not()) {
|
||||
addQueryParameter("contentRating[]", "safe")
|
||||
}
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
}
|
||||
|
@ -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<MangaListResponse>(MdUtil.jsonParser)
|
||||
val results = mangaListResponse.results.toMutableList()
|
||||
|
||||
var hasMoreResults = mangaListResponse.limit + mangaListResponse.offset under mangaListResponse.total
|
||||
var lastOffset = mangaListResponse.offset
|
||||
|
||||
while (hasMoreResults) {
|
||||
val offset = lastOffset + mangaListResponse.limit
|
||||
val newMangaListResponse = client.newCall(followsListRequest(offset)).await()
|
||||
.parseAs<MangaListResponse>(MdUtil.jsonParser)
|
||||
results.addAll(newMangaListResponse.results)
|
||||
hasMoreResults = newMangaListResponse.limit + newMangaListResponse.offset under newMangaListResponse.total
|
||||
lastOffset = newMangaListResponse.offset
|
||||
}
|
||||
val statusListResponse = client.newCall(statusListRequest()).await().parseAs<JsonObject>()
|
||||
followsParseMangaPage(results, statusListResponse)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse follows api to manga page
|
||||
* used when multiple follows
|
||||
*/
|
||||
private fun followsParseMangaPage(response: Response, forceHd: Boolean = false): MetadataMangasPage {
|
||||
val followsPageResult = try {
|
||||
MdUtil.jsonParser.decodeFromString(
|
||||
response.body?.string().orEmpty()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
xLogE("error parsing follows", e)
|
||||
FollowsPageSerializer(404, emptyList())
|
||||
}
|
||||
private fun followsParseMangaPage(response: List<MangaResponse>, statusListResponse: JsonObject): MetadataMangasPage {
|
||||
val comparator = compareBy<Pair<MangaInfo, MangaDexSearchMetadata>> { it.second.followStatus }
|
||||
.thenBy { it.first.title }
|
||||
val result = response.map {
|
||||
MdUtil.createMangaEntry(it, lang, useLowQualityCovers) to MangaDexSearchMetadata().apply {
|
||||
followStatus = getFollowStatus(statusListResponse, it.data.id).int
|
||||
}
|
||||
}.sortedWith(comparator)
|
||||
|
||||
if (followsPageResult.data.isNullOrEmpty() || followsPageResult.code != 200) {
|
||||
return MetadataMangasPage(emptyList(), false, emptyList())
|
||||
}
|
||||
val lowQualityCovers = if (forceHd) false else useLowQualityCovers
|
||||
|
||||
val follows = followsPageResult.data.map {
|
||||
followFromElement(it, lowQualityCovers)
|
||||
}
|
||||
|
||||
val comparator = compareBy<Pair<SManga, MangaDexSearchMetadata>> { it.second.follow_status }.thenBy { it.first.title }
|
||||
|
||||
val result = follows.sortedWith(comparator)
|
||||
|
||||
return MetadataMangasPage(result.map { it.first }, false, result.map { it.second })
|
||||
return MetadataMangasPage(result.map { it.first.toSManga() }, false, result.map { it.second })
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch follow status used when fetching status for 1 manga
|
||||
*/
|
||||
private fun followStatusParse(response: Response): Track {
|
||||
val followsPageResult = try {
|
||||
response.parseAs<FollowsIndividualSerializer>(MdUtil.jsonParser)
|
||||
} catch (e: Exception) {
|
||||
xLogE("error parsing follows", e)
|
||||
throw e
|
||||
}
|
||||
|
||||
private fun followStatusParse(response: Response, statusListResponse: JsonObject): Track {
|
||||
val mangaResponse = response.parseAs<MangaResponse>(MdUtil.jsonParser)
|
||||
val track = Track.create(TrackManager.MDLIST)
|
||||
if (followsPageResult.code == 404) {
|
||||
track.status = FollowStatus.UNFOLLOWED.int
|
||||
} else {
|
||||
val follow = followsPageResult.data ?: throw Exception("Invalid response ${followsPageResult.code}")
|
||||
track.status = follow.followType
|
||||
if (follow.chapter.isNotBlank()) {
|
||||
track.status = getFollowStatus(statusListResponse, mangaResponse.data.id).int
|
||||
track.tracking_url = MdUtil.baseUrl + "/manga/" + mangaResponse.data.id
|
||||
track.title = mangaResponse.data.attributes.title[lang] ?: mangaResponse.data.attributes.title["en"]!!
|
||||
|
||||
/* if (follow.chapter.isNotBlank()) {
|
||||
track.last_chapter_read = follow.chapter.toFloat().floor()
|
||||
}
|
||||
track.tracking_url = MdUtil.baseUrl + follow.mangaId.toString()
|
||||
track.title = follow.mangaTitle
|
||||
}
|
||||
}*/
|
||||
return track
|
||||
}
|
||||
|
||||
/**
|
||||
* build Request for follows page
|
||||
*/
|
||||
private fun followsListRequest(): Request {
|
||||
return GET("${MdUtil.apiUrl}${MdUtil.followsAllApi}", headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
private fun followsListRequest(offset: Int): Request {
|
||||
val tempUrl = MdUtil.userFollows.toHttpUrl().newBuilder()
|
||||
|
||||
/**
|
||||
* Parse result element to manga
|
||||
*/
|
||||
private fun followFromElement(result: FollowPage, lowQualityCovers: Boolean): Pair<SManga, MangaDexSearchMetadata> {
|
||||
val manga = SManga.create()
|
||||
manga.title = MdUtil.cleanString(result.mangaTitle)
|
||||
manga.url = "/manga/${result.mangaId}/"
|
||||
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, lowQualityCovers)
|
||||
return manga to MangaDexSearchMetadata().apply {
|
||||
title = manga.title
|
||||
mdUrl = manga.url
|
||||
thumbnail_url = manga.thumbnail_url
|
||||
follow_status = FollowStatus.fromInt(result.followType).int
|
||||
tempUrl.apply {
|
||||
addQueryParameter("limit", MdUtil.mangaLimit.toString())
|
||||
addQueryParameter("offset", offset.toString())
|
||||
}
|
||||
return GET(tempUrl.build().toString(), MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the status of a manga
|
||||
*/
|
||||
suspend fun updateFollowStatus(mangaID: String, followStatus: FollowStatus): Boolean {
|
||||
suspend fun updateFollowStatus(mangaId: String, followStatus: FollowStatus): Boolean {
|
||||
return withIOContext {
|
||||
if (followStatus == FollowStatus.UNFOLLOWED) {
|
||||
client.newCall(
|
||||
GET(
|
||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_unfollow&id=$mangaID&type=$mangaID",
|
||||
headers,
|
||||
CacheControl.FORCE_NETWORK
|
||||
)
|
||||
val status = when (followStatus == FollowStatus.UNFOLLOWED) {
|
||||
true -> null
|
||||
false -> followStatus.name.toLowerCase(Locale.US)
|
||||
}
|
||||
|
||||
val jsonString = MdUtil.jsonParser.encodeToString(UpdateReadingStatus(status))
|
||||
|
||||
val postResult = client.newCall(
|
||||
POST(
|
||||
MdUtil.updateReadingStatusUrl(mangaId),
|
||||
MdUtil.getAuthHeaders(headers, preferences, mdList),
|
||||
jsonString.toRequestBody("application/json".toMediaType())
|
||||
)
|
||||
} else {
|
||||
val status = followStatus.int
|
||||
client.newCall(
|
||||
GET(
|
||||
"${MdUtil.baseUrl}/ajax/actions.ajax.php?function=manga_follow&id=$mangaID&type=$status",
|
||||
headers,
|
||||
CacheControl.FORCE_NETWORK
|
||||
)
|
||||
)
|
||||
}.succeeded()
|
||||
).await()
|
||||
postResult.isSuccessful
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateReadingProgress(track: Track): Boolean {
|
||||
return withIOContext {
|
||||
val mangaID = MdUtil.getMangaId(track.tracking_url)
|
||||
return true
|
||||
/*return withIOContext {
|
||||
val mangaID = getMangaId(track.tracking_url)
|
||||
val formBody = FormBody.Builder()
|
||||
.add("volume", "0")
|
||||
.add("chapter", track.last_chapter_read.toString())
|
||||
xLogD("chapter to update %s", track.last_chapter_read.toString())
|
||||
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<Pair<SManga, MangaDexSearchMetadata>> {
|
||||
suspend fun fetchAllFollows(): List<Pair<SManga, MangaDexSearchMetadata>> {
|
||||
return withIOContext {
|
||||
val response = client.newCall(followsListRequest()).await()
|
||||
val mangasPage = followsParseMangaPage(response, forceHd)
|
||||
mangasPage.mangas.mapIndexed { index, sManga ->
|
||||
sManga to mangasPage.mangasMetadata[index] as MangaDexSearchMetadata
|
||||
val metadata: List<MangaDexSearchMetadata>
|
||||
fetchFollows().also { metadata = it.mangasMetadata.filterIsInstance<MangaDexSearchMetadata>() }.mangas.mapIndexed { index, manga ->
|
||||
manga to metadata[index]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -206,12 +207,20 @@ class FollowsHandler(val client: OkHttpClient, val headers: Headers, val prefere
|
||||
suspend fun fetchTrackingInfo(url: String): Track {
|
||||
return withIOContext {
|
||||
val request = GET(
|
||||
MdUtil.apiUrl + MdUtil.followsMangaApi + MdUtil.getMangaId(url),
|
||||
headers,
|
||||
MdUtil.mangaUrl + "/" + MdUtil.getMangaId(url),
|
||||
MdUtil.getAuthHeaders(headers, preferences, mdList),
|
||||
CacheControl.FORCE_NETWORK
|
||||
)
|
||||
val response = client.newCall(request).awaitResponse()
|
||||
followStatusParse(response)
|
||||
val response = client.newCall(request).await()
|
||||
val statusListResponse = client.newCall(statusListRequest()).await().parseAs<JsonObject>(MdUtil.jsonParser)
|
||||
followStatusParse(response, statusListResponse)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFollowStatus(jsonObject: JsonObject, id: String) =
|
||||
FollowStatus.fromDex(jsonObject["statuses"]?.jsonObject?.get(id)?.jsonPrimitive?.content)
|
||||
|
||||
private fun statusListRequest(): Request {
|
||||
return GET(MdUtil.mangaStatus, MdUtil.getAuthHeaders(headers, preferences, mdList), CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
@ -9,12 +10,15 @@ import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.model.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.model.toSChapter
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.md.handlers.serializers.ApiCovers
|
||||
import exh.md.handlers.serializers.ApiMangaSerializer
|
||||
import exh.md.handlers.serializers.ChapterListResponse
|
||||
import exh.md.handlers.serializers.ChapterResponse
|
||||
import exh.md.handlers.serializers.GroupListResponse
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.MangaDexSearchMetadata
|
||||
import exh.util.under
|
||||
import kotlinx.coroutines.async
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
@ -26,124 +30,179 @@ import tachiyomi.source.model.MangaInfo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class MangaHandler(val client: OkHttpClient, val headers: Headers, val lang: String, val forceLatestCovers: Boolean = false) {
|
||||
class MangaHandler(val client: OkHttpClient, val headers: Headers, private val lang: String, private val forceLatestCovers: Boolean = false) {
|
||||
|
||||
// TODO make use of this
|
||||
suspend fun fetchMangaAndChapterDetails(manga: MangaInfo, sourceId: Long): Pair<MangaInfo, List<ChapterInfo>> {
|
||||
return withIOContext {
|
||||
val apiNetworkManga = client.newCall(apiRequest(manga)).await().parseAs<ApiMangaSerializer>(MdUtil.jsonParser)
|
||||
val response = client.newCall(mangaRequest(manga)).await()
|
||||
val covers = getCovers(manga, forceLatestCovers)
|
||||
val parser = ApiMangaParser(lang)
|
||||
val parser = ApiMangaParser(client, lang)
|
||||
|
||||
// TODO fix this
|
||||
/*val mangaInfo = parser.parseToManga(manga, response, covers, sourceId)
|
||||
val chapterList = parser.chapterListParse(apiNetworkManga)
|
||||
|
||||
mangaInfo to chapterList*/
|
||||
manga to emptyList()
|
||||
parser.parseToManga(manga, response, covers, sourceId) to getChapterList(manga)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> {
|
||||
return if (forceLatestCovers) {
|
||||
val covers = client.newCall(coverRequest(manga)).await().parseAs<ApiCovers>(MdUtil.jsonParser)
|
||||
covers.data.map { it.url }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
suspend fun getCovers(manga: MangaInfo, forceLatestCovers: Boolean): List<String> {
|
||||
/* if (forceLatestCovers) {
|
||||
val covers = client.newCall(coverRequest(manga)).await().parseAs<ApiCovers>(MdUtil.jsonParser)
|
||||
return covers.data.map { it.url }
|
||||
} else {*/
|
||||
return emptyList<String>()
|
||||
// }
|
||||
}
|
||||
|
||||
suspend fun getMangaIdFromChapterId(urlChapterId: String): Int {
|
||||
suspend fun getMangaIdFromChapterId(urlChapterId: String): String {
|
||||
return withIOContext {
|
||||
val request = GET(MdUtil.apiUrl + MdUtil.newApiChapter + urlChapterId + MdUtil.apiChapterSuffix, headers, CacheControl.FORCE_NETWORK)
|
||||
val request = GET(MdUtil.chapterUrl + urlChapterId)
|
||||
val response = client.newCall(request).await()
|
||||
ApiMangaParser(lang).chapterParseForMangaId(response)
|
||||
ApiMangaParser(client, lang).chapterParseForMangaId(response)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMangaDetails(manga: MangaInfo, sourceId: Long): MangaInfo {
|
||||
return withIOContext {
|
||||
val response = client.newCall(apiRequest(manga)).await()
|
||||
val response = client.newCall(mangaRequest(manga)).await()
|
||||
val covers = getCovers(manga, forceLatestCovers)
|
||||
ApiMangaParser(lang).parseToManga(manga, response, covers, sourceId)
|
||||
ApiMangaParser(client, lang).parseToManga(manga, response, covers, sourceId)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchMangaDetailsObservable(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(apiRequest(manga.toMangaInfo()))
|
||||
fun fetchMangaDetailsObservable(manga: SManga, sourceId: Long): Observable<SManga> {
|
||||
return client.newCall(mangaRequest(manga.toMangaInfo()))
|
||||
.asObservableSuccess()
|
||||
.flatMap { response ->
|
||||
runAsObservable({
|
||||
getCovers(manga.toMangaInfo(), forceLatestCovers)
|
||||
}).map {
|
||||
response to it
|
||||
}
|
||||
}
|
||||
.flatMap {
|
||||
ApiMangaParser(lang).parseToManga(manga, it.first, it.second).andThen(
|
||||
Observable.just(
|
||||
manga.apply {
|
||||
initialized = true
|
||||
}
|
||||
)
|
||||
)
|
||||
ApiMangaParser(client, lang).parseToManga(manga.toMangaInfo(), response, emptyList(), sourceId).toSManga()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchChapterListObservable(manga: SManga): Observable<List<SChapter>> {
|
||||
return client.newCall(apiRequest(manga.toMangaInfo()))
|
||||
return client.newCall(mangaFeedRequest(manga.toMangaInfo(), 0, lang))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
ApiMangaParser(lang).chapterListParse(response).map { it.toSChapter() }
|
||||
val chapterListResponse = response.parseAs<ChapterListResponse>(MdUtil.jsonParser)
|
||||
val results = chapterListResponse.results.toMutableList()
|
||||
|
||||
var hasMoreResults = chapterListResponse.limit + chapterListResponse.offset under chapterListResponse.total
|
||||
var lastOffset = chapterListResponse.offset
|
||||
|
||||
while (hasMoreResults) {
|
||||
val offset = lastOffset + chapterListResponse.limit
|
||||
val newChapterListResponse = client.newCall(mangaFeedRequest(manga.toMangaInfo(), offset, lang)).execute()
|
||||
.parseAs<ChapterListResponse>(MdUtil.jsonParser)
|
||||
results.addAll(newChapterListResponse.results)
|
||||
hasMoreResults = newChapterListResponse.limit + newChapterListResponse.offset under newChapterListResponse.total
|
||||
lastOffset = newChapterListResponse.offset
|
||||
}
|
||||
val groupIds =
|
||||
results.asSequence()
|
||||
.map { chapter -> chapter.relationships }
|
||||
.flatten()
|
||||
.filter { it.type == "scanlation_group" }
|
||||
.map { it.id }
|
||||
.distinct()
|
||||
.toList()
|
||||
|
||||
val groupMap = runCatching {
|
||||
groupIds.chunked(100).mapIndexed { index, ids ->
|
||||
val groupList = client.newCall(groupIdRequest(ids, 100 * index)).execute()
|
||||
.parseAs<GroupListResponse>(MdUtil.jsonParser)
|
||||
groupList.results.map { group -> Pair(group.data.id, group.data.attributes.name) }
|
||||
}.flatten().toMap()
|
||||
}.getOrNull() ?: emptyMap()
|
||||
|
||||
ApiMangaParser(client, lang).chapterListParse(results, groupMap).map { it.toSChapter() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
|
||||
return withIOContext {
|
||||
val response = client.newCall(apiRequest(manga)).await()
|
||||
ApiMangaParser(lang).chapterListParse(response)
|
||||
val chapterListResponse = client.newCall(mangaFeedRequest(manga, 0, lang)).await().parseAs<ChapterListResponse>(MdUtil.jsonParser)
|
||||
val results = chapterListResponse.results
|
||||
|
||||
var hasMoreResults = chapterListResponse.limit + chapterListResponse.offset under chapterListResponse.total
|
||||
var lastOffset = chapterListResponse.offset
|
||||
|
||||
while (hasMoreResults) {
|
||||
val offset = lastOffset + chapterListResponse.limit
|
||||
val newChapterListResponse = client.newCall(mangaFeedRequest(manga, offset, lang)).await()
|
||||
.parseAs<ChapterListResponse>(MdUtil.jsonParser)
|
||||
hasMoreResults = newChapterListResponse.limit + newChapterListResponse.offset under newChapterListResponse.total
|
||||
lastOffset = newChapterListResponse.offset
|
||||
}
|
||||
|
||||
val groupMap = getGroupMap(results)
|
||||
|
||||
ApiMangaParser(client, lang).chapterListParse(results, groupMap)
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchRandomMangaIdObservable(): Observable<String> {
|
||||
return client.newCall(randomMangaRequest())
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
ApiMangaParser(lang).randomMangaIdParse(response)
|
||||
}
|
||||
private suspend fun getGroupMap(results: List<ChapterResponse>): Map<String, String> {
|
||||
val groupIds = results.map { chapter -> chapter.relationships }.flatten().filter { it.type == "scanlation_group" }.map { it.id }.distinct()
|
||||
val groupMap = runCatching {
|
||||
groupIds.chunked(100).mapIndexed { index, ids ->
|
||||
client.newCall(groupIdRequest(ids, 100 * index)).await()
|
||||
.parseAs<GroupListResponse>(MdUtil.jsonParser)
|
||||
.results.map { group -> Pair(group.data.id, group.data.attributes.name) }
|
||||
}.flatten().toMap()
|
||||
}.getOrNull() ?: emptyMap()
|
||||
|
||||
return groupMap
|
||||
}
|
||||
|
||||
suspend fun fetchRandomMangaId(): String {
|
||||
return withIOContext {
|
||||
val response = client.newCall(randomMangaRequest()).await()
|
||||
ApiMangaParser(lang).randomMangaIdParse(response)
|
||||
ApiMangaParser(client, lang).randomMangaIdParse(response)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean): Pair<Track, MangaDexSearchMetadata> {
|
||||
suspend fun getTrackingInfo(track: Track, useLowQualityCovers: Boolean, mdList: MdList): Pair<Track, MangaDexSearchMetadata> {
|
||||
return withIOContext {
|
||||
val metadata = async {
|
||||
val mangaUrl = MdUtil.mapMdIdToMangaUrl(MdUtil.getMangaId(track.tracking_url).toInt())
|
||||
val mangaUrl = "/manga/" + MdUtil.getMangaId(track.tracking_url)
|
||||
val manga = MangaInfo(mangaUrl, track.title)
|
||||
val response = client.newCall(apiRequest(manga)).await()
|
||||
val response = client.newCall(mangaRequest(manga)).await()
|
||||
val metadata = MangaDexSearchMetadata()
|
||||
ApiMangaParser(lang).parseIntoMetadata(metadata, response, emptyList())
|
||||
ApiMangaParser(client, lang).parseIntoMetadata(metadata, response, emptyList())
|
||||
metadata
|
||||
}
|
||||
val remoteTrack = async { FollowsHandler(client, headers, Injekt.get(), useLowQualityCovers).fetchTrackingInfo(track.tracking_url) }
|
||||
val remoteTrack = async {
|
||||
FollowsHandler(
|
||||
client,
|
||||
headers,
|
||||
Injekt.get(),
|
||||
lang,
|
||||
useLowQualityCovers,
|
||||
mdList
|
||||
).fetchTrackingInfo(track.tracking_url)
|
||||
}
|
||||
remoteTrack.await() to metadata.await()
|
||||
}
|
||||
}
|
||||
|
||||
private fun randomMangaRequest(): Request {
|
||||
return GET(MdUtil.baseUrl + MdUtil.randMangaPage, cache = CacheControl.FORCE_NETWORK)
|
||||
return GET(MdUtil.randomMangaUrl, cache = CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
private fun apiRequest(manga: MangaInfo): Request {
|
||||
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.includeChapters, headers, CacheControl.FORCE_NETWORK)
|
||||
private fun mangaRequest(manga: MangaInfo): Request {
|
||||
return GET(MdUtil.mangaUrl + "/" + MdUtil.getMangaId(manga.key), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
private fun coverRequest(manga: MangaInfo): Request {
|
||||
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.key) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK)
|
||||
private fun mangaFeedRequest(manga: MangaInfo, offset: Int, lang: String): Request {
|
||||
return GET(MdUtil.mangaFeedUrl(MdUtil.getMangaId(manga.key), offset, lang), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
private fun groupIdRequest(id: List<String>, offset: Int): Request {
|
||||
val urlSuffix = id.joinToString("&ids[]=", "?limit=100&offset=$offset&ids[]=")
|
||||
return GET(MdUtil.groupUrl + urlSuffix, headers)
|
||||
}
|
||||
|
||||
/* private fun coverRequest(manga: SManga): Request {
|
||||
return GET(MdUtil.apiUrl + MdUtil.apiManga + MdUtil.getMangaId(manga.url) + MdUtil.apiCovers, headers, CacheControl.FORCE_NETWORK)
|
||||
}*/
|
||||
|
||||
companion object {
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,7 @@ import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import rx.Observable
|
||||
|
||||
// Unused, kept for reference todo
|
||||
class PageHandler(val client: OkHttpClient, val headers: Headers, private val imageServer: String, val dataSaver: String?) {
|
||||
class PageHandler(val client: OkHttpClient, val headers: Headers, private val dataSaver: Boolean) {
|
||||
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
if (chapter.scanlator.equals("MangaPlus")) {
|
||||
@ -26,12 +25,12 @@ class PageHandler(val client: OkHttpClient, val headers: Headers, private val im
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
ApiChapterParser().pageListParse(response)
|
||||
val host = MdUtil.atHomeUrlHostUrl("${MdUtil.atHomeUrl}/${MdUtil.getChapterId(chapter.url)}", client)
|
||||
ApiChapterParser().pageListParse(response, host, dataSaver)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pageListRequest(chapter: SChapter): Request {
|
||||
val chpUrl = chapter.url.substringBefore(MdUtil.apiChapterSuffix)
|
||||
return GET("${MdUtil.apiUrl}${chpUrl}${MdUtil.apiChapterSuffix}&server=$imageServer&saver=$dataSaver", headers, CacheControl.FORCE_NETWORK)
|
||||
return GET("${MdUtil.chapterUrl}${MdUtil.getChapterId(chapter.url)}", headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
}
|
||||
|
@ -2,24 +2,23 @@ package exh.md.handlers
|
||||
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.util.asJsoup
|
||||
import eu.kanade.tachiyomi.source.model.toSManga
|
||||
import exh.md.handlers.serializers.MangaListResponse
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.md.utils.setMDUrlWithoutDomain
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
|
||||
// Unused, kept for reference todo
|
||||
/**
|
||||
* Returns the latest manga from the updates url since it actually respects the users settings
|
||||
*/
|
||||
class PopularHandler(val client: OkHttpClient, private val headers: Headers, private val useLowQualityCovers: Boolean) {
|
||||
class PopularHandler(val client: OkHttpClient, private val headers: Headers, private val lang: String, private val useLowQualityCovers: Boolean) {
|
||||
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage> {
|
||||
return client.newCall(popularMangaRequest(page))
|
||||
@ -30,38 +29,20 @@ class PopularHandler(val client: OkHttpClient, private val headers: Headers, pri
|
||||
}
|
||||
|
||||
private fun popularMangaRequest(page: Int): Request {
|
||||
return GET("${MdUtil.baseUrl}/updates/$page/", headers, CacheControl.FORCE_NETWORK)
|
||||
val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder()
|
||||
|
||||
tempUrl.apply {
|
||||
addQueryParameter("limit", MdUtil.mangaLimit.toString())
|
||||
addQueryParameter("offset", (MdUtil.getMangaListOffset(page)))
|
||||
}
|
||||
|
||||
return GET(tempUrl.build().toString(), headers, CacheControl.FORCE_NETWORK)
|
||||
}
|
||||
|
||||
private fun popularMangaParse(response: Response): MangasPage {
|
||||
val document = response.asJsoup()
|
||||
|
||||
val mangas = document.select(popularMangaSelector).map { element ->
|
||||
popularMangaFromElement(element)
|
||||
}.distinctBy { it.url }
|
||||
|
||||
val hasNextPage = popularMangaNextPageSelector.let { selector ->
|
||||
document.select(selector).first()
|
||||
} != null
|
||||
|
||||
return MangasPage(mangas, hasNextPage)
|
||||
}
|
||||
|
||||
private fun popularMangaFromElement(element: Element): SManga {
|
||||
val manga = SManga.create()
|
||||
element.select("a.manga_title").first().let {
|
||||
val url = MdUtil.modifyMangaUrl(it.attr("href"))
|
||||
manga.setMDUrlWithoutDomain(url)
|
||||
manga.title = it.text().trim()
|
||||
}
|
||||
|
||||
manga.thumbnail_url = MdUtil.formThumbUrl(manga.url, useLowQualityCovers)
|
||||
|
||||
return manga
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val popularMangaSelector = "tr a.manga_title"
|
||||
const val popularMangaNextPageSelector = ".pagination li:not(.disabled) span[title*=last page]:not(disabled)"
|
||||
val mlResponse = response.parseAs<MangaListResponse>(MdUtil.jsonParser)
|
||||
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
|
||||
val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() }
|
||||
return MangasPage(mangaList, hasMoreResults)
|
||||
}
|
||||
}
|
||||
|
@ -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<MangasPage> {
|
||||
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<MangasPage> {
|
||||
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<MangaResponse>(MdUtil.jsonParser)
|
||||
val details = ApiMangaParser(client, lang)
|
||||
.parseToManga(MdUtil.createMangaEntry(mangaResponse, lang, useLowQualityCovers), response, emptyList(), sourceId).toSManga()
|
||||
MangasPage(listOf(details), false)
|
||||
}
|
||||
}
|
||||
query.startsWith(PREFIX_GROUP_SEARCH) -> {
|
||||
val realQuery = query.removePrefix(PREFIX_GROUP_SEARCH)
|
||||
client.newCall(searchMangaByGroupRequest(realQuery))
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
response.asJsoup().select(groupSelector).firstOrNull()?.attr("abs:href")
|
||||
?.let {
|
||||
searchMangaParse(client.newCall(GET("$it/manga/0", headers)).execute())
|
||||
}
|
||||
?: MangasPage(emptyList(), false)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
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<MangaListResponse>(MdUtil.jsonParser)
|
||||
val hasMoreResults = mlResponse.limit + mlResponse.offset < mlResponse.total
|
||||
val mangaList = mlResponse.results.map { MdUtil.createMangaEntry(it, lang, useLowQualityCovers).toSManga() }
|
||||
return MangasPage(mangaList, hasMoreResults)
|
||||
}
|
||||
|
||||
private fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
|
||||
val tags = mutableListOf<String>()
|
||||
val statuses = mutableListOf<String>()
|
||||
val demographics = mutableListOf<String>()
|
||||
val tempUrl = MdUtil.mangaUrl.toHttpUrl().newBuilder()
|
||||
|
||||
// Do traditional search
|
||||
val url = "${MdUtil.baseUrl}/?page=search".toHttpUrl().newBuilder()
|
||||
.addQueryParameter("p", page.toString())
|
||||
.addQueryParameter("title", query.replace(WHITESPACE_REGEX, " "))
|
||||
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is FilterHandler.TextField -> url.addQueryParameter(filter.key, filter.state)
|
||||
is FilterHandler.DemographicList -> {
|
||||
filter.state.forEach { demographic ->
|
||||
if (demographic.state) {
|
||||
demographics.add(demographic.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -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<MangasPage> {
|
||||
suspend fun fetchSimilar(manga: Manga): MangasPage {
|
||||
// Parse the Mangadex id from the URL
|
||||
return Observable.just(MdUtil.getMangaId(manga.url).toLong())
|
||||
.flatMap { mangaId ->
|
||||
Injekt.get<DatabaseHelper>().getSimilar(mangaId).asRxObservable()
|
||||
}.map { similarMangaDb: MangaSimilar? ->
|
||||
if (similarMangaDb != null) {
|
||||
val 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<DatabaseHelper>().getSimilar(mangaId).executeOnIO()
|
||||
return if (similarMangaDb != null) {
|
||||
val similarMangaTitles = similarMangaDb.matched_titles.split(MangaSimilarImpl.DELIMITER)
|
||||
val similarMangaIds = similarMangaDb.matched_ids.split(MangaSimilarImpl.DELIMITER)
|
||||
val similarMangas = similarMangaIds.mapIndexed { index, similarId ->
|
||||
SManga.create().apply {
|
||||
title = similarMangaTitles[index]
|
||||
url = "/manga/$similarId/"
|
||||
thumbnail_url = MdUtil.formThumbUrl(url, useLowQualityCovers)
|
||||
}
|
||||
}
|
||||
MangasPage(similarMangas, false)
|
||||
} else MangasPage(mutableListOf(), false)
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/*
|
||||
* Copyright (C) 2020 The Neko Manga Open Source Project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@Serializable
|
||||
data class ApiChapterSerializer(
|
||||
val data: ChapterPageSerializer
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterPageSerializer(
|
||||
val hash: String,
|
||||
val pages: List<String>,
|
||||
val server: String,
|
||||
val mangaId: Int
|
||||
)
|
@ -1,76 +0,0 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ApiMangaSerializer(
|
||||
val data: DataSerializer,
|
||||
val status: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DataSerializer(
|
||||
val manga: MangaSerializer,
|
||||
val chapters: List<ChapterSerializer>,
|
||||
val groups: List<GroupSerializer>,
|
||||
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaSerializer(
|
||||
val artist: List<String>,
|
||||
val author: List<String>,
|
||||
val mainCover: String,
|
||||
val description: String,
|
||||
val tags: List<Int>,
|
||||
val isHentai: Boolean,
|
||||
val lastChapter: String? = null,
|
||||
val publication: PublicationSerializer? = null,
|
||||
val links: LinksSerializer? = null,
|
||||
val rating: RatingSerializer? = null,
|
||||
val title: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PublicationSerializer(
|
||||
val language: String? = null,
|
||||
val status: Int,
|
||||
val demographic: Int?
|
||||
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class LinksSerializer(
|
||||
val al: String? = null,
|
||||
val amz: String? = null,
|
||||
val ap: String? = null,
|
||||
val engtl: String? = null,
|
||||
val kt: String? = null,
|
||||
val mal: String? = null,
|
||||
val mu: String? = null,
|
||||
val raw: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RatingSerializer(
|
||||
val bayesian: String? = null,
|
||||
val mean: String? = null,
|
||||
val users: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterSerializer(
|
||||
val id: Long,
|
||||
val volume: String? = null,
|
||||
val chapter: String? = null,
|
||||
val title: String? = null,
|
||||
val language: String,
|
||||
val groups: List<Long>,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupSerializer(
|
||||
val id: Long,
|
||||
val name: String? = null
|
||||
)
|
39
app/src/main/java/exh/md/handlers/serializers/Auth.kt
Normal file
39
app/src/main/java/exh/md/handlers/serializers/Auth.kt
Normal file
@ -0,0 +1,39 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Login Request object for Dex Api
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginRequest(val username: String, val password: String)
|
||||
|
||||
/**
|
||||
* Response after login
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginResponse(val result: String, val token: LoginBodyToken)
|
||||
|
||||
/**
|
||||
* Tokens for the logins
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginBodyToken(val session: String, val refresh: String)
|
||||
|
||||
/**
|
||||
* Response after logout
|
||||
*/
|
||||
@Serializable
|
||||
data class LogoutResponse(val result: String)
|
||||
|
||||
/**
|
||||
* Check if session token is valid
|
||||
*/
|
||||
@Serializable
|
||||
data class CheckTokenResponse(val isAuthenticated: Boolean)
|
||||
|
||||
/**
|
||||
* Request to refresh token
|
||||
*/
|
||||
@Serializable
|
||||
data class RefreshTokenRequest(val token: String)
|
@ -0,0 +1,40 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CacheApiMangaSerializer(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val url: String,
|
||||
val description: String,
|
||||
val is_r18: Boolean,
|
||||
val rating: Float,
|
||||
val demographic: List<String>,
|
||||
val content: List<String>,
|
||||
val format: List<String>,
|
||||
val genre: List<String>,
|
||||
val theme: List<String>,
|
||||
val languages: List<String>,
|
||||
val related: List<CacheRelatedSerializer>,
|
||||
val external: MutableMap<String, String>,
|
||||
val last_updated: String,
|
||||
val matches: List<CacheSimilarMatchesSerializer>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CacheRelatedSerializer(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val type: String,
|
||||
val r18: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CacheSimilarMatchesSerializer(
|
||||
val id: Long,
|
||||
val title: String,
|
||||
val score: Float,
|
||||
val r18: Boolean,
|
||||
val languages: List<String>,
|
||||
)
|
@ -0,0 +1,67 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ChapterListResponse(
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val total: Int,
|
||||
val results: List<ChapterResponse>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterResponse(
|
||||
val result: String,
|
||||
val data: NetworkChapter,
|
||||
val relationships: List<Relationships>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NetworkChapter(
|
||||
val id: String,
|
||||
val type: String,
|
||||
val attributes: ChapterAttributes,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ChapterAttributes(
|
||||
val title: String?,
|
||||
val volume: Int?,
|
||||
val chapter: String?,
|
||||
val translatedLanguage: String,
|
||||
val publishAt: String,
|
||||
val data: List<String>,
|
||||
val dataSaver: List<String>,
|
||||
val hash: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AtHomeResponse(
|
||||
val baseUrl: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupListResponse(
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val total: Int,
|
||||
val results: List<GroupResponse>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupResponse(
|
||||
val result: String,
|
||||
val data: GroupData,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupData(
|
||||
val id: String,
|
||||
val attributes: GroupAttributes,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class GroupAttributes(
|
||||
val name: String,
|
||||
)
|
@ -1,14 +0,0 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ApiCovers(
|
||||
val data: List<CoversResult>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CoversResult(
|
||||
val volume: String,
|
||||
val url: String
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FollowsPageSerializer(
|
||||
val code: Int,
|
||||
val data: List<FollowPage>?
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FollowsIndividualSerializer(
|
||||
val code: Int,
|
||||
val data: FollowPage? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FollowPage(
|
||||
val mangaTitle: String,
|
||||
val chapter: String,
|
||||
val followType: Int,
|
||||
val mangaId: Int,
|
||||
val volume: String
|
||||
)
|
@ -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?
|
||||
)
|
@ -56,7 +56,6 @@ data class TitleDetailView(
|
||||
@ProtoNumber(5) val nextTimeStamp: Int = 0,
|
||||
@ProtoNumber(6) val updateTiming: UpdateTiming? = UpdateTiming.DAY,
|
||||
@ProtoNumber(7) val viewingPeriodDescription: String = "",
|
||||
@ProtoNumber(8) val nonAppearanceInfo: String = "",
|
||||
@ProtoNumber(9) val firstChapterList: List<Chapter> = emptyList(),
|
||||
@ProtoNumber(10) val lastChapterList: List<Chapter> = emptyList(),
|
||||
@ProtoNumber(14) val isSimulReleased: Boolean = true,
|
||||
|
@ -0,0 +1,82 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MangaListResponse(
|
||||
val limit: Int,
|
||||
val offset: Int,
|
||||
val total: Int,
|
||||
val results: List<MangaResponse>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MangaResponse(
|
||||
val result: String,
|
||||
val data: NetworkManga,
|
||||
val relationships: List<Relationships>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NetworkManga(val id: String, val type: String, val attributes: NetworkMangaAttributes)
|
||||
|
||||
@Serializable
|
||||
data class NetworkMangaAttributes(
|
||||
val title: Map<String, String>,
|
||||
val altTitles: List<Map<String, String>>,
|
||||
val description: Map<String, String>,
|
||||
val links: Map<String, String>?,
|
||||
val originalLanguage: String,
|
||||
val lastVolume: Int?,
|
||||
val lastChapter: String,
|
||||
val contentRating: String?,
|
||||
val publicationDemographic: String?,
|
||||
val status: String?,
|
||||
val year: Int?,
|
||||
val tags: List<TagsSerializer>
|
||||
// val readingStatus: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TagsSerializer(
|
||||
val id: String,
|
||||
val attributes: TagAttributes
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TagAttributes(
|
||||
val name: Map<String, String>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Relationships(
|
||||
val id: String,
|
||||
val type: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorResponseList(
|
||||
val results: List<AuthorResponse>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorResponse(
|
||||
val result: String,
|
||||
val data: NetworkAuthor,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NetworkAuthor(
|
||||
val id: String,
|
||||
val attributes: AuthorAttributes,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuthorAttributes(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateReadingStatus(
|
||||
val id: String?
|
||||
)
|
@ -0,0 +1,13 @@
|
||||
package exh.md.handlers.serializers
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class NetworkFollowed(
|
||||
val code: Int,
|
||||
val message: String = "",
|
||||
val data: List<FollowedSerializer>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FollowedSerializer(val mangaId: String, val mangaTitle: String, val followType: Int)
|
106
app/src/main/java/exh/md/network/MangaDexLoginHelper.kt
Normal file
106
app/src/main/java/exh/md/network/MangaDexLoginHelper.kt
Normal file
@ -0,0 +1,106 @@
|
||||
package exh.md.network
|
||||
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.mdlist.MdList
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import exh.log.xLogI
|
||||
import exh.md.handlers.serializers.CheckTokenResponse
|
||||
import exh.md.handlers.serializers.LoginBodyToken
|
||||
import exh.md.handlers.serializers.LoginRequest
|
||||
import exh.md.handlers.serializers.LoginResponse
|
||||
import exh.md.handlers.serializers.LogoutResponse
|
||||
import exh.md.handlers.serializers.RefreshTokenRequest
|
||||
import exh.md.utils.MdUtil
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.encodeToString
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class MangaDexLoginHelper(val client: OkHttpClient, val preferences: PreferencesHelper, val mdList: MdList) {
|
||||
suspend fun isAuthenticated(authHeaders: Headers): Boolean {
|
||||
val response = client.newCall(GET(MdUtil.checkTokenUrl, authHeaders, CacheControl.FORCE_NETWORK)).await()
|
||||
val body = response.parseAs<CheckTokenResponse>(MdUtil.jsonParser)
|
||||
return body.isAuthenticated
|
||||
}
|
||||
|
||||
suspend fun refreshToken(authHeaders: Headers): Boolean {
|
||||
val refreshToken = MdUtil.refreshToken(preferences, mdList)
|
||||
if (refreshToken.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
val result = RefreshTokenRequest(refreshToken)
|
||||
val jsonString = MdUtil.jsonParser.encodeToString(result)
|
||||
val postResult = client.newCall(
|
||||
POST(
|
||||
MdUtil.refreshTokenUrl,
|
||||
authHeaders,
|
||||
jsonString.toRequestBody("application/json".toMediaType())
|
||||
)
|
||||
).await()
|
||||
val refresh = runCatching {
|
||||
val jsonResponse = postResult.parseAs<LoginResponse>(MdUtil.jsonParser)
|
||||
preferences.trackToken(mdList).set(MdUtil.jsonParser.encodeToString(jsonResponse.token))
|
||||
}
|
||||
return refresh.isSuccess
|
||||
}
|
||||
|
||||
suspend fun login(
|
||||
username: String,
|
||||
password: String,
|
||||
): LoginResult {
|
||||
return withIOContext {
|
||||
val loginRequest = LoginRequest(username, password)
|
||||
|
||||
val jsonString = MdUtil.jsonParser.encodeToString(loginRequest)
|
||||
|
||||
val postResult = client.newCall(
|
||||
POST(
|
||||
MdUtil.loginUrl,
|
||||
Headers.Builder().build(),
|
||||
jsonString.toRequestBody("application/json".toMediaType())
|
||||
)
|
||||
).await()
|
||||
|
||||
// if it fails to parse then login failed
|
||||
val loginResponse = try {
|
||||
postResult.parseAs<LoginResponse>(MdUtil.jsonParser)
|
||||
} catch (e: SerializationException) {
|
||||
null
|
||||
}
|
||||
|
||||
if (postResult.code == 200 && loginResponse != null) {
|
||||
LoginResult.Success(loginResponse.token)
|
||||
} else {
|
||||
LoginResult.Failure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class LoginResult {
|
||||
object Failure : LoginResult()
|
||||
data class Success(val token: LoginBodyToken) : LoginResult()
|
||||
}
|
||||
|
||||
suspend fun login(): LoginResult {
|
||||
val username = preferences.trackUsername(mdList)
|
||||
val password = preferences.trackPassword(mdList)
|
||||
if (username.isNullOrBlank() || password.isNullOrBlank()) {
|
||||
xLogI("No username or password stored, can't login")
|
||||
return LoginResult.Failure
|
||||
}
|
||||
return login(username, password)
|
||||
}
|
||||
|
||||
suspend fun logout(authHeaders: Headers): Boolean {
|
||||
val response = client.newCall(GET(MdUtil.logoutUrl, authHeaders, CacheControl.FORCE_NETWORK)).await()
|
||||
val body = response.parseAs<LogoutResponse>(MdUtil.jsonParser)
|
||||
return body.result == "ok"
|
||||
}
|
||||
}
|
3
app/src/main/java/exh/md/network/NoSessionException.kt
Normal file
3
app/src/main/java/exh/md/network/NoSessionException.kt
Normal file
@ -0,0 +1,3 @@
|
||||
package exh.md.network
|
||||
|
||||
class NoSessionException : IllegalArgumentException("Session token does not exist")
|
73
app/src/main/java/exh/md/network/TokenAuthenticator.kt
Normal file
73
app/src/main/java/exh/md/network/TokenAuthenticator.kt
Normal file
@ -0,0 +1,73 @@
|
||||
package exh.md.network
|
||||
|
||||
import exh.log.xLogD
|
||||
import exh.log.xLogI
|
||||
import exh.md.utils.MdUtil
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
|
||||
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
|
||||
override fun authenticate(route: Route?, response: Response): Request? {
|
||||
xLogI("Detected Auth error ${response.code} on ${response.request.url}")
|
||||
val token = refreshToken(loginHelper)
|
||||
if (token.isEmpty()) {
|
||||
return null
|
||||
throw Exception("Unable to authenticate request, please re login")
|
||||
}
|
||||
return response.request.newBuilder().header("Authorization", token).build()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun refreshToken(loginHelper: MangaDexLoginHelper): String {
|
||||
var validated = false
|
||||
|
||||
runBlocking {
|
||||
val checkToken = try {
|
||||
loginHelper.isAuthenticated(
|
||||
MdUtil.getAuthHeaders(
|
||||
Headers.Builder().build(),
|
||||
loginHelper.preferences,
|
||||
loginHelper.mdList
|
||||
)
|
||||
)
|
||||
} catch (e: NoSessionException) {
|
||||
xLogD("Session token does not exist")
|
||||
false
|
||||
}
|
||||
|
||||
if (checkToken) {
|
||||
xLogI("Token is valid, other thread must have refreshed it")
|
||||
validated = true
|
||||
}
|
||||
if (validated.not()) {
|
||||
xLogI("Token is invalid trying to refresh")
|
||||
validated = loginHelper.refreshToken(
|
||||
MdUtil.getAuthHeaders(
|
||||
Headers.Builder().build(), loginHelper.preferences, loginHelper.mdList
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (validated.not()) {
|
||||
xLogI("Did not refresh token, trying to login")
|
||||
val loginResult = loginHelper.login()
|
||||
validated = if (loginResult is MangaDexLoginHelper.LoginResult.Success) {
|
||||
MdUtil.updateLoginToken(
|
||||
loginResult.token,
|
||||
loginHelper.preferences,
|
||||
loginHelper.mdList
|
||||
)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
}
|
||||
return when {
|
||||
validated -> "bearer: ${MdUtil.sessionToken(loginHelper.preferences, loginHelper.mdList)!!}"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
import eu.kanade.tachiyomi.util.lang.runAsObservable
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
@ -15,7 +16,7 @@ import rx.schedulers.Schedulers
|
||||
class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
|
||||
|
||||
override fun requestNext(): Observable<MangasPage> {
|
||||
return source.fetchMangaSimilar(manga)
|
||||
return runAsObservable({ source.fetchMangaSimilar(manga) })
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> {
|
||||
if (scanlators.isBlank()) return emptyList()
|
||||
fun getScanlators(scanlators: String?): List<String> {
|
||||
if (scanlators.isNullOrBlank()) return emptyList()
|
||||
return scanlators.split(scanlatorSeparator).distinct()
|
||||
}
|
||||
|
||||
@ -234,7 +259,6 @@ class MdUtil {
|
||||
fun getMissingChapterCount(chapters: List<SChapter>, mangaStatus: Int): String? {
|
||||
if (mangaStatus == SManga.COMPLETED) return null
|
||||
|
||||
// TODO
|
||||
val remove0ChaptersFromCount = chapters.distinctBy {
|
||||
/*if (it.chapter_txt.isNotEmpty()) {
|
||||
it.vol + it.chapter_txt
|
||||
@ -257,15 +281,63 @@ class MdUtil {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getEnabledMangaDex(preferences: PreferencesHelper = Injekt.get(), sourceManager: SourceManager = Injekt.get()): MangaDex? {
|
||||
fun atHomeUrlHostUrl(requestUrl: String, client: OkHttpClient): String {
|
||||
val atHomeRequest = GET(requestUrl)
|
||||
val atHomeResponse = client.newCall(atHomeRequest).execute()
|
||||
return atHomeResponse.parseAs<AtHomeResponse>(jsonParser).baseUrl
|
||||
}
|
||||
|
||||
val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss+SSS", Locale.US)
|
||||
.apply { timeZone = TimeZone.getTimeZone("UTC") }
|
||||
|
||||
fun parseDate(dateAsString: String): Long =
|
||||
dateFormatter.parse(dateAsString)?.time ?: 0
|
||||
|
||||
fun createMangaEntry(json: MangaResponse, lang: String, lowQualityCovers: Boolean): MangaInfo {
|
||||
val key = "/manga/" + json.data.id
|
||||
return MangaInfo(
|
||||
key = key,
|
||||
title = cleanString(json.data.attributes.title[lang] ?: json.data.attributes.title["en"]!!),
|
||||
cover = formThumbUrl(key, lowQualityCovers)
|
||||
)
|
||||
}
|
||||
|
||||
fun sessionToken(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
|
||||
try {
|
||||
jsonParser.decodeFromString<LoginBodyToken>(it)
|
||||
} catch (e: SerializationException) {
|
||||
xLogD("Unable to load session token")
|
||||
null
|
||||
}
|
||||
}?.session
|
||||
|
||||
fun refreshToken(preferences: PreferencesHelper, mdList: MdList) = preferences.trackToken(mdList).get().nullIfBlank()?.let {
|
||||
try {
|
||||
jsonParser.decodeFromString<LoginBodyToken>(it)
|
||||
} catch (e: SerializationException) {
|
||||
xLogD("Unable to load session token")
|
||||
null
|
||||
}
|
||||
}?.refresh
|
||||
|
||||
fun updateLoginToken(token: LoginBodyToken, preferences: PreferencesHelper, mdList: MdList) {
|
||||
preferences.trackToken(mdList).set(jsonParser.encodeToString(token))
|
||||
}
|
||||
|
||||
fun getAuthHeaders(headers: Headers, preferences: PreferencesHelper, mdList: MdList) =
|
||||
headers.newBuilder().add("Authorization", "Bearer ${sessionToken(preferences, mdList) ?: throw NoSessionException()}").build()
|
||||
|
||||
fun getEnabledMangaDex(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): MangaDex? {
|
||||
return getEnabledMangaDexs(preferences, sourceManager).let { mangadexs ->
|
||||
preferences.preferredMangaDexId().get().toLongOrNull()?.nullIfZero()?.let { preferredMangaDexId ->
|
||||
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<MangaDex> {
|
||||
fun getEnabledMangaDexs(preferences: PreferencesHelper, sourceManager: SourceManager = Injekt.get()): List<MangaDex> {
|
||||
val languages = preferences.enabledLanguages().get()
|
||||
val disabledSourceIds = preferences.disabledSources().get()
|
||||
|
||||
@ -275,54 +347,5 @@ class MdUtil {
|
||||
.filter { it.lang in languages }
|
||||
.filterNot { it.id.toString() in disabledSourceIds }
|
||||
}
|
||||
|
||||
fun mapMdIdToMangaUrl(id: Int) = "/manga/$id/"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the chapter without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the chapter.
|
||||
*/
|
||||
fun SChapter.setMDUrlWithoutDomain(url: String) {
|
||||
this.url = getMDUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the url of the manga without the scheme and domain. It saves some redundancy from
|
||||
* database and the urls could still work after a domain change.
|
||||
*
|
||||
* @param url the full url to the manga.
|
||||
*/
|
||||
fun SManga.setMDUrlWithoutDomain(url: String) {
|
||||
this.url = getMDUrlWithoutDomain(url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the url of the given string without the scheme and domain.
|
||||
*
|
||||
* @param orig the full url.
|
||||
*/
|
||||
private fun getMDUrlWithoutDomain(orig: String): String {
|
||||
return try {
|
||||
val uri = orig.toUri()
|
||||
var out = uri.path.orEmpty()
|
||||
if (uri.query != null) {
|
||||
out += "?" + uri.query
|
||||
}
|
||||
if (uri.fragment != null) {
|
||||
out += "#" + uri.fragment
|
||||
}
|
||||
out
|
||||
} catch (e: URISyntaxException) {
|
||||
orig
|
||||
}
|
||||
}
|
||||
|
||||
fun Chapter.scanlatorList(): List<String> {
|
||||
return this.scanlator?.let {
|
||||
MdUtil.getScanlators(it)
|
||||
} ?: listOf("No scanlator")
|
||||
}
|
||||
|
@ -1,65 +1,56 @@
|
||||
package exh.metadata.metadata
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.R
|
||||
import exh.md.utils.MdUtil
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.util.nullIfBlank
|
||||
import kotlinx.serialization.Serializable
|
||||
import tachiyomi.source.model.MangaInfo
|
||||
|
||||
@Serializable
|
||||
class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
||||
var mdId: String? = null
|
||||
var mdUuid: String? = null
|
||||
|
||||
var mdUrl: String? = null
|
||||
// var mdUrl: String? = null
|
||||
|
||||
var thumbnail_url: String? = null
|
||||
var cover: String? = null
|
||||
|
||||
var title: String? by titleDelegate(TITLE_TYPE_MAIN)
|
||||
var altTitles: List<String>? = null
|
||||
|
||||
var description: String? = null
|
||||
|
||||
var author: String? = null
|
||||
var artist: String? = null
|
||||
var authors: List<String>? = null
|
||||
|
||||
var lang_flag: String? = null
|
||||
var langFlag: String? = null
|
||||
|
||||
var last_chapter_number: Int? = null
|
||||
var rating: String? = null
|
||||
var users: String? = null
|
||||
var lastChapterNumber: Int? = null
|
||||
// var rating: String? = null
|
||||
// var users: String? = null
|
||||
|
||||
var anilist_id: String? = null
|
||||
var kitsu_id: String? = null
|
||||
var my_anime_list_id: String? = null
|
||||
var manga_updates_id: String? = null
|
||||
var anime_planet_id: String? = null
|
||||
var anilistId: String? = null
|
||||
var kitsuId: String? = null
|
||||
var myAnimeListId: String? = null
|
||||
var mangaUpdatesId: String? = null
|
||||
var animePlanetId: String? = null
|
||||
|
||||
var status: Int? = null
|
||||
|
||||
var missing_chapters: String? = null
|
||||
// var missing_chapters: String? = null
|
||||
|
||||
var follow_status: Int? = null
|
||||
var followStatus: Int? = null
|
||||
|
||||
var maxChapterNumber: Int? = null
|
||||
// var maxChapterNumber: Int? = null
|
||||
|
||||
override fun createMangaInfo(manga: MangaInfo): MangaInfo {
|
||||
val key = mdUrl?.let {
|
||||
try {
|
||||
val uri = it.toUri()
|
||||
val out = uri.path!!.removePrefix("/api")
|
||||
out + if (out.endsWith("/")) "" else "/"
|
||||
} catch (e: Exception) {
|
||||
it
|
||||
}
|
||||
}
|
||||
val key = mdUuid?.let { "/manga/$it" }
|
||||
|
||||
val title = title
|
||||
|
||||
val cover = thumbnail_url
|
||||
val cover = cover ?: manga.cover.nullIfBlank() ?: "https://i.imgur.com/6TrIues.jpg" // cover
|
||||
|
||||
val author = author
|
||||
|
||||
val artist = artist
|
||||
val author = authors?.joinToString()?.let { MdUtil.cleanString(it) }
|
||||
|
||||
val status = status
|
||||
|
||||
@ -72,7 +63,6 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
||||
title = title ?: manga.title,
|
||||
cover = cover ?: manga.cover,
|
||||
author = author ?: manga.author,
|
||||
artist = artist ?: manga.artist,
|
||||
status = status ?: manga.status,
|
||||
genres = genres,
|
||||
description = description ?: manga.description
|
||||
@ -81,29 +71,30 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
|
||||
|
||||
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
|
||||
val pairs = mutableListOf<Pair<String, String>>()
|
||||
mdId?.let { pairs += context.getString(R.string.id) to it }
|
||||
mdUrl?.let { pairs += context.getString(R.string.url) to it }
|
||||
thumbnail_url?.let { pairs += context.getString(R.string.thumbnail_url) to it }
|
||||
mdUuid?.let { pairs += context.getString(R.string.id) to it }
|
||||
// mdUrl?.let { pairs += context.getString(R.string.url) to it }
|
||||
cover?.let { pairs += context.getString(R.string.thumbnail_url) to it }
|
||||
title?.let { pairs += context.getString(R.string.title) to it }
|
||||
author?.let { pairs += context.getString(R.string.author) to it }
|
||||
artist?.let { pairs += context.getString(R.string.artist) to it }
|
||||
lang_flag?.let { pairs += context.getString(R.string.language) to it }
|
||||
last_chapter_number?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() }
|
||||
rating?.let { pairs += context.getString(R.string.average_rating) to it }
|
||||
users?.let { pairs += context.getString(R.string.total_ratings) to it }
|
||||
authors?.let { pairs += context.getString(R.string.author) to it.joinToString() }
|
||||
// artist?.let { pairs += context.getString(R.string.artist) to it }
|
||||
langFlag?.let { pairs += context.getString(R.string.language) to it }
|
||||
lastChapterNumber?.let { pairs += context.getString(R.string.last_chapter_number) to it.toString() }
|
||||
// rating?.let { pairs += context.getString(R.string.average_rating) to it }
|
||||
// users?.let { pairs += context.getString(R.string.total_ratings) to it }
|
||||
status?.let { pairs += context.getString(R.string.status) to it.toString() }
|
||||
missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it }
|
||||
follow_status?.let { pairs += context.getString(R.string.follow_status) to it.toString() }
|
||||
anilist_id?.let { pairs += context.getString(R.string.anilist_id) to it }
|
||||
kitsu_id?.let { pairs += context.getString(R.string.kitsu_id) to it }
|
||||
my_anime_list_id?.let { pairs += context.getString(R.string.mal_id) to it }
|
||||
manga_updates_id?.let { pairs += context.getString(R.string.manga_updates_id) to it }
|
||||
anime_planet_id?.let { pairs += context.getString(R.string.anime_planet_id) to it }
|
||||
// missing_chapters?.let { pairs += context.getString(R.string.missing_chapters) to it }
|
||||
followStatus?.let { pairs += context.getString(R.string.follow_status) to it.toString() }
|
||||
anilistId?.let { pairs += context.getString(R.string.anilist_id) to it }
|
||||
kitsuId?.let { pairs += context.getString(R.string.kitsu_id) to it }
|
||||
myAnimeListId?.let { pairs += context.getString(R.string.mal_id) to it }
|
||||
mangaUpdatesId?.let { pairs += context.getString(R.string.manga_updates_id) to it }
|
||||
animePlanetId?.let { pairs += context.getString(R.string.anime_planet_id) to it }
|
||||
return pairs
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TITLE_TYPE_MAIN = 0
|
||||
private const val TITLE_TYPE_ALT_TITLE = 1
|
||||
|
||||
const val TAG_TYPE_DEFAULT = 0
|
||||
}
|
||||
|
@ -48,8 +48,6 @@ abstract class RaisedSearchMetadata {
|
||||
@Transient
|
||||
val titles = mutableListOf<RaisedTitle>()
|
||||
|
||||
var filteredScanlators: String? = null
|
||||
|
||||
fun getTitleOfType(type: Int): String? = titles.find { it.type == type }?.title
|
||||
|
||||
fun replaceTitleOfType(type: Int, newTitle: String?) {
|
||||
|
@ -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)
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user