mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Add MangaUpdates as a tracker (#7170)
* Add MangaUpdates as a tracker - jobobby04 co-authored for suggestion in BackupTracking.kt Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com> * Changes from code review Co-authored-by: arkon <arkon@users.noreply.github.com> Co-authored-by: jobobby04 <jobobby04@users.noreply.github.com> Co-authored-by: arkon <arkon@users.noreply.github.com>
This commit is contained in:
		| @@ -12,7 +12,7 @@ data class BackupTracking( | ||||
|     @ProtoNumber(1) var syncId: Int, | ||||
|     // LibraryId is not null in 1.x | ||||
|     @ProtoNumber(2) var libraryId: Long, | ||||
|     @ProtoNumber(3) var mediaId: Int = 0, | ||||
|     @Deprecated("Use mediaId instead", level = DeprecationLevel.WARNING) @ProtoNumber(3) var mediaIdInt: Int = 0, | ||||
|     // trackingUrl is called mediaUrl in 1.x | ||||
|     @ProtoNumber(4) var trackingUrl: String = "", | ||||
|     @ProtoNumber(5) var title: String = "", | ||||
| @@ -25,11 +25,17 @@ data class BackupTracking( | ||||
|     @ProtoNumber(10) var startedReadingDate: Long = 0, | ||||
|     // finishedReadingDate is called endReadTime in 1.x | ||||
|     @ProtoNumber(11) var finishedReadingDate: Long = 0, | ||||
|     @ProtoNumber(100) var mediaId: Long = 0, | ||||
| ) { | ||||
|  | ||||
|     fun getTrackingImpl(): TrackImpl { | ||||
|         return TrackImpl().apply { | ||||
|             sync_id = this@BackupTracking.syncId | ||||
|             media_id = this@BackupTracking.mediaId | ||||
|             media_id = if (this@BackupTracking.mediaIdInt != 0) { | ||||
|                 this@BackupTracking.mediaIdInt.toLong() | ||||
|             } else { | ||||
|                 this@BackupTracking.mediaId | ||||
|             } | ||||
|             library_id = this@BackupTracking.libraryId | ||||
|             title = this@BackupTracking.title | ||||
|             last_chapter_read = this@BackupTracking.lastChapterRead | ||||
|   | ||||
| @@ -45,7 +45,7 @@ open class TrackBaseSerializer<T : Track> : KSerializer<T> { | ||||
|             val jsonObject = decoder.decodeJsonElement().jsonObject | ||||
|             title = jsonObject[TITLE]!!.jsonPrimitive.content | ||||
|             sync_id = jsonObject[SYNC]!!.jsonPrimitive.int | ||||
|             media_id = jsonObject[MEDIA]!!.jsonPrimitive.int | ||||
|             media_id = jsonObject[MEDIA]!!.jsonPrimitive.long | ||||
|             library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long | ||||
|             last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float | ||||
|             tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content | ||||
|   | ||||
| @@ -68,7 +68,7 @@ class TrackGetResolver : DefaultGetResolver<Track>() { | ||||
|         id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID)) | ||||
|         manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID)) | ||||
|         sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID)) | ||||
|         media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID)) | ||||
|         media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID)) | ||||
|         library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID)) | ||||
|         title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE)) | ||||
|         last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ)) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ interface Track : Serializable { | ||||
|  | ||||
|     var sync_id: Int | ||||
|  | ||||
|     var media_id: Int | ||||
|     var media_id: Long | ||||
|  | ||||
|     var library_id: Long? | ||||
|  | ||||
|   | ||||
| @@ -8,7 +8,7 @@ class TrackImpl : Track { | ||||
|  | ||||
|     override var sync_id: Int = 0 | ||||
|  | ||||
|     override var media_id: Int = 0 | ||||
|     override var media_id: Long = 0 | ||||
|  | ||||
|     override var library_id: Long? = null | ||||
|  | ||||
| @@ -42,7 +42,7 @@ class TrackImpl : Track { | ||||
|     override fun hashCode(): Int { | ||||
|         var result = (manga_id xor manga_id.ushr(32)).toInt() | ||||
|         result = 31 * result + sync_id | ||||
|         result = 31 * result + media_id | ||||
|         result = 31 * result + media_id.toInt() | ||||
|         return result | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.track.anilist.Anilist | ||||
| import eu.kanade.tachiyomi.data.track.bangumi.Bangumi | ||||
| import eu.kanade.tachiyomi.data.track.kitsu.Kitsu | ||||
| import eu.kanade.tachiyomi.data.track.komga.Komga | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates | ||||
| import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList | ||||
| import eu.kanade.tachiyomi.data.track.shikimori.Shikimori | ||||
|  | ||||
| @@ -17,6 +18,7 @@ class TrackManager(context: Context) { | ||||
|         const val SHIKIMORI = 4 | ||||
|         const val BANGUMI = 5 | ||||
|         const val KOMGA = 6 | ||||
|         const val MANGA_UPDATES = 7 | ||||
|     } | ||||
|  | ||||
|     val myAnimeList = MyAnimeList(context, MYANIMELIST) | ||||
| @@ -31,7 +33,9 @@ class TrackManager(context: Context) { | ||||
|  | ||||
|     val komga = Komga(context, KOMGA) | ||||
|  | ||||
|     val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga) | ||||
|     val mangaUpdates = MangaUpdates(context, MANGA_UPDATES) | ||||
|  | ||||
|     val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates) | ||||
|  | ||||
|     fun getService(id: Int) = services.find { it.id == id } | ||||
|  | ||||
|   | ||||
| @@ -268,7 +268,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|  | ||||
|     private fun jsonToALManga(struct: JsonObject): ALManga { | ||||
|         return ALManga( | ||||
|             struct["id"]!!.jsonPrimitive.int, | ||||
|             struct["id"]!!.jsonPrimitive.long, | ||||
|             struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content, | ||||
|             struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content, | ||||
|             struct["description"]!!.jsonPrimitive.contentOrNull, | ||||
| @@ -329,7 +329,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) { | ||||
|         private const val baseUrl = "https://anilist.co/api/v2/" | ||||
|         private const val baseMangaUrl = "https://anilist.co/manga/" | ||||
|  | ||||
|         fun mangaUrl(mediaId: Int): String { | ||||
|         fun mangaUrl(mediaId: Long): String { | ||||
|             return baseMangaUrl + mediaId | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ import java.text.SimpleDateFormat | ||||
| import java.util.Locale | ||||
|  | ||||
| data class ALManga( | ||||
|     val media_id: Int, | ||||
|     val media_id: Long, | ||||
|     val title_user_pref: String, | ||||
|     val image_url_lge: String, | ||||
|     val description: String?, | ||||
|   | ||||
| @@ -18,6 +18,7 @@ import kotlinx.serialization.json.int | ||||
| import kotlinx.serialization.json.jsonArray | ||||
| import kotlinx.serialization.json.jsonObject | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import kotlinx.serialization.json.long | ||||
| import okhttp3.CacheControl | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.OkHttpClient | ||||
| @@ -106,7 +107,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept | ||||
|             0 | ||||
|         } | ||||
|         return TrackSearch.create(TrackManager.BANGUMI).apply { | ||||
|             media_id = obj["id"]!!.jsonPrimitive.int | ||||
|             media_id = obj["id"]!!.jsonPrimitive.long | ||||
|             title = obj["name_cn"]!!.jsonPrimitive.content | ||||
|             cover_url = coverUrl | ||||
|             summary = obj["name"]!!.jsonPrimitive.content | ||||
|   | ||||
| @@ -11,10 +11,10 @@ import eu.kanade.tachiyomi.network.parseAs | ||||
| import eu.kanade.tachiyomi.util.lang.withIOContext | ||||
| import kotlinx.serialization.json.JsonObject | ||||
| import kotlinx.serialization.json.buildJsonObject | ||||
| import kotlinx.serialization.json.int | ||||
| import kotlinx.serialization.json.jsonArray | ||||
| import kotlinx.serialization.json.jsonObject | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import kotlinx.serialization.json.long | ||||
| import kotlinx.serialization.json.put | ||||
| import kotlinx.serialization.json.putJsonObject | ||||
| import okhttp3.FormBody | ||||
| @@ -70,7 +70,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|                 .await() | ||||
|                 .parseAs<JsonObject>() | ||||
|                 .let { | ||||
|                     track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.int | ||||
|                     track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long | ||||
|                     track | ||||
|                 } | ||||
|         } | ||||
| @@ -241,7 +241,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) | ||||
|         private const val algoliaFilter = | ||||
|             "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D" | ||||
|  | ||||
|         fun mangaUrl(remoteId: Int): String { | ||||
|         fun mangaUrl(remoteId: Long): String { | ||||
|             return baseMangaUrl + remoteId | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -10,12 +10,13 @@ import kotlinx.serialization.json.int | ||||
| import kotlinx.serialization.json.intOrNull | ||||
| import kotlinx.serialization.json.jsonObject | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import kotlinx.serialization.json.long | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
|  | ||||
| class KitsuSearchManga(obj: JsonObject) { | ||||
|     val id = obj["id"]!!.jsonPrimitive.int | ||||
|     val id = obj["id"]!!.jsonPrimitive.long | ||||
|     private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content | ||||
|     private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull | ||||
|     val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull | ||||
| @@ -60,7 +61,7 @@ class KitsuLibManga(obj: JsonObject, manga: JsonObject) { | ||||
|     private val startDate = manga["attributes"]!!.jsonObject["startDate"]?.jsonPrimitive?.contentOrNull.orEmpty() | ||||
|     private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull | ||||
|     private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull | ||||
|     private val libraryId = obj["id"]!!.jsonPrimitive.int | ||||
|     private val libraryId = obj["id"]!!.jsonPrimitive.long | ||||
|     val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content | ||||
|     private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull | ||||
|     val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.Color | ||||
| import androidx.annotation.StringRes | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.dto.copyTo | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.dto.toTrackSearch | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
|  | ||||
| class MangaUpdates(private val context: Context, id: Int) : TrackService(id) { | ||||
|  | ||||
|     companion object { | ||||
|         const val READING_LIST = 0 | ||||
|         const val WISH_LIST = 1 | ||||
|         const val COMPLETE_LIST = 2 | ||||
|         const val UNFINISHED_LIST = 3 | ||||
|         const val ON_HOLD_LIST = 4 | ||||
|     } | ||||
|  | ||||
|     private val interceptor by lazy { MangaUpdatesInterceptor(this) } | ||||
|  | ||||
|     private val api by lazy { MangaUpdatesApi(interceptor, client) } | ||||
|  | ||||
|     @StringRes | ||||
|     override fun nameRes(): Int = R.string.tracker_manga_updates | ||||
|  | ||||
|     override fun getLogo(): Int = R.drawable.ic_manga_updates | ||||
|  | ||||
|     override fun getLogoColor(): Int = Color.rgb(146, 160, 173) | ||||
|  | ||||
|     override fun getStatusList(): List<Int> { | ||||
|         return listOf(READING_LIST, COMPLETE_LIST, ON_HOLD_LIST, UNFINISHED_LIST, WISH_LIST) | ||||
|     } | ||||
|  | ||||
|     override fun getStatus(status: Int): String = with(context) { | ||||
|         when (status) { | ||||
|             READING_LIST -> getString(R.string.reading_list) | ||||
|             WISH_LIST -> getString(R.string.wish_list) | ||||
|             COMPLETE_LIST -> getString(R.string.complete_list) | ||||
|             ON_HOLD_LIST -> getString(R.string.on_hold_list) | ||||
|             UNFINISHED_LIST -> getString(R.string.unfinished_list) | ||||
|             else -> "" | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override fun getReadingStatus(): Int = READING_LIST | ||||
|  | ||||
|     override fun getRereadingStatus(): Int = -1 | ||||
|  | ||||
|     override fun getCompletionStatus(): Int = COMPLETE_LIST | ||||
|  | ||||
|     override fun getScoreList(): List<String> = (0..10).map(Int::toString) | ||||
|  | ||||
|     override fun displayScore(track: Track): String = track.score.toInt().toString() | ||||
|  | ||||
|     override suspend fun update(track: Track, didReadChapter: Boolean): Track { | ||||
|         api.updateSeriesListItem(track) | ||||
|         return track | ||||
|     } | ||||
|  | ||||
|     override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { | ||||
|         return try { | ||||
|             val (series, rating) = api.getSeriesListItem(track) | ||||
|             series.copyTo(track) | ||||
|             rating?.copyTo(track) ?: track | ||||
|         } catch (e: Exception) { | ||||
|             api.addSeriesToList(track, hasReadChapters) | ||||
|             track | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     override suspend fun search(query: String): List<TrackSearch> { | ||||
|         return api.search(query) | ||||
|             .map { | ||||
|                 it.toTrackSearch(id) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     override suspend fun refresh(track: Track): Track { | ||||
|         val (series, rating) = api.getSeriesListItem(track) | ||||
|         series.copyTo(track) | ||||
|         return rating?.copyTo(track) ?: track | ||||
|     } | ||||
|  | ||||
|     override suspend fun login(username: String, password: String) { | ||||
|         val authenticated = api.authenticate(username, password) ?: throw Throwable("Unable to login") | ||||
|         saveCredentials(authenticated.uid.toString(), authenticated.sessionToken) | ||||
|         interceptor.newAuth(authenticated.sessionToken) | ||||
|     } | ||||
|  | ||||
|     fun restoreSession(): String? { | ||||
|         return preferences.trackPassword(this) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,189 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.WISH_LIST | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Context | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.dto.ListItem | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Rating | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.dto.Record | ||||
| import eu.kanade.tachiyomi.network.DELETE | ||||
| import eu.kanade.tachiyomi.network.GET | ||||
| import eu.kanade.tachiyomi.network.POST | ||||
| import eu.kanade.tachiyomi.network.PUT | ||||
| import eu.kanade.tachiyomi.network.await | ||||
| import eu.kanade.tachiyomi.network.parseAs | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import kotlinx.serialization.json.Json | ||||
| import kotlinx.serialization.json.JsonObject | ||||
| import kotlinx.serialization.json.addJsonObject | ||||
| import kotlinx.serialization.json.buildJsonArray | ||||
| import kotlinx.serialization.json.buildJsonObject | ||||
| import kotlinx.serialization.json.decodeFromJsonElement | ||||
| import kotlinx.serialization.json.jsonArray | ||||
| import kotlinx.serialization.json.jsonObject | ||||
| import kotlinx.serialization.json.put | ||||
| import kotlinx.serialization.json.putJsonObject | ||||
| import logcat.LogPriority | ||||
| import okhttp3.MediaType.Companion.toMediaType | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.RequestBody.Companion.toRequestBody | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class MangaUpdatesApi( | ||||
|     interceptor: MangaUpdatesInterceptor, | ||||
|     private val client: OkHttpClient, | ||||
| ) { | ||||
|     private val baseUrl = "https://api.mangaupdates.com" | ||||
|     private val contentType = "application/vnd.api+json".toMediaType() | ||||
|  | ||||
|     private val json by injectLazy<Json>() | ||||
|  | ||||
|     private val authClient by lazy { | ||||
|         client.newBuilder() | ||||
|             .addInterceptor(interceptor) | ||||
|             .build() | ||||
|     } | ||||
|  | ||||
|     suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> { | ||||
|         val listItem = | ||||
|             authClient.newCall( | ||||
|                 GET( | ||||
|                     url = "$baseUrl/v1/lists/series/${track.media_id}", | ||||
|                 ), | ||||
|             ) | ||||
|                 .await() | ||||
|                 .parseAs<ListItem>() | ||||
|  | ||||
|         val rating = getSeriesRating(track) | ||||
|  | ||||
|         return listItem to rating | ||||
|     } | ||||
|  | ||||
|     suspend fun addSeriesToList(track: Track, hasReadChapters: Boolean) { | ||||
|         val status = if (hasReadChapters) READING_LIST else WISH_LIST | ||||
|         val body = buildJsonArray { | ||||
|             addJsonObject { | ||||
|                 putJsonObject("series") { | ||||
|                     put("id", track.media_id) | ||||
|                 } | ||||
|                 put("list_id", status) | ||||
|             } | ||||
|         } | ||||
|         authClient.newCall( | ||||
|             POST( | ||||
|                 url = "$baseUrl/v1/lists/series", | ||||
|                 body = body.toString().toRequestBody(contentType), | ||||
|             ), | ||||
|         ) | ||||
|             .await() | ||||
|             .let { | ||||
|                 if (it.code == 200) { | ||||
|                     track.status = status | ||||
|                     track.last_chapter_read = 1f | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     suspend fun updateSeriesListItem(track: Track) { | ||||
|         val body = buildJsonArray { | ||||
|             addJsonObject { | ||||
|                 putJsonObject("series") { | ||||
|                     put("id", track.media_id) | ||||
|                 } | ||||
|                 put("list_id", track.status) | ||||
|                 putJsonObject("status") { | ||||
|                     put("chapter", track.last_chapter_read.toInt()) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         authClient.newCall( | ||||
|             POST( | ||||
|                 url = "$baseUrl/v1/lists/series/update", | ||||
|                 body = body.toString().toRequestBody(contentType), | ||||
|             ), | ||||
|         ) | ||||
|             .await() | ||||
|  | ||||
|         updateSeriesRating(track) | ||||
|     } | ||||
|  | ||||
|     suspend fun getSeriesRating(track: Track): Rating? { | ||||
|         return try { | ||||
|             authClient.newCall( | ||||
|                 GET( | ||||
|                     url = "$baseUrl/v1/series/${track.media_id}/rating", | ||||
|                 ), | ||||
|             ) | ||||
|                 .await() | ||||
|                 .parseAs<Rating>() | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun updateSeriesRating(track: Track) { | ||||
|         if (track.score != 0f) { | ||||
|             val body = buildJsonObject { | ||||
|                 put("rating", track.score.toInt()) | ||||
|             } | ||||
|             authClient.newCall( | ||||
|                 PUT( | ||||
|                     url = "$baseUrl/v1/series/${track.media_id}/rating", | ||||
|                     body = body.toString().toRequestBody(contentType), | ||||
|                 ), | ||||
|             ) | ||||
|                 .await() | ||||
|         } else { | ||||
|             authClient.newCall( | ||||
|                 DELETE( | ||||
|                     url = "$baseUrl/v1/series/${track.media_id}/rating", | ||||
|                 ), | ||||
|             ) | ||||
|                 .await() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun search(query: String): List<Record> { | ||||
|         val body = buildJsonObject { | ||||
|             put("search", query) | ||||
|         } | ||||
|         return client.newCall( | ||||
|             POST( | ||||
|                 url = "$baseUrl/v1/series/search", | ||||
|                 body = body.toString().toRequestBody(contentType), | ||||
|             ), | ||||
|         ) | ||||
|             .await() | ||||
|             .parseAs<JsonObject>() | ||||
|             .let { obj -> | ||||
|                 obj["results"]?.jsonArray?.map { element -> | ||||
|                     json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!) | ||||
|                 } | ||||
|             } | ||||
|             .orEmpty() | ||||
|     } | ||||
|  | ||||
|     suspend fun authenticate(username: String, password: String): Context? { | ||||
|         val body = buildJsonObject { | ||||
|             put("username", username) | ||||
|             put("password", password) | ||||
|         } | ||||
|         return client.newCall( | ||||
|             PUT( | ||||
|                 url = "$baseUrl/v1/account/login", | ||||
|                 body = body.toString().toRequestBody(contentType), | ||||
|             ), | ||||
|         ) | ||||
|             .await() | ||||
|             .parseAs<JsonObject>() | ||||
|             .let { obj -> | ||||
|                 try { | ||||
|                     json.decodeFromJsonElement<Context>(obj["context"]!!) | ||||
|                 } catch (e: Exception) { | ||||
|                     logcat(LogPriority.ERROR, e) | ||||
|                     null | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,29 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates | ||||
|  | ||||
| import okhttp3.Interceptor | ||||
| import okhttp3.Response | ||||
| import java.io.IOException | ||||
|  | ||||
| class MangaUpdatesInterceptor( | ||||
|     mangaUpdates: MangaUpdates, | ||||
| ) : Interceptor { | ||||
|  | ||||
|     private var token: String? = mangaUpdates.restoreSession() | ||||
|  | ||||
|     override fun intercept(chain: Interceptor.Chain): Response { | ||||
|         val originalRequest = chain.request() | ||||
|  | ||||
|         val token = token ?: throw IOException("Not authenticated with MangaUpdates") | ||||
|  | ||||
|         // Add the authorization header to the original request. | ||||
|         val authRequest = originalRequest.newBuilder() | ||||
|             .addHeader("Authorization", "Bearer $token") | ||||
|             .build() | ||||
|  | ||||
|         return chain.proceed(authRequest) | ||||
|     } | ||||
|  | ||||
|     fun newAuth(token: String?) { | ||||
|         this.token = token | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,11 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Context( | ||||
|     @SerialName("session_token") | ||||
|     val sessionToken: String, | ||||
|     val uid: Long, | ||||
| ) | ||||
| @@ -0,0 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Image( | ||||
|     val url: Url? = null, | ||||
|     val height: Int? = null, | ||||
|     val width: Int? = null, | ||||
| ) | ||||
| @@ -0,0 +1,22 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates.Companion.READING_LIST | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class ListItem( | ||||
|     val series: Series? = null, | ||||
|     @SerialName("list_id") | ||||
|     val listId: Int? = null, | ||||
|     val status: Status? = null, | ||||
|     val priority: Int? = null, | ||||
| ) | ||||
|  | ||||
| fun ListItem.copyTo(track: Track): Track { | ||||
|     return track.apply { | ||||
|         this.status = listId ?: READING_LIST | ||||
|         this.last_chapter_read = this@copyTo.status?.chapter?.toFloat() ?: 0f | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Rating( | ||||
|     val rating: Int? = null, | ||||
| ) | ||||
|  | ||||
| fun Rating.copyTo(track: Track): Track { | ||||
|     return track.apply { | ||||
|         this.score = rating?.toFloat() ?: 0f | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,37 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.track.model.TrackSearch | ||||
| import kotlinx.serialization.SerialName | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Record( | ||||
|     @SerialName("series_id") | ||||
|     val seriesId: Long? = null, | ||||
|     val title: String? = null, | ||||
|     val url: String? = null, | ||||
|     val description: String? = null, | ||||
|     val image: Image? = null, | ||||
|     val type: String? = null, | ||||
|     val year: String? = null, | ||||
|     @SerialName("bayesian_rating") | ||||
|     val bayesianRating: Double? = null, | ||||
|     @SerialName("rating_votes") | ||||
|     val ratingVotes: Int? = null, | ||||
|     @SerialName("latest_chapter") | ||||
|     val latestChapter: Int? = null, | ||||
| ) | ||||
|  | ||||
| fun Record.toTrackSearch(id: Int): TrackSearch { | ||||
|     return TrackSearch.create(id).apply { | ||||
|         media_id = this@toTrackSearch.seriesId ?: 0L | ||||
|         title = this@toTrackSearch.title ?: "" | ||||
|         total_chapters = 0 | ||||
|         cover_url = this@toTrackSearch.image?.url?.original ?: "" | ||||
|         summary = this@toTrackSearch.description ?: "" | ||||
|         tracking_url = this@toTrackSearch.url ?: "" | ||||
|         publishing_status = "" | ||||
|         publishing_type = this@toTrackSearch.type.toString() | ||||
|         start_date = this@toTrackSearch.year.toString() | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Series( | ||||
|     val id: Long? = null, | ||||
|     val title: String? = null, | ||||
| ) | ||||
| @@ -0,0 +1,9 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Status( | ||||
|     val volume: Int? = null, | ||||
|     val chapter: Int? = null, | ||||
| ) | ||||
| @@ -0,0 +1,9 @@ | ||||
| package eu.kanade.tachiyomi.data.track.mangaupdates.dto | ||||
|  | ||||
| import kotlinx.serialization.Serializable | ||||
|  | ||||
| @Serializable | ||||
| data class Url( | ||||
|     val original: String? = null, | ||||
|     val thumb: String? = null, | ||||
| ) | ||||
| @@ -10,7 +10,7 @@ class TrackSearch : Track { | ||||
|  | ||||
|     override var sync_id: Int = 0 | ||||
|  | ||||
|     override var media_id: Int = 0 | ||||
|     override var media_id: Long = 0 | ||||
|  | ||||
|     override var library_id: Long? = null | ||||
|  | ||||
| @@ -54,7 +54,7 @@ class TrackSearch : Track { | ||||
|     override fun hashCode(): Int { | ||||
|         var result = (manga_id xor manga_id.ushr(32)).toInt() | ||||
|         result = 31 * result + sync_id | ||||
|         result = 31 * result + media_id | ||||
|         result = 31 * result + media_id.toInt() | ||||
|         return result | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import kotlinx.serialization.json.int | ||||
| import kotlinx.serialization.json.jsonArray | ||||
| import kotlinx.serialization.json.jsonObject | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import kotlinx.serialization.json.long | ||||
| import okhttp3.FormBody | ||||
| import okhttp3.OkHttpClient | ||||
| import okhttp3.Request | ||||
| @@ -94,7 +95,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|                 .let { | ||||
|                     val obj = it.jsonObject | ||||
|                     TrackSearch.create(TrackManager.MYANIMELIST).apply { | ||||
|                         media_id = obj["id"]!!.jsonPrimitive.int | ||||
|                         media_id = obj["id"]!!.jsonPrimitive.long | ||||
|                         title = obj["title"]!!.jsonPrimitive.content | ||||
|                         summary = obj["synopsis"]?.jsonPrimitive?.content ?: "" | ||||
|                         total_chapters = obj["num_chapters"]!!.jsonPrimitive.int | ||||
| @@ -251,7 +252,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI | ||||
|             .appendQueryParameter("response_type", "code") | ||||
|             .build() | ||||
|  | ||||
|         fun mangaUrl(id: Int): Uri = "$baseApiUrl/manga".toUri().buildUpon() | ||||
|         fun mangaUrl(id: Long): Uri = "$baseApiUrl/manga".toUri().buildUpon() | ||||
|             .appendPath(id.toString()) | ||||
|             .appendPath("my_list_status") | ||||
|             .build() | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import kotlinx.serialization.json.float | ||||
| import kotlinx.serialization.json.int | ||||
| import kotlinx.serialization.json.jsonObject | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import kotlinx.serialization.json.long | ||||
| import kotlinx.serialization.json.put | ||||
| import kotlinx.serialization.json.putJsonObject | ||||
| import okhttp3.FormBody | ||||
| @@ -73,7 +74,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|  | ||||
|     private fun jsonToSearch(obj: JsonObject): TrackSearch { | ||||
|         return TrackSearch.create(TrackManager.SHIKIMORI).apply { | ||||
|             media_id = obj["id"]!!.jsonPrimitive.int | ||||
|             media_id = obj["id"]!!.jsonPrimitive.long | ||||
|             title = obj["name"]!!.jsonPrimitive.content | ||||
|             total_chapters = obj["chapters"]!!.jsonPrimitive.int | ||||
|             cover_url = baseUrl + obj["image"]!!.jsonObject["preview"]!!.jsonPrimitive.content | ||||
| @@ -88,7 +89,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter | ||||
|     private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track { | ||||
|         return Track.create(TrackManager.SHIKIMORI).apply { | ||||
|             title = mangas["name"]!!.jsonPrimitive.content | ||||
|             media_id = obj["id"]!!.jsonPrimitive.int | ||||
|             media_id = obj["id"]!!.jsonPrimitive.long | ||||
|             total_chapters = mangas["chapters"]!!.jsonPrimitive.int | ||||
|             last_chapter_read = obj["chapters"]!!.jsonPrimitive.float | ||||
|             score = (obj["score"]!!.jsonPrimitive.int).toFloat() | ||||
|   | ||||
| @@ -36,3 +36,31 @@ fun POST( | ||||
|         .cacheControl(cache) | ||||
|         .build() | ||||
| } | ||||
|  | ||||
| fun PUT( | ||||
|     url: String, | ||||
|     headers: Headers = DEFAULT_HEADERS, | ||||
|     body: RequestBody = DEFAULT_BODY, | ||||
|     cache: CacheControl = DEFAULT_CACHE_CONTROL, | ||||
| ): Request { | ||||
|     return Request.Builder() | ||||
|         .url(url) | ||||
|         .put(body) | ||||
|         .headers(headers) | ||||
|         .cacheControl(cache) | ||||
|         .build() | ||||
| } | ||||
|  | ||||
| fun DELETE( | ||||
|     url: String, | ||||
|     headers: Headers = DEFAULT_HEADERS, | ||||
|     body: RequestBody = DEFAULT_BODY, | ||||
|     cache: CacheControl = DEFAULT_CACHE_CONTROL, | ||||
| ): Request { | ||||
|     return Request.Builder() | ||||
|         .url(url) | ||||
|         .delete(body) | ||||
|         .headers(headers) | ||||
|         .cacheControl(cache) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -63,13 +63,17 @@ class SettingsTrackingController : | ||||
|                 dialog.targetController = this@SettingsTrackingController | ||||
|                 dialog.showDialog(router) | ||||
|             } | ||||
|             trackPreference(trackManager.mangaUpdates) { | ||||
|                 val dialog = TrackLoginDialog(trackManager.mangaUpdates, R.string.username) | ||||
|                 dialog.targetController = this@SettingsTrackingController | ||||
|                 dialog.showDialog(router) | ||||
|             } | ||||
|             trackPreference(trackManager.shikimori) { | ||||
|                 activity?.openInBrowser(ShikimoriApi.authUrl(), forceDefaultBrowser = true) | ||||
|             } | ||||
|             trackPreference(trackManager.bangumi) { | ||||
|                 activity?.openInBrowser(BangumiApi.authUrl(), forceDefaultBrowser = true) | ||||
|             } | ||||
|  | ||||
|             infoPreference(R.string.tracking_info) | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								app/src/main/res/drawable-nodpi/ic_manga_updates.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								app/src/main/res/drawable-nodpi/ic_manga_updates.webp
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 11 KiB | 
| @@ -643,6 +643,7 @@ | ||||
|     <string name="tracker_komga_warning">This tracker is only compatible with the Komga source.</string> | ||||
|     <string name="tracker_bangumi" translatable="false">Bangumi</string> | ||||
|     <string name="tracker_shikimori" translatable="false">Shikimori</string> | ||||
|     <string name="tracker_manga_updates" translatable="false">MangaUpdates</string> | ||||
|     <string name="manga_tracking_tab">Tracking</string> | ||||
|     <plurals name="num_trackers"> | ||||
|         <item quantity="one">%d tracker</item> | ||||
| @@ -657,6 +658,11 @@ | ||||
|     <string name="paused">Paused</string> | ||||
|     <string name="plan_to_read">Plan to read</string> | ||||
|     <string name="repeating">Rereading</string> | ||||
|     <string name="reading_list">Reading List</string> | ||||
|     <string name="wish_list">Wish List</string> | ||||
|     <string name="complete_list">Complete List</string> | ||||
|     <string name="on_hold_list">On Hold List</string> | ||||
|     <string name="unfinished_list">Unfinished List</string> | ||||
|     <string name="score">Score</string> | ||||
|     <string name="title">Title</string> | ||||
|     <string name="status">Status</string> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user