Fix Kavita interceptor crashing app + minor cleanup

This commit is contained in:
arkon 2022-11-25 23:03:33 -05:00
parent 7e74949d38
commit a54d9912d0
4 changed files with 93 additions and 93 deletions

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.track.kavita package eu.kanade.tachiyomi.data.track.kavita
import android.app.Application
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Color import android.graphics.Color
@ -13,18 +12,20 @@ import eu.kanade.tachiyomi.data.track.NoLoginTrackService
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.Source import eu.kanade.tachiyomi.source.Source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.security.MessageDigest import java.security.MessageDigest
import eu.kanade.domain.manga.model.Manga as DomainManga
import eu.kanade.domain.track.model.Track as DomainTrack
class Kavita(private val context: Context, id: Long) : TrackService(id), EnhancedTrackService, NoLoginTrackService { class Kavita(private val context: Context, id: Long) : TrackService(id), EnhancedTrackService, NoLoginTrackService {
var authentications: OAuth? = null
companion object { companion object {
const val UNREAD = 1 const val UNREAD = 1
const val READING = 2 const val READING = 2
const val COMPLETED = 3 const val COMPLETED = 3
} }
var authentications: OAuth? = null
private val interceptor by lazy { KavitaInterceptor(this) } private val interceptor by lazy { KavitaInterceptor(this) }
val api by lazy { KavitaApi(client, interceptor) } val api by lazy { KavitaApi(client, interceptor) }
@ -39,18 +40,18 @@ class Kavita(private val context: Context, id: Long) : TrackService(id), Enhance
override fun getStatus(status: Int): String = with(context) { override fun getStatus(status: Int): String = with(context) {
when (status) { when (status) {
Kavita.UNREAD -> getString(R.string.unread) UNREAD -> getString(R.string.unread)
Kavita.READING -> getString(R.string.reading) READING -> getString(R.string.reading)
Kavita.COMPLETED -> getString(R.string.completed) COMPLETED -> getString(R.string.completed)
else -> "" else -> ""
} }
} }
override fun getReadingStatus(): Int = Kavita.READING override fun getReadingStatus(): Int = READING
override fun getRereadingStatus(): Int = -1 override fun getRereadingStatus(): Int = -1
override fun getCompletionStatus(): Int = Kavita.COMPLETED override fun getCompletionStatus(): Int = COMPLETED
override fun getScoreList(): List<String> = emptyList() override fun getScoreList(): List<String> = emptyList()
@ -103,10 +104,10 @@ class Kavita(private val context: Context, id: Long) : TrackService(id), Enhance
null null
} }
override fun isTrackFrom(track: eu.kanade.domain.track.model.Track, manga: eu.kanade.domain.manga.model.Manga, source: Source?): Boolean = override fun isTrackFrom(track: DomainTrack, manga: DomainManga, source: Source?): Boolean =
track.remoteUrl == manga.url && source?.let { accept(it) } == true 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? = override fun migrateTrack(track: DomainTrack, manga: DomainManga, newSource: Source): DomainTrack? =
if (accept(newSource)) { if (accept(newSource)) {
track.copy(remoteUrl = manga.url) track.copy(remoteUrl = manga.url)
} else { } else {
@ -118,13 +119,13 @@ class Kavita(private val context: Context, id: Long) : TrackService(id), Enhance
for (sourceId in 1..3) { for (sourceId in 1..3) {
val authentication = oauth.authentications[sourceId - 1] val authentication = oauth.authentications[sourceId - 1]
val sourceSuffixID by lazy { val sourceSuffixID by lazy {
val key = "${"kavita_$sourceId"}/all/1" // Hardcoded versionID to 1 val key = "kavita_$sourceId/all/1" // Hardcoded versionID to 1
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray()) val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) } (0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }
.reduce(Long::or) and Long.MAX_VALUE .reduce(Long::or) and Long.MAX_VALUE
} }
val preferences: SharedPreferences by lazy { val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$sourceSuffixID", 0x0000) context.getSharedPreferences("source_$sourceSuffixID", 0x0000)
} }
val prefApiUrl = preferences.getString("APIURL", "")!! val prefApiUrl = preferences.getString("APIURL", "")!!
if (prefApiUrl.isEmpty()) { if (prefApiUrl.isEmpty()) {
@ -133,7 +134,6 @@ class Kavita(private val context: Context, id: Long) : TrackService(id), Enhance
} }
val prefApiKey = preferences.getString("APIKEY", "")!! val prefApiKey = preferences.getString("APIKEY", "")!!
val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey) val token = api.getNewToken(apiUrl = prefApiUrl, apiKey = prefApiKey)
if (token.isNullOrEmpty()) { if (token.isNullOrEmpty()) {
// Source is not accessible. Skip // Source is not accessible. Skip
continue continue

View File

@ -13,51 +13,57 @@ import okhttp3.Dns
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) { class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) {
private val authClient = client.newBuilder().dns(Dns.SYSTEM).addInterceptor(interceptor).build()
private val authClient = client.newBuilder()
.dns(Dns.SYSTEM)
.addInterceptor(interceptor)
.build()
fun getApiFromUrl(url: String): String { fun getApiFromUrl(url: String): String {
return url.split("/api/").first() + "/api" return url.split("/api/").first() + "/api"
} }
/*
* Uses url to compare against each source APIURL's to get the correct custom source preference.
* Now having source preference we can do getString("APIKEY")
* Authenticates to get the token
* Saves the token in the var jwtToken
*/
fun getNewToken(apiUrl: String, apiKey: String): String? { fun getNewToken(apiUrl: String, apiKey: String): String? {
/*
* Uses url to compare against each source APIURL's to get the correct custom source preference.
* Now having source preference we can do getString("APIKEY")
* Authenticates to get the token
* Saves the token in the var jwtToken
*/
val request = POST( val request = POST(
"$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita", "$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita",
body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()), body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
) )
try { try {
client.newCall(request).execute().use { client.newCall(request).execute().use {
if (it.code == 200) { when (it.code) {
return it.parseAs<AuthenticationDto>().token 200 -> return it.parseAs<AuthenticationDto>().token
} 401 -> {
if (it.code == 401) { logcat(LogPriority.WARN) { "Unauthorized / api key not valid: Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
logcat(LogPriority.WARN) { "Unauthorized / api key not valid:Cleaned api URL:${apiUrl}Api key is empty:${apiKey.isEmpty()}" } throw IOException("Unauthorized / api key not valid")
throw Exception("Unauthorized / api key not valid") }
} 500 -> {
if (it.code == 500) { logcat(LogPriority.WARN) { "Error fetching JWT token. Cleaned api URL: $apiUrl, Api key is empty: ${apiKey.isEmpty()}" }
logcat(LogPriority.WARN) { "Error fetching jwt token. Cleaned api URL:$apiUrl Api key is empty:${apiKey.isEmpty()}" } throw IOException("Error fetching JWT token")
throw Exception("Error fetching jwt token") }
else -> {}
} }
} }
// Not sure which one to cathc // Not sure which one to catch
} catch (e: SocketTimeoutException) { } catch (e: SocketTimeoutException) {
logcat(LogPriority.WARN) { logcat(LogPriority.WARN) {
"Could not fetch jwt token. Probably due to connectivity issue or the url '$apiUrl' is not available. Skipping" "Could not fetch JWT token. Probably due to connectivity issue or the url '$apiUrl' is not available, skipping"
} }
return null return null
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR) { logcat(LogPriority.ERROR) {
"Unhandled Exception fetching jwt token for url: '$apiUrl'" "Unhandled exception fetching JWT token for url: '$apiUrl'"
} }
throw e throw IOException(e)
} }
return null return null
@ -67,16 +73,17 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
return "${getApiFromUrl(url)}/Series/volumes?seriesId=${getIdFromUrl(url)}" return "${getApiFromUrl(url)}/Series/volumes?seriesId=${getIdFromUrl(url)}"
} }
/* Strips serie id from URL */
private fun getIdFromUrl(url: String): Int { private fun getIdFromUrl(url: String): Int {
/*Strips serie id from Url*/
return url.substringAfterLast("/").toInt() return url.substringAfterLast("/").toInt()
} }
/*
* Returns total chapters in the series.
* Ignores volumes.
* Volumes consisting of 1 file treated as chapter
*/
private fun getTotalChapters(url: String): Int { private fun getTotalChapters(url: String): Int {
/*Returns total chapters in the series.
* Ignores volumes.
* Volumes consisting of 1 file treated as chapter
*/
val requestUrl = getApiVolumesUrl(url) val requestUrl = getApiVolumesUrl(url)
try { try {
val listVolumeDto = authClient.newCall(GET(requestUrl)) val listVolumeDto = authClient.newCall(GET(requestUrl))
@ -103,50 +110,46 @@ class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor
val serieId = getIdFromUrl(url) val serieId = getIdFromUrl(url)
val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId" val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId"
try { try {
authClient.newCall(GET(requestUrl)) authClient.newCall(GET(requestUrl)).execute().use {
.execute().use { if (it.code == 200) {
if (it.code == 200) { return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
}
if (it.code == 204) {
return 0F
}
} }
if (it.code == 204) {
return 0F
}
}
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest:$requestUrl" } logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest: $requestUrl" }
throw e throw e
} }
return 0F return 0F
} }
suspend fun getTrackSearch(url: String): TrackSearch = suspend fun getTrackSearch(url: String): TrackSearch = withIOContext {
withIOContext { try {
try { val serieDto: SeriesDto = authClient.newCall(GET(url))
val serieDto: SeriesDto = .await()
authClient.newCall(GET(url)) .parseAs()
.await()
.parseAs<SeriesDto>()
val track = serieDto.toTrack() val track = serieDto.toTrack()
track.apply {
cover_url = serieDto.thumbnail_url.toString()
tracking_url = url
total_chapters = getTotalChapters(url)
track.apply { title = serieDto.name
cover_url = serieDto.thumbnail_url.toString() status = when (serieDto.pagesRead) {
tracking_url = url serieDto.pages -> Kavita.COMPLETED
total_chapters = getTotalChapters(url) 0 -> Kavita.UNREAD
else -> Kavita.READING
title = serieDto.name
status = when (serieDto.pagesRead) {
serieDto.pages -> Kavita.COMPLETED
0 -> Kavita.UNREAD
else -> Kavita.READING
}
last_chapter_read = getLatestChapterRead(url)
} }
} catch (e: Exception) { last_chapter_read = getLatestChapterRead(url)
logcat(LogPriority.WARN, e) { "Could not get item: $url" }
throw e
} }
} catch (e: Exception) {
logcat(LogPriority.WARN, e) { "Could not get item: $url" }
throw e
} }
}
suspend fun updateProgress(track: Track): Track { suspend fun updateProgress(track: Track): Track {
val requestUrl = "${getApiFromUrl(track.tracking_url)}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl(track.tracking_url)}&chapterNumber=${track.last_chapter_read}" val requestUrl = "${getApiFromUrl(track.tracking_url)}/Tachiyomi/mark-chapter-until-as-read?seriesId=${getIdFromUrl(track.tracking_url)}&chapterNumber=${track.last_chapter_read}"

View File

@ -21,7 +21,6 @@ data class SeriesDto(
val created: String? = "", val created: String? = "",
val libraryId: Int, val libraryId: Int,
val libraryName: String? = "", val libraryName: String? = "",
) { ) {
fun toTrack(): TrackSearch = TrackSearch.create(TrackManager.KAVITA).also { fun toTrack(): TrackSearch = TrackSearch.create(TrackManager.KAVITA).also {
it.title = name it.title = name
@ -63,6 +62,23 @@ data class AuthenticationDto(
val apiKey: String, val apiKey: String,
) )
class OAuth(
val authentications: List<SourceAuth> = listOf(
SourceAuth(1),
SourceAuth(2),
SourceAuth(3),
),
) {
fun getToken(apiUrl: String): String? {
for (authentication in authentications) {
if (authentication.apiUrl == apiUrl) {
return authentication.jwtToken
}
}
return null
}
}
data class SourceAuth( data class SourceAuth(
var sourceId: Int, var sourceId: Int,
var apiUrl: String = "", var apiUrl: String = "",

View File

@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.data.track.kavita
class OAuth(
val authentications: List<SourceAuth> = listOf<SourceAuth>(
SourceAuth(1),
SourceAuth(2),
SourceAuth(3),
),
) {
fun getToken(apiUrl: String): String? {
for (authentication in authentications) {
if (authentication.apiUrl == apiUrl) {
return authentication.jwtToken
}
}
return null
}
}