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