mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-06-30 09:24:34 -05:00
it compiles
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
package eu.kanade.domain.track.service
|
||||
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
// import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class TrackPreferences(
|
||||
private val preferenceStore: PreferenceStore
|
||||
) {
|
||||
|
||||
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
|
||||
|
||||
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
|
||||
|
||||
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
|
||||
trackUsername(sync).set(username)
|
||||
trackPassword(sync).set(password)
|
||||
}
|
||||
|
||||
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
|
||||
|
||||
// fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
|
||||
|
||||
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
|
||||
|
||||
companion object {
|
||||
fun trackUsername(syncId: Long) = "pref_mangasync_username_$syncId"
|
||||
|
||||
private fun trackPassword(syncId: Long) = "pref_mangasync_password_$syncId"
|
||||
|
||||
private fun trackToken(syncId: Long) = "track_token_$syncId"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
import java.io.Serializable
|
||||
|
||||
interface Track : Serializable {
|
||||
|
||||
var id: Long?
|
||||
|
||||
var manga_id: Long
|
||||
|
||||
var sync_id: Int
|
||||
|
||||
var media_id: Long
|
||||
|
||||
var library_id: Long?
|
||||
|
||||
var title: String
|
||||
|
||||
var last_chapter_read: Float
|
||||
|
||||
var total_chapters: Int
|
||||
|
||||
var score: Float
|
||||
|
||||
var status: Int
|
||||
|
||||
var started_reading_date: Long
|
||||
|
||||
var finished_reading_date: Long
|
||||
|
||||
var tracking_url: String
|
||||
|
||||
fun copyPersonalFrom(other: Track) {
|
||||
last_chapter_read = other.last_chapter_read
|
||||
score = other.score
|
||||
status = other.status
|
||||
started_reading_date = other.started_reading_date
|
||||
finished_reading_date = other.finished_reading_date
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(serviceId: Long): Track = TrackImpl().apply {
|
||||
sync_id = serviceId.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.kanade.tachiyomi.data.database.models
|
||||
|
||||
class TrackImpl : Track {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long = 0
|
||||
|
||||
override var sync_id: Int = 0
|
||||
|
||||
override var media_id: Long = 0
|
||||
|
||||
override var library_id: Long? = null
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var last_chapter_read: Float = 0F
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
|
||||
override var score: Float = 0f
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var started_reading_date: Long = 0
|
||||
|
||||
override var finished_reading_date: Long = 0
|
||||
|
||||
override var tracking_url: String = ""
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
||||
/**
|
||||
* An Enhanced Track Service will never prompt the user to match a manga with the remote.
|
||||
* It is expected that such Track Service can only work with specific sources and unique IDs.
|
||||
*/
|
||||
interface EnhancedTrackService {
|
||||
/**
|
||||
* This TrackService will only work with the sources that are accepted by this filter function.
|
||||
*/
|
||||
fun accept(source: Source): Boolean {
|
||||
return source::class.qualifiedName in getAcceptedSources()
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully qualified source classes that this track service is compatible with.
|
||||
*/
|
||||
fun getAcceptedSources(): List<String>
|
||||
|
||||
fun loginNoop()
|
||||
|
||||
/**
|
||||
* match is similar to TrackService.search, but only return zero or one match.
|
||||
*/
|
||||
suspend fun match(manga: Manga): TrackSearch?
|
||||
|
||||
/**
|
||||
* Checks whether the provided source/track/manga triplet is from this TrackService
|
||||
*/
|
||||
fun isTrackFrom(track: Track, manga: Manga, source: Source?): Boolean
|
||||
|
||||
/**
|
||||
* Migrates the given track for the manga to the newSource, if possible
|
||||
*/
|
||||
fun migrateTrack(track: Track, manga: Manga, newSource: Source): Track?
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
// import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
// 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.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
|
||||
// import eu.kanade.tachiyomi.data.track.suwayomi.Suwayomi
|
||||
|
||||
class TrackManager {
|
||||
|
||||
companion object {
|
||||
const val MYANIMELIST = 1L
|
||||
const val ANILIST = 2L
|
||||
const val KITSU = 3L
|
||||
const val SHIKIMORI = 4L
|
||||
const val BANGUMI = 5L
|
||||
const val KOMGA = 6L
|
||||
const val MANGA_UPDATES = 7L
|
||||
const val KAVITA = 8L
|
||||
const val SUWAYOMI = 9L
|
||||
}
|
||||
|
||||
// val myAnimeList = MyAnimeList(MYANIMELIST)
|
||||
// val aniList = Anilist(ANILIST)
|
||||
// val kitsu = Kitsu(KITSU)
|
||||
// val shikimori = Shikimori(SHIKIMORI)
|
||||
// val bangumi = Bangumi(BANGUMI)
|
||||
// val komga = Komga(KOMGA)
|
||||
val mangaUpdates = MangaUpdates(MANGA_UPDATES)
|
||||
// val kavita = Kavita(KAVITA)
|
||||
// val suwayomi = Suwayomi(SUWAYOMI)
|
||||
|
||||
// val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga, mangaUpdates, kavita, suwayomi)
|
||||
val services = listOf(mangaUpdates)
|
||||
|
||||
fun getService(id: Long) = services.find { it.id == id }
|
||||
|
||||
fun hasLoggedServices() = services.any { it.isLogged }
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
// import androidx.annotation.CallSuper
|
||||
// import androidx.annotation.ColorInt
|
||||
// import androidx.annotation.DrawableRes
|
||||
// import androidx.annotation.StringRes
|
||||
// import eu.kanade.domain.base.BasePreferences
|
||||
// import eu.kanade.domain.chapter.interactor.SyncChaptersWithTrackServiceTwoWay
|
||||
// import eu.kanade.domain.track.model.toDbTrack
|
||||
// import eu.kanade.domain.track.model.toDomainTrack
|
||||
import eu.kanade.domain.track.service.TrackPreferences
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
// import eu.kanade.tachiyomi.util.system.toast
|
||||
// import logcat.LogPriority
|
||||
import okhttp3.OkHttpClient
|
||||
// import tachiyomi.core.util.lang.withIOContext
|
||||
// import tachiyomi.core.util.lang.withUIContext
|
||||
// import tachiyomi.core.util.system.logcat
|
||||
// import tachiyomi.domain.chapter.interactor.GetChapterByMangaId
|
||||
// import tachiyomi.domain.track.interactor.InsertTrack
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import tachiyomi.domain.track.model.Track as DomainTrack
|
||||
|
||||
abstract class TrackService(val id: Long) {
|
||||
|
||||
// val preferences: BasePreferences by injectLazy()
|
||||
val trackPreferences: TrackPreferences by injectLazy()
|
||||
val networkService: NetworkHelper by injectLazy()
|
||||
|
||||
open val client: OkHttpClient
|
||||
get() = networkService.client
|
||||
|
||||
// Name of the manga sync service to display
|
||||
// @StringRes
|
||||
// abstract fun nameRes(): String
|
||||
|
||||
// Application and remote support for reading dates
|
||||
open val supportsReadingDates: Boolean = false
|
||||
|
||||
// @DrawableRes
|
||||
// abstract fun getLogo(): Int
|
||||
|
||||
// @ColorInt
|
||||
// abstract fun getLogoColor(): Int
|
||||
|
||||
abstract fun getStatusList(): List<Int>
|
||||
|
||||
// @StringRes
|
||||
abstract fun getStatus(status: Int): String?
|
||||
|
||||
abstract fun getReadingStatus(): Int
|
||||
|
||||
abstract fun getRereadingStatus(): Int
|
||||
|
||||
abstract fun getCompletionStatus(): Int
|
||||
|
||||
abstract fun getScoreList(): List<String>
|
||||
|
||||
// TODO: Store all scores as 10 point in the future maybe?
|
||||
open fun get10PointScore(track: DomainTrack): Float {
|
||||
return track.score
|
||||
}
|
||||
|
||||
open fun indexToScore(index: Int): Float {
|
||||
return index.toFloat()
|
||||
}
|
||||
|
||||
abstract fun displayScore(track: Track): String
|
||||
|
||||
abstract suspend fun update(track: Track, didReadChapter: Boolean = false): Track
|
||||
|
||||
abstract suspend fun bind(track: Track, hasReadChapters: Boolean = false): Track
|
||||
|
||||
abstract suspend fun search(query: String): List<TrackSearch>
|
||||
|
||||
abstract suspend fun refresh(track: Track): Track
|
||||
|
||||
abstract suspend fun login(username: String, password: String)
|
||||
|
||||
// @CallSuper
|
||||
open fun logout() {
|
||||
trackPreferences.setTrackCredentials(this, "", "")
|
||||
}
|
||||
|
||||
open val isLogged: Boolean
|
||||
get() = getUsername().isNotEmpty() &&
|
||||
getPassword().isNotEmpty()
|
||||
|
||||
fun getUsername() = trackPreferences.trackUsername(this).get()
|
||||
|
||||
fun getPassword() = trackPreferences.trackPassword(this).get()
|
||||
|
||||
fun saveCredentials(username: String, password: String) {
|
||||
trackPreferences.setTrackCredentials(this, username, password)
|
||||
}
|
||||
|
||||
fun withIOContext(body: () -> Unit) { body() }
|
||||
fun withUIContext(body: () -> Unit) { body() }
|
||||
|
||||
fun registerTracking(item: Track, mangaId: Long) {
|
||||
// item.manga_id = mangaId
|
||||
// try {
|
||||
// withIOContext {
|
||||
// val allChapters = Injekt.get<GetChapterByMangaId>().await(mangaId)
|
||||
// val hasReadChapters = allChapters.any { it.read }
|
||||
// bind(item, hasReadChapters)
|
||||
//
|
||||
// val track = item.toDomainTrack(idRequired = false) ?: return@withIOContext
|
||||
//
|
||||
// Injekt.get<InsertTrack>().await(track)
|
||||
//
|
||||
// // Update chapter progress if newer chapters marked read locally
|
||||
// if (hasReadChapters) {
|
||||
// val latestLocalReadChapterNumber = allChapters
|
||||
// .sortedBy { it.chapterNumber }
|
||||
// .takeWhile { it.read }
|
||||
// .lastOrNull()
|
||||
// ?.chapterNumber?.toDouble() ?: -1.0
|
||||
//
|
||||
// if (latestLocalReadChapterNumber > track.lastChapterRead) {
|
||||
// val updatedTrack = track.copy(
|
||||
// lastChapterRead = latestLocalReadChapterNumber
|
||||
// )
|
||||
// setRemoteLastChapterRead(updatedTrack.toDbTrack(), latestLocalReadChapterNumber.toInt())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if (this is EnhancedTrackService) {
|
||||
// // Injekt.get<SyncChaptersWithTrackServiceTwoWay>().await(allChapters, track, this@TrackService)
|
||||
// }
|
||||
// }
|
||||
// } catch (e: Throwable) {
|
||||
// withUIContext {
|
||||
// // Injekt.get<Application>().toast(e.message)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fun setRemoteStatus(track: Track, status: Int) {
|
||||
track.status = status
|
||||
if (track.status == getCompletionStatus() && track.total_chapters != 0) {
|
||||
track.last_chapter_read = track.total_chapters.toFloat()
|
||||
}
|
||||
withIOContext { updateRemote(track) }
|
||||
}
|
||||
|
||||
fun setRemoteLastChapterRead(track: Track, chapterNumber: Int) {
|
||||
if (track.last_chapter_read == 0F && track.last_chapter_read < chapterNumber && track.status != getRereadingStatus()) {
|
||||
track.status = getReadingStatus()
|
||||
}
|
||||
track.last_chapter_read = chapterNumber.toFloat()
|
||||
if (track.total_chapters != 0 && track.last_chapter_read.toInt() == track.total_chapters) {
|
||||
track.status = getCompletionStatus()
|
||||
}
|
||||
withIOContext { updateRemote(track) }
|
||||
}
|
||||
|
||||
fun setRemoteScore(track: Track, scoreString: String) {
|
||||
track.score = indexToScore(getScoreList().indexOf(scoreString))
|
||||
withIOContext { updateRemote(track) }
|
||||
}
|
||||
|
||||
fun setRemoteStartDate(track: Track, epochMillis: Long) {
|
||||
track.started_reading_date = epochMillis
|
||||
withIOContext { updateRemote(track) }
|
||||
}
|
||||
|
||||
fun setRemoteFinishDate(track: Track, epochMillis: Long) {
|
||||
track.finished_reading_date = epochMillis
|
||||
withIOContext { updateRemote(track) }
|
||||
}
|
||||
|
||||
private fun updateRemote(track: Track) {
|
||||
// withIOContext {
|
||||
// try {
|
||||
// update(track)
|
||||
// track.toDomainTrack(idRequired = false)?.let {
|
||||
// Injekt.get<InsertTrack>().await(it)
|
||||
// }
|
||||
// } catch (e: Exception) {
|
||||
// logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" }
|
||||
// withUIContext { Injekt.get<Application>().toast(e.message) }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package eu.kanade.tachiyomi.data.track.mangaupdates
|
||||
|
||||
// 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(id: Long) : 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(): String = 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)
|
||||
}
|
||||
|
||||
// @StringRes
|
||||
override fun getStatus(status: Int): String? = when (status) {
|
||||
READING_LIST -> "R.string.reading_list"
|
||||
WISH_LIST -> "R.string.wish_list"
|
||||
COMPLETE_LIST -> "R.string.complete_list"
|
||||
ON_HOLD_LIST -> "R.string.on_hold_list"
|
||||
UNFINISHED_LIST -> "R.string.unfinished_list"
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun getReadingStatus(): Int = READING_LIST
|
||||
|
||||
override fun getRereadingStatus(): Int = -1
|
||||
|
||||
override fun getCompletionStatus(): Int = COMPLETE_LIST
|
||||
|
||||
private val _scoreList = (0..9).flatMap { i -> (0..9).map { j -> "$i.$j" } } + listOf("10.0")
|
||||
|
||||
override fun getScoreList(): List<String> = _scoreList
|
||||
|
||||
override fun indexToScore(index: Int): Float = _scoreList[index].toFloat()
|
||||
|
||||
override fun displayScore(track: Track): String = track.score.toString()
|
||||
|
||||
override suspend fun update(track: Track, didReadChapter: Boolean): Track {
|
||||
if (track.status != COMPLETE_LIST && didReadChapter) {
|
||||
track.status = READING_LIST
|
||||
}
|
||||
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 trackPreferences.trackPassword(this).get()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
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.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.add
|
||||
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 tachiyomi.core.util.system.logcat
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class MangaUpdatesApi(
|
||||
interceptor: MangaUpdatesInterceptor,
|
||||
private val client: OkHttpClient
|
||||
) {
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val baseUrl = "https://api.mangaupdates.com"
|
||||
private val contentType = "application/vnd.api+json".toMediaType()
|
||||
|
||||
private val authClient by lazy {
|
||||
client.newBuilder()
|
||||
.addInterceptor(interceptor)
|
||||
.build()
|
||||
}
|
||||
|
||||
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
|
||||
val listItem = with(json) {
|
||||
authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
|
||||
.awaitSuccess()
|
||||
.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)
|
||||
)
|
||||
)
|
||||
.awaitSuccess()
|
||||
.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)
|
||||
)
|
||||
)
|
||||
.awaitSuccess()
|
||||
|
||||
updateSeriesRating(track)
|
||||
}
|
||||
|
||||
private suspend fun getSeriesRating(track: Track): Rating? {
|
||||
return try {
|
||||
with(json) {
|
||||
authClient.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
|
||||
.awaitSuccess()
|
||||
.parseAs<Rating>()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateSeriesRating(track: Track) {
|
||||
if (track.score != 0f) {
|
||||
val body = buildJsonObject {
|
||||
put("rating", track.score)
|
||||
}
|
||||
authClient.newCall(
|
||||
PUT(
|
||||
url = "$baseUrl/v1/series/${track.media_id}/rating",
|
||||
body = body.toString().toRequestBody(contentType)
|
||||
)
|
||||
)
|
||||
.awaitSuccess()
|
||||
} else {
|
||||
authClient.newCall(
|
||||
DELETE(
|
||||
url = "$baseUrl/v1/series/${track.media_id}/rating"
|
||||
)
|
||||
)
|
||||
.awaitSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun search(query: String): List<Record> {
|
||||
val body = buildJsonObject {
|
||||
put("search", query)
|
||||
put(
|
||||
"filter_types",
|
||||
buildJsonArray {
|
||||
add("drama cd")
|
||||
add("novel")
|
||||
}
|
||||
)
|
||||
}
|
||||
return with(json) {
|
||||
client.newCall(
|
||||
POST(
|
||||
url = "$baseUrl/v1/series/search",
|
||||
body = body.toString().toRequestBody(contentType)
|
||||
)
|
||||
)
|
||||
.awaitSuccess()
|
||||
.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 with(json) {
|
||||
client.newCall(
|
||||
PUT(
|
||||
url = "$baseUrl/v1/account/login",
|
||||
body = body.toString().toRequestBody(contentType)
|
||||
)
|
||||
)
|
||||
.awaitSuccess()
|
||||
.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: Float? = null
|
||||
)
|
||||
|
||||
fun Rating.copyTo(track: Track): Track {
|
||||
return track.apply {
|
||||
this.score = rating ?: 0f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package eu.kanade.tachiyomi.data.track.mangaupdates.dto
|
||||
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.lang.htmlDecode
|
||||
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: Long): TrackSearch {
|
||||
return TrackSearch.create(id).apply {
|
||||
media_id = this@toTrackSearch.seriesId ?: 0L
|
||||
title = this@toTrackSearch.title?.htmlDecode() ?: ""
|
||||
total_chapters = 0
|
||||
cover_url = this@toTrackSearch.image?.url?.original ?: ""
|
||||
summary = this@toTrackSearch.description?.htmlDecode() ?: ""
|
||||
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
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
package eu.kanade.tachiyomi.data.track.model
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
class TrackSearch : Track {
|
||||
|
||||
override var id: Long? = null
|
||||
|
||||
override var manga_id: Long = 0
|
||||
|
||||
override var sync_id: Int = 0
|
||||
|
||||
override var media_id: Long = 0
|
||||
|
||||
override var library_id: Long? = null
|
||||
|
||||
override lateinit var title: String
|
||||
|
||||
override var last_chapter_read: Float = 0F
|
||||
|
||||
override var total_chapters: Int = 0
|
||||
|
||||
override var score: Float = 0f
|
||||
|
||||
override var status: Int = 0
|
||||
|
||||
override var started_reading_date: Long = 0
|
||||
|
||||
override var finished_reading_date: Long = 0
|
||||
|
||||
override lateinit var tracking_url: String
|
||||
|
||||
var cover_url: String = ""
|
||||
|
||||
var summary: String = ""
|
||||
|
||||
var publishing_status: String = ""
|
||||
|
||||
var publishing_type: String = ""
|
||||
|
||||
var start_date: String = ""
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TrackSearch
|
||||
|
||||
if (manga_id != other.manga_id) return false
|
||||
if (sync_id != other.sync_id) return false
|
||||
if (media_id != other.media_id) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = manga_id.hashCode()
|
||||
result = 31 * result + sync_id
|
||||
result = 31 * result + media_id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun create(serviceId: Long): TrackSearch = TrackSearch().apply {
|
||||
sync_id = serviceId.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,11 @@ suspend fun Call.await(): Response {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Call.awaitSuccess(): Response {
|
||||
// awaitSuccess is a renamed version of our await, they added a new await that allows non-success error codes
|
||||
return await()
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable()
|
||||
.doOnNext { response ->
|
||||
|
||||
@@ -4,6 +4,7 @@ import okhttp3.CacheControl
|
||||
import okhttp3.FormBody
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
@@ -17,11 +18,7 @@ fun GET(
|
||||
headers: Headers = DEFAULT_HEADERS,
|
||||
cache: CacheControl = DEFAULT_CACHE_CONTROL
|
||||
): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.headers(headers)
|
||||
.cacheControl(cache)
|
||||
.build()
|
||||
return GET(url.toHttpUrl(), headers, cache)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,3 +49,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()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package eu.kanade.tachiyomi.util.lang
|
||||
|
||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.safety.Safelist
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
@@ -56,3 +58,10 @@ fun String.takeBytes(n: Int): String {
|
||||
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTML-decode the string
|
||||
*/
|
||||
fun String.htmlDecode(): String {
|
||||
return Jsoup.clean(this, Safelist.none()).toString()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package tachiyomi.core.preference
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface Preference<T> {
|
||||
|
||||
fun key(): String
|
||||
|
||||
fun get(): T
|
||||
|
||||
fun set(value: T)
|
||||
|
||||
fun isSet(): Boolean
|
||||
|
||||
fun delete()
|
||||
|
||||
fun defaultValue(): T
|
||||
|
||||
fun changes(): Flow<T>
|
||||
|
||||
fun stateIn(scope: CoroutineScope): StateFlow<T>
|
||||
}
|
||||
|
||||
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(block(get()))
|
||||
@@ -0,0 +1,41 @@
|
||||
package tachiyomi.core.preference
|
||||
|
||||
interface PreferenceStore {
|
||||
|
||||
fun getString(key: String, defaultValue: String = ""): Preference<String>
|
||||
|
||||
fun getLong(key: String, defaultValue: Long = 0): Preference<Long>
|
||||
|
||||
fun getInt(key: String, defaultValue: Int = 0): Preference<Int>
|
||||
|
||||
fun getFloat(key: String, defaultValue: Float = 0f): Preference<Float>
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean = false): Preference<Boolean>
|
||||
|
||||
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
||||
|
||||
fun <T> getObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T
|
||||
): Preference<T>
|
||||
}
|
||||
|
||||
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||
key: String,
|
||||
defaultValue: T
|
||||
): Preference<T> {
|
||||
return getObject(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
serializer = { it.name },
|
||||
deserializer = {
|
||||
try {
|
||||
enumValueOf(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
114
server/src/main/kotlin/tachiyomi/domain/manga/model/Manga.kt
Normal file
114
server/src/main/kotlin/tachiyomi/domain/manga/model/Manga.kt
Normal file
@@ -0,0 +1,114 @@
|
||||
package tachiyomi.domain.manga.model
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import java.io.Serializable
|
||||
|
||||
data class Manga(
|
||||
val id: Long,
|
||||
val source: Long,
|
||||
val favorite: Boolean,
|
||||
val lastUpdate: Long,
|
||||
val nextUpdate: Long,
|
||||
val calculateInterval: Int,
|
||||
val dateAdded: Long,
|
||||
val viewerFlags: Long,
|
||||
val chapterFlags: Long,
|
||||
val coverLastModified: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val artist: String?,
|
||||
val author: String?,
|
||||
val description: String?,
|
||||
val genre: List<String>?,
|
||||
val status: Long,
|
||||
val thumbnailUrl: String?,
|
||||
val updateStrategy: UpdateStrategy,
|
||||
val initialized: Boolean
|
||||
) : Serializable {
|
||||
|
||||
val sorting: Long
|
||||
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||
|
||||
val displayMode: Long
|
||||
get() = chapterFlags and CHAPTER_DISPLAY_MASK
|
||||
|
||||
val unreadFilterRaw: Long
|
||||
get() = chapterFlags and CHAPTER_UNREAD_MASK
|
||||
|
||||
val downloadedFilterRaw: Long
|
||||
get() = chapterFlags and CHAPTER_DOWNLOADED_MASK
|
||||
|
||||
val bookmarkedFilterRaw: Long
|
||||
get() = chapterFlags and CHAPTER_BOOKMARKED_MASK
|
||||
|
||||
// val unreadFilter: TriStateFilter
|
||||
// get() = when (unreadFilterRaw) {
|
||||
// CHAPTER_SHOW_UNREAD -> TriStateFilter.ENABLED_IS
|
||||
// CHAPTER_SHOW_READ -> TriStateFilter.ENABLED_NOT
|
||||
// else -> TriStateFilter.DISABLED
|
||||
// }
|
||||
//
|
||||
// val bookmarkedFilter: TriStateFilter
|
||||
// get() = when (bookmarkedFilterRaw) {
|
||||
// CHAPTER_SHOW_BOOKMARKED -> TriStateFilter.ENABLED_IS
|
||||
// CHAPTER_SHOW_NOT_BOOKMARKED -> TriStateFilter.ENABLED_NOT
|
||||
// else -> TriStateFilter.DISABLED
|
||||
// }
|
||||
|
||||
fun sortDescending(): Boolean {
|
||||
return chapterFlags and CHAPTER_SORT_DIR_MASK == CHAPTER_SORT_DESC
|
||||
}
|
||||
|
||||
companion object {
|
||||
// Generic filter that does not filter anything
|
||||
const val SHOW_ALL = 0x00000000L
|
||||
|
||||
const val CHAPTER_SORT_DESC = 0x00000000L
|
||||
const val CHAPTER_SORT_ASC = 0x00000001L
|
||||
const val CHAPTER_SORT_DIR_MASK = 0x00000001L
|
||||
|
||||
const val CHAPTER_SHOW_UNREAD = 0x00000002L
|
||||
const val CHAPTER_SHOW_READ = 0x00000004L
|
||||
const val CHAPTER_UNREAD_MASK = 0x00000006L
|
||||
|
||||
const val CHAPTER_SHOW_DOWNLOADED = 0x00000008L
|
||||
const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010L
|
||||
const val CHAPTER_DOWNLOADED_MASK = 0x00000018L
|
||||
|
||||
const val CHAPTER_SHOW_BOOKMARKED = 0x00000020L
|
||||
const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040L
|
||||
const val CHAPTER_BOOKMARKED_MASK = 0x00000060L
|
||||
|
||||
const val CHAPTER_SORTING_SOURCE = 0x00000000L
|
||||
const val CHAPTER_SORTING_NUMBER = 0x00000100L
|
||||
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L
|
||||
const val CHAPTER_SORTING_MASK = 0x00000300L
|
||||
|
||||
const val CHAPTER_DISPLAY_NAME = 0x00000000L
|
||||
const val CHAPTER_DISPLAY_NUMBER = 0x00100000L
|
||||
const val CHAPTER_DISPLAY_MASK = 0x00100000L
|
||||
|
||||
fun create() = Manga(
|
||||
id = -1L,
|
||||
url = "",
|
||||
title = "",
|
||||
source = -1L,
|
||||
favorite = false,
|
||||
lastUpdate = 0L,
|
||||
nextUpdate = 0L,
|
||||
calculateInterval = 0,
|
||||
dateAdded = 0L,
|
||||
viewerFlags = 0L,
|
||||
chapterFlags = 0L,
|
||||
coverLastModified = 0L,
|
||||
artist = null,
|
||||
author = null,
|
||||
description = null,
|
||||
genre = null,
|
||||
status = 0L,
|
||||
thumbnailUrl = null,
|
||||
updateStrategy = UpdateStrategy.ALWAYS_UPDATE,
|
||||
initialized = false
|
||||
)
|
||||
}
|
||||
}
|
||||
17
server/src/main/kotlin/tachiyomi/domain/track/model/Track.kt
Normal file
17
server/src/main/kotlin/tachiyomi/domain/track/model/Track.kt
Normal file
@@ -0,0 +1,17 @@
|
||||
package tachiyomi.domain.track.model
|
||||
|
||||
data class Track(
|
||||
val id: Long,
|
||||
val mangaId: Long,
|
||||
val syncId: Long,
|
||||
val remoteId: Long,
|
||||
val libraryId: Long?,
|
||||
val title: String,
|
||||
val lastChapterRead: Double,
|
||||
val totalChapters: Long,
|
||||
val status: Long,
|
||||
val score: Float,
|
||||
val remoteUrl: String,
|
||||
val startDate: Long,
|
||||
val finishDate: Long
|
||||
)
|
||||
Reference in New Issue
Block a user