mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-01 09:54:34 -05:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
464b9659fe | ||
|
|
7a59d0d4dd |
@@ -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,188 @@
|
|||||||
|
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
|
||||||
|
abstract val name: 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,101 @@
|
|||||||
|
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) }
|
||||||
|
|
||||||
|
override val name: String = "MangaUpdates"
|
||||||
|
|
||||||
|
// 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 -> "Reading List"
|
||||||
|
WISH_LIST -> "Wish List"
|
||||||
|
COMPLETE_LIST -> "Complete List"
|
||||||
|
ON_HOLD_LIST -> ">On Hold List"
|
||||||
|
UNFINISHED_LIST -> "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> {
|
fun Call.asObservableSuccess(): Observable<Response> {
|
||||||
return asObservable()
|
return asObservable()
|
||||||
.doOnNext { response ->
|
.doOnNext { response ->
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import okhttp3.CacheControl
|
|||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import java.util.concurrent.TimeUnit.MINUTES
|
import java.util.concurrent.TimeUnit.MINUTES
|
||||||
@@ -17,11 +18,7 @@ fun GET(
|
|||||||
headers: Headers = DEFAULT_HEADERS,
|
headers: Headers = DEFAULT_HEADERS,
|
||||||
cache: CacheControl = DEFAULT_CACHE_CONTROL
|
cache: CacheControl = DEFAULT_CACHE_CONTROL
|
||||||
): Request {
|
): Request {
|
||||||
return Request.Builder()
|
return GET(url.toHttpUrl(), headers, cache)
|
||||||
.url(url)
|
|
||||||
.headers(headers)
|
|
||||||
.cacheControl(cache)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,3 +49,31 @@ fun POST(
|
|||||||
.cacheControl(cache)
|
.cacheControl(cache)
|
||||||
.build()
|
.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
|
package eu.kanade.tachiyomi.util.lang
|
||||||
|
|
||||||
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator
|
||||||
|
import org.jsoup.Jsoup
|
||||||
|
import org.jsoup.safety.Safelist
|
||||||
import kotlin.math.floor
|
import kotlin.math.floor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,3 +58,10 @@ fun String.takeBytes(n: Int): String {
|
|||||||
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
|
bytes.decodeToString(endIndex = n).replace("\uFFFD", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML-decode the string
|
||||||
|
*/
|
||||||
|
fun String.htmlDecode(): String {
|
||||||
|
return Jsoup.clean(this, Safelist.none()).toString()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package suwayomi.tachidesk.graphql.queries
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.graphql.types.TrackServiceType
|
||||||
|
import suwayomi.tachidesk.server.trackManager
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
class TrackQuery {
|
||||||
|
fun trackService(id: Long): TrackServiceType? =
|
||||||
|
trackManager.services.find { it.id == id }?.let {
|
||||||
|
TrackServiceType(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackServices(): List<TrackServiceType> = trackManager.services.map {
|
||||||
|
TrackServiceType(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import suwayomi.tachidesk.graphql.queries.ExtensionQuery
|
|||||||
import suwayomi.tachidesk.graphql.queries.MangaQuery
|
import suwayomi.tachidesk.graphql.queries.MangaQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.MetaQuery
|
import suwayomi.tachidesk.graphql.queries.MetaQuery
|
||||||
import suwayomi.tachidesk.graphql.queries.SourceQuery
|
import suwayomi.tachidesk.graphql.queries.SourceQuery
|
||||||
|
import suwayomi.tachidesk.graphql.queries.TrackQuery
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor
|
import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString
|
import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString
|
||||||
@@ -53,7 +54,8 @@ val schema = toSchema(
|
|||||||
TopLevelObject(ExtensionQuery()),
|
TopLevelObject(ExtensionQuery()),
|
||||||
TopLevelObject(MangaQuery()),
|
TopLevelObject(MangaQuery()),
|
||||||
TopLevelObject(MetaQuery()),
|
TopLevelObject(MetaQuery()),
|
||||||
TopLevelObject(SourceQuery())
|
TopLevelObject(SourceQuery()),
|
||||||
|
TopLevelObject(TrackQuery())
|
||||||
),
|
),
|
||||||
mutations = listOf(
|
mutations = listOf(
|
||||||
TopLevelObject(CategoryMutation()),
|
TopLevelObject(CategoryMutation()),
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import suwayomi.tachidesk.graphql.server.primitives.Cursor
|
||||||
|
import suwayomi.tachidesk.graphql.server.primitives.Edge
|
||||||
|
import suwayomi.tachidesk.graphql.server.primitives.Node
|
||||||
|
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||||
|
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||||
|
|
||||||
|
data class TrackServiceType(
|
||||||
|
val id: Long,
|
||||||
|
val name: String
|
||||||
|
) : Node {
|
||||||
|
constructor(trackService: TrackService) : this(
|
||||||
|
trackService.id,
|
||||||
|
trackService.name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TrackServiceNodeList(
|
||||||
|
override val nodes: List<TrackServiceType>,
|
||||||
|
override val edges: List<TrackServiceEdge>,
|
||||||
|
override val pageInfo: PageInfo,
|
||||||
|
override val totalCount: Int
|
||||||
|
) : NodeList() {
|
||||||
|
data class TrackServiceEdge(
|
||||||
|
override val cursor: Cursor,
|
||||||
|
override val node: TrackServiceType
|
||||||
|
) : Edge()
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.server
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import io.javalin.plugin.json.JavalinJackson
|
import io.javalin.plugin.json.JavalinJackson
|
||||||
import io.javalin.plugin.json.JsonMapper
|
import io.javalin.plugin.json.JsonMapper
|
||||||
@@ -55,6 +56,8 @@ val systemTrayInstance by lazy { systemTray() }
|
|||||||
|
|
||||||
val androidCompat by lazy { AndroidCompat() }
|
val androidCompat by lazy { AndroidCompat() }
|
||||||
|
|
||||||
|
val trackManager by lazy { TrackManager() }
|
||||||
|
|
||||||
fun applicationSetup() {
|
fun applicationSetup() {
|
||||||
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
|
logger.info("Running Tachidesk ${BuildConfig.VERSION} revision ${BuildConfig.REVISION}")
|
||||||
|
|
||||||
|
|||||||
@@ -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