|
@@ -0,0 +1,146 @@
|
|
|
|
+package eu.kanade.tachiyomi.data.track.kavita
|
|
|
|
+
|
|
|
|
+import android.app.Application
|
|
|
|
+import android.content.Context
|
|
|
|
+import android.content.SharedPreferences
|
|
|
|
+import android.graphics.Color
|
|
|
|
+import androidx.annotation.StringRes
|
|
|
|
+import eu.kanade.tachiyomi.R
|
|
|
|
+import eu.kanade.tachiyomi.data.database.models.Manga
|
|
|
|
+import eu.kanade.tachiyomi.data.database.models.Track
|
|
|
|
+import eu.kanade.tachiyomi.data.track.EnhancedTrackService
|
|
|
|
+import eu.kanade.tachiyomi.data.track.NoLoginTrackService
|
|
|
|
+import eu.kanade.tachiyomi.data.track.TrackService
|
|
|
|
+import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|
|
|
+import eu.kanade.tachiyomi.source.Source
|
|
|
|
+import uy.kohesive.injekt.Injekt
|
|
|
|
+import uy.kohesive.injekt.api.get
|
|
|
|
+import java.security.MessageDigest
|
|
|
|
+
|
|
|
|
+class Kavita(private val context: Context, id: Long) : TrackService(id), EnhancedTrackService, NoLoginTrackService {
|
|
|
|
+ var authentications: OAuth? = null
|
|
|
|
+ companion object {
|
|
|
|
+ const val UNREAD = 1
|
|
|
|
+ const val READING = 2
|
|
|
|
+ const val COMPLETED = 3
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private val interceptor by lazy { KavitaInterceptor(this) }
|
|
|
|
+ val api by lazy { KavitaApi(client, interceptor) }
|
|
|
|
+
|
|
|
|
+ @StringRes
|
|
|
|
+ override fun nameRes() = R.string.tracker_kavita
|
|
|
|
+
|
|
|
|
+ override fun getLogo(): Int = R.drawable.ic_tracker_kavita
|
|
|
|
+
|
|
|
|
+ override fun getLogoColor() = Color.rgb(74, 198, 148)
|
|
|
|
+
|
|
|
|
+ override fun getStatusList() = listOf(UNREAD, READING, COMPLETED)
|
|
|
|
+
|
|
|
|
+ override fun getStatus(status: Int): String = with(context) {
|
|
|
|
+ when (status) {
|
|
|
|
+ Kavita.UNREAD -> getString(R.string.unread)
|
|
|
|
+ Kavita.READING -> getString(R.string.reading)
|
|
|
|
+ Kavita.COMPLETED -> getString(R.string.completed)
|
|
|
|
+ else -> ""
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override fun getReadingStatus(): Int = Kavita.READING
|
|
|
|
+
|
|
|
|
+ override fun getRereadingStatus(): Int = -1
|
|
|
|
+
|
|
|
|
+ override fun getCompletionStatus(): Int = Kavita.COMPLETED
|
|
|
|
+
|
|
|
|
+ override fun getScoreList(): List<String> = emptyList()
|
|
|
|
+
|
|
|
|
+ override fun displayScore(track: Track): String = ""
|
|
|
|
+
|
|
|
|
+ override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
|
|
|
+ if (track.status != COMPLETED) {
|
|
|
|
+ if (didReadChapter) {
|
|
|
|
+ if (track.last_chapter_read.toInt() == track.total_chapters && track.total_chapters > 0) {
|
|
|
|
+ track.status = COMPLETED
|
|
|
|
+ } else {
|
|
|
|
+ track.status = READING
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return api.updateProgress(track)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
|
|
|
|
+ return track
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override suspend fun search(query: String): List<TrackSearch> {
|
|
|
|
+ TODO("Not yet implemented: search")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override suspend fun refresh(track: Track): Track {
|
|
|
|
+ val remoteTrack = api.getTrackSearch(track.tracking_url)
|
|
|
|
+ track.copyPersonalFrom(remoteTrack)
|
|
|
|
+ track.total_chapters = remoteTrack.total_chapters
|
|
|
|
+ return track
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override suspend fun login(username: String, password: String) {
|
|
|
|
+ saveCredentials("user", "pass")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // TrackService.isLogged works by checking that credentials are saved.
|
|
|
|
+ // By saving dummy, unused credentials, we can activate the tracker simply by login/logout
|
|
|
|
+ override fun loginNoop() {
|
|
|
|
+ saveCredentials("user", "pass")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override fun getAcceptedSources() = listOf("eu.kanade.tachiyomi.extension.all.kavita.Kavita")
|
|
|
|
+
|
|
|
|
+ override suspend fun match(manga: Manga): TrackSearch? =
|
|
|
|
+ try {
|
|
|
|
+ api.getTrackSearch(manga.url)
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
+ null
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ override fun isTrackFrom(track: eu.kanade.domain.track.model.Track, manga: eu.kanade.domain.manga.model.Manga, source: Source?): Boolean =
|
|
|
|
+ track.remoteUrl == manga.url && source?.let { accept(it) } == true
|
|
|
|
+
|
|
|
|
+ override fun migrateTrack(track: eu.kanade.domain.track.model.Track, manga: eu.kanade.domain.manga.model.Manga, newSource: Source): eu.kanade.domain.track.model.Track? =
|
|
|
|
+ if (accept(newSource)) {
|
|
|
|
+ track.copy(remoteUrl = manga.url)
|
|
|
|
+ } else {
|
|
|
|
+ null
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ fun loadOAuth() {
|
|
|
|
+ val oauth = OAuth()
|
|
|
|
+ for (sourceId in 1..3) {
|
|
|
|
+ val authentication = oauth.authentications[sourceId - 1]
|
|
|
|
+ val sourceSuffixID by lazy {
|
|
|
|
+ val key = "${"kavita_$sourceId"}/all/1" // Hardcoded versionID to 1
|
|
|
|
+ val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
|
|
|
+ (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
|
|
|
|
+ .reduce(Long::or) and Long.MAX_VALUE
|
|
|
|
+ }
|
|
|
|
+ val preferences: SharedPreferences by lazy {
|
|
|
|
+ Injekt.get<Application>().getSharedPreferences("source_$sourceSuffixID", 0x0000)
|
|
|
|
+ }
|
|
|
|
+ val prefApiUrl = preferences.getString("APIURL", "")!!
|
|
|
|
+ if (prefApiUrl.isEmpty()) {
|
|
|
|
+ // Source not configured. Skip
|
|
|
|
+ continue
|
|
|
|
+ }
|
|
|
|
+ val prefApiKey = preferences.getString("APIKEY", "")!!
|
|
|
|
+ val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey)
|
|
|
|
+
|
|
|
|
+ if (token.isNullOrEmpty()) {
|
|
|
|
+ // Source is not accessible. Skip
|
|
|
|
+ continue
|
|
|
|
+ }
|
|
|
|
+ authentication.apiUrl = prefApiUrl
|
|
|
|
+ authentication.jwtToken = token.toString()
|
|
|
|
+ }
|
|
|
|
+ authentications = oauth
|
|
|
|
+ }
|
|
|
|
+}
|