mirror of
https://github.com/mihonapp/mihon.git
synced 2024-12-25 02:18:24 +01:00
Add Kavita tracker (#7488)
* Added kavita tracker * Changed api endpoint since tachiyomi has it's own. Moved some processing to backend * Bugfix. Parsing to int instead of float * Ignore DOH, update migration and cleanup * Fix Unexpected JSON token modified: app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt modified: app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaApi.kt modified: app/src/main/java/eu/kanade/tachiyomi/data/track/kavita/KavitaModels.kt * Apply code format suggestions from code review Co-authored-by: Andreas <andreas.everos@gmail.com> * Apply simplified code suggestions from code review Co-authored-by: Andreas <andreas.everos@gmail.com> * Removed unused dtos * Use setter instead of function to get apiurl * Added Interceptor * Handle not configured/not accesible sources * Unused import * Added kavita to new tracking settings screen * Delete SettingsTrackingController.kt to solve conflict * Review comments * Removed break lines from log messages * Fixed jwt typo * Merged enhanced services compatibility warning message to be more generic. * Updated Komga String res to use new formatted one * Added Kavita String res to use formatted one * Apply suggestions from code review - hardcoded strings to track name Co-authored-by: Andreas <andreas.everos@gmail.com> Co-authored-by: Andreas <andreas.everos@gmail.com>
This commit is contained in:
parent
acc65529a0
commit
92b039fac7
@ -164,11 +164,28 @@ class SettingsTrackingScreen : SearchableSettings {
|
|||||||
if (hasValidSourceInstalled) {
|
if (hasValidSourceInstalled) {
|
||||||
trackManager.komga.loginNoop()
|
trackManager.komga.loginNoop()
|
||||||
} else {
|
} else {
|
||||||
context.toast(R.string.tracker_komga_warning, Toast.LENGTH_LONG)
|
context.toast(context.getString(R.string.enhanced_tracking_warning, context.getString(trackManager.komga.nameRes())), Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
logout = trackManager.komga::logout,
|
logout = trackManager.komga::logout,
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceItem.TrackingPreference(
|
||||||
|
title = stringResource(trackManager.kavita.nameRes()),
|
||||||
|
service = trackManager.kavita,
|
||||||
|
login = {
|
||||||
|
val sourceManager = Injekt.get<SourceManager>()
|
||||||
|
val acceptedSources = trackManager.kavita.getAcceptedSources()
|
||||||
|
val hasValidSourceInstalled = sourceManager.getCatalogueSources()
|
||||||
|
.any { it::class.qualifiedName in acceptedSources }
|
||||||
|
|
||||||
|
if (hasValidSourceInstalled) {
|
||||||
|
trackManager.kavita.loginNoop()
|
||||||
|
} else {
|
||||||
|
context.toast(context.getString(R.string.enhanced_tracking_warning, context.getString(trackManager.kavita.nameRes())), Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logout = trackManager.kavita::logout,
|
||||||
|
),
|
||||||
Preference.PreferenceItem.InfoPreference(stringResource(R.string.enhanced_tracking_info)),
|
Preference.PreferenceItem.InfoPreference(stringResource(R.string.enhanced_tracking_info)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.track
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
import eu.kanade.tachiyomi.data.track.kavita.Kavita
|
||||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||||
import eu.kanade.tachiyomi.data.track.komga.Komga
|
import eu.kanade.tachiyomi.data.track.komga.Komga
|
||||||
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
|
import eu.kanade.tachiyomi.data.track.mangaupdates.MangaUpdates
|
||||||
@ -19,6 +20,7 @@ class TrackManager(context: Context) {
|
|||||||
const val BANGUMI = 5L
|
const val BANGUMI = 5L
|
||||||
const val KOMGA = 6L
|
const val KOMGA = 6L
|
||||||
const val MANGA_UPDATES = 7L
|
const val MANGA_UPDATES = 7L
|
||||||
|
const val KAVITA = 8L
|
||||||
}
|
}
|
||||||
|
|
||||||
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
val myAnimeList = MyAnimeList(context, MYANIMELIST)
|
||||||
@ -28,8 +30,9 @@ class TrackManager(context: Context) {
|
|||||||
val bangumi = Bangumi(context, BANGUMI)
|
val bangumi = Bangumi(context, BANGUMI)
|
||||||
val komga = Komga(context, KOMGA)
|
val komga = Komga(context, KOMGA)
|
||||||
val mangaUpdates = MangaUpdates(context, MANGA_UPDATES)
|
val mangaUpdates = MangaUpdates(context, MANGA_UPDATES)
|
||||||
|
val kavita = Kavita(context, KAVITA)
|
||||||
|
|
||||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates)
|
val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita)
|
||||||
|
|
||||||
fun getService(id: Long) = services.find { it.id == id }
|
fun getService(id: Long) = services.find { it.id == id }
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
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 eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import logcat.LogPriority
|
||||||
|
import okhttp3.Dns
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
|
||||||
|
class KavitaApi(private val client: OkHttpClient, interceptor: KavitaInterceptor) {
|
||||||
|
private val authClient = client.newBuilder().dns(Dns.SYSTEM).addInterceptor(interceptor).build()
|
||||||
|
fun getApiFromUrl(url: String): String {
|
||||||
|
return url.split("/api/").first() + "/api"
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
"$apiUrl/Plugin/authenticate?apiKey=$apiKey&pluginName=Tachiyomi-Kavita",
|
||||||
|
body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()),
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
client.newCall(request).execute().use {
|
||||||
|
if (it.code == 200) {
|
||||||
|
return it.parseAs<AuthenticationDto>().token
|
||||||
|
}
|
||||||
|
if (it.code == 401) {
|
||||||
|
logcat(LogPriority.WARN) { "Unauthorized / api key not valid:Cleaned api URL:${apiUrl}Api key is empty:${apiKey.isEmpty()}" }
|
||||||
|
throw Exception("Unauthorized / api key not valid")
|
||||||
|
}
|
||||||
|
if (it.code == 500) {
|
||||||
|
logcat(LogPriority.WARN) { "Error fetching jwt token. Cleaned api URL:$apiUrl Api key is empty:${apiKey.isEmpty()}" }
|
||||||
|
throw Exception("Error fetching jwt token")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Not sure which one to cathc
|
||||||
|
} catch (e: SocketTimeoutException) {
|
||||||
|
logcat(LogPriority.WARN) {
|
||||||
|
"Could not fetch jwt token. Probably due to connectivity issue or the url '$apiUrl' is not available. Skipping"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR) {
|
||||||
|
"Unhandled Exception fetching jwt token for url: '$apiUrl'"
|
||||||
|
}
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getApiVolumesUrl(url: String): String {
|
||||||
|
return "${getApiFromUrl(url)}/Series/volumes?seriesId=${getIdFromUrl(url)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getIdFromUrl(url: String): Int {
|
||||||
|
/*Strips serie id from Url*/
|
||||||
|
return url.substringAfterLast("/").toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
try {
|
||||||
|
val listVolumeDto = authClient.newCall(GET(requestUrl))
|
||||||
|
.execute()
|
||||||
|
.parseAs<List<VolumeDto>>()
|
||||||
|
var volumeNumber = 0
|
||||||
|
var maxChapterNumber = 0
|
||||||
|
for (volume in listVolumeDto) {
|
||||||
|
if (volume.chapters.maxOf { it.number!!.toFloat() } == 0f) {
|
||||||
|
volumeNumber++
|
||||||
|
} else if (maxChapterNumber < volume.chapters.maxOf { it.number!!.toFloat() }) {
|
||||||
|
maxChapterNumber = volume.chapters.maxOf { it.number!!.toFloat().toInt() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (maxChapterNumber > volumeNumber) maxChapterNumber else volumeNumber
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Exception fetching Total Chapters. Request:$requestUrl" }
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLatestChapterRead(url: String): Float {
|
||||||
|
val serieId = getIdFromUrl(url)
|
||||||
|
val requestUrl = "${getApiFromUrl(url)}/Tachiyomi/latest-chapter?seriesId=$serieId"
|
||||||
|
try {
|
||||||
|
authClient.newCall(GET(requestUrl))
|
||||||
|
.execute().use {
|
||||||
|
if (it.code == 200) {
|
||||||
|
return it.parseAs<ChapterDto>().number!!.replace(",", ".").toFloat()
|
||||||
|
}
|
||||||
|
if (it.code == 204) {
|
||||||
|
return 0F
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Exception getting latest chapter read. Could not get itemRequest:$requestUrl" }
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
return 0F
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getTrackSearch(url: String): TrackSearch =
|
||||||
|
withIOContext {
|
||||||
|
try {
|
||||||
|
val serieDto: SeriesDto =
|
||||||
|
authClient.newCall(GET(url))
|
||||||
|
.await()
|
||||||
|
.parseAs<SeriesDto>()
|
||||||
|
|
||||||
|
val track = serieDto.toTrack()
|
||||||
|
|
||||||
|
track.apply {
|
||||||
|
cover_url = serieDto.thumbnail_url.toString()
|
||||||
|
tracking_url = url
|
||||||
|
total_chapters = getTotalChapters(url)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
logcat(LogPriority.WARN, e) { "Could not get item: $url" }
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"
|
||||||
|
authClient.newCall(POST(requestUrl, body = "{}".toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())))
|
||||||
|
.await()
|
||||||
|
return getTrackSearch(track.tracking_url)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Response
|
||||||
|
|
||||||
|
class KavitaInterceptor(private val kavita: Kavita) : Interceptor {
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val originalRequest = chain.request()
|
||||||
|
if (kavita.authentications == null) {
|
||||||
|
kavita.loadOAuth()
|
||||||
|
}
|
||||||
|
val jwtToken = kavita.authentications?.getToken(
|
||||||
|
kavita.api.getApiFromUrl(originalRequest.url.toString()),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add the authorization header to the original request.
|
||||||
|
val authRequest = originalRequest.newBuilder()
|
||||||
|
.addHeader("Authorization", "Bearer $jwtToken")
|
||||||
|
.header("User-Agent", "Tachiyomi Kavita v${BuildConfig.VERSION_NAME}")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return chain.proceed(authRequest)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kavita
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SeriesDto(
|
||||||
|
val id: Int,
|
||||||
|
val name: String,
|
||||||
|
val originalName: String = "",
|
||||||
|
val thumbnail_url: String? = "",
|
||||||
|
val localizedName: String? = "",
|
||||||
|
val sortName: String? = "",
|
||||||
|
val pages: Int,
|
||||||
|
val coverImageLocked: Boolean = true,
|
||||||
|
val pagesRead: Int,
|
||||||
|
val userRating: Int? = 0,
|
||||||
|
val userReview: String? = "",
|
||||||
|
val format: Int,
|
||||||
|
val created: String? = "",
|
||||||
|
val libraryId: Int,
|
||||||
|
val libraryName: String? = "",
|
||||||
|
|
||||||
|
) {
|
||||||
|
fun toTrack(): TrackSearch = TrackSearch.create(TrackManager.KAVITA).also {
|
||||||
|
it.title = name
|
||||||
|
it.summary = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class VolumeDto(
|
||||||
|
val id: Int,
|
||||||
|
val number: Int,
|
||||||
|
val name: String,
|
||||||
|
val pages: Int,
|
||||||
|
val pagesRead: Int,
|
||||||
|
val lastModified: String,
|
||||||
|
val created: String,
|
||||||
|
val seriesId: Int,
|
||||||
|
val chapters: List<ChapterDto> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ChapterDto(
|
||||||
|
val id: Int? = -1,
|
||||||
|
val range: String? = "",
|
||||||
|
val number: String? = "-1",
|
||||||
|
val pages: Int? = 0,
|
||||||
|
val isSpecial: Boolean? = false,
|
||||||
|
val title: String? = "",
|
||||||
|
val pagesRead: Int? = 0,
|
||||||
|
val coverImageLocked: Boolean? = false,
|
||||||
|
val volumeId: Int? = -1,
|
||||||
|
val created: String? = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthenticationDto(
|
||||||
|
val username: String,
|
||||||
|
val token: String,
|
||||||
|
val apiKey: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SourceAuth(
|
||||||
|
var sourceId: Int,
|
||||||
|
var apiUrl: String = "",
|
||||||
|
var jwtToken: String = "",
|
||||||
|
)
|
@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
BIN
app/src/main/res/drawable-nodpi/ic_tracker_kavita.webp
Normal file
BIN
app/src/main/res/drawable-nodpi/ic_tracker_kavita.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
@ -446,7 +446,8 @@
|
|||||||
<string name="services">Services</string>
|
<string name="services">Services</string>
|
||||||
<string name="tracking_info">One-way sync to update the chapter progress in tracking services. Set up tracking for individual entries from their tracking button.</string>
|
<string name="tracking_info">One-way sync to update the chapter progress in tracking services. Set up tracking for individual entries from their tracking button.</string>
|
||||||
<string name="enhanced_services">Enhanced services</string>
|
<string name="enhanced_services">Enhanced services</string>
|
||||||
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
|
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Manga are automatically tracked when added to your library.</string>
|
||||||
|
<string name="enhanced_tracking_warning">This tracker is only compatible with the %1$s source.</string>
|
||||||
<string name="action_track">Track</string>
|
<string name="action_track">Track</string>
|
||||||
|
|
||||||
<!-- Browse section -->
|
<!-- Browse section -->
|
||||||
@ -672,10 +673,10 @@
|
|||||||
<string name="tracker_myanimelist" translatable="false">MyAnimeList</string>
|
<string name="tracker_myanimelist" translatable="false">MyAnimeList</string>
|
||||||
<string name="tracker_kitsu" translatable="false">Kitsu</string>
|
<string name="tracker_kitsu" translatable="false">Kitsu</string>
|
||||||
<string name="tracker_komga" translatable="false">Komga</string>
|
<string name="tracker_komga" translatable="false">Komga</string>
|
||||||
<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_bangumi" translatable="false">Bangumi</string>
|
||||||
<string name="tracker_shikimori" translatable="false">Shikimori</string>
|
<string name="tracker_shikimori" translatable="false">Shikimori</string>
|
||||||
<string name="tracker_manga_updates" translatable="false">MangaUpdates</string>
|
<string name="tracker_manga_updates" translatable="false">MangaUpdates</string>
|
||||||
|
<string name="tracker_kavita" translatable="false">Kavita</string>
|
||||||
<string name="manga_tracking_tab">Tracking</string>
|
<string name="manga_tracking_tab">Tracking</string>
|
||||||
<plurals name="num_trackers">
|
<plurals name="num_trackers">
|
||||||
<item quantity="one">%d tracker</item>
|
<item quantity="one">%d tracker</item>
|
||||||
|
Loading…
Reference in New Issue
Block a user