mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-28 09:15:12 +03: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:
parent
9b0d85bf6c
commit
0c631a4990
29 changed files with 513 additions and 24 deletions
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue