From 7a59d0d4dd97f6461a8b0f737f6aa3a6f9e3fcad Mon Sep 17 00:00:00 2001 From: Aria Moradi Date: Fri, 9 Jun 2023 22:56:14 +0330 Subject: [PATCH] it compiles --- .../domain/track/service/TrackPreferences.kt | 33 +++ .../tachiyomi/data/database/models/Track.kt | 46 ++++ .../data/database/models/TrackImpl.kt | 30 +++ .../data/track/EnhancedTrackService.kt | 41 ++++ .../tachiyomi/data/track/TrackManager.kt | 43 ++++ .../tachiyomi/data/track/TrackService.kt | 189 +++++++++++++++++ .../data/track/mangaupdates/MangaUpdates.kt | 102 +++++++++ .../track/mangaupdates/MangaUpdatesApi.kt | 196 ++++++++++++++++++ .../mangaupdates/MangaUpdatesInterceptor.kt | 29 +++ .../data/track/mangaupdates/dto/Context.kt | 11 + .../data/track/mangaupdates/dto/Image.kt | 10 + .../data/track/mangaupdates/dto/ListItem.kt | 22 ++ .../data/track/mangaupdates/dto/Rating.kt | 15 ++ .../data/track/mangaupdates/dto/Record.kt | 38 ++++ .../data/track/mangaupdates/dto/Series.kt | 9 + .../data/track/mangaupdates/dto/Status.kt | 9 + .../data/track/mangaupdates/dto/Url.kt | 9 + .../tachiyomi/data/track/model/TrackSearch.kt | 68 ++++++ .../tachiyomi/network/OkHttpExtensions.kt | 5 + .../eu/kanade/tachiyomi/network/Requests.kt | 35 +++- .../tachiyomi/util/lang/StringExtensions.kt | 9 + .../core/preference/Preference.kt.kt | 26 +++ .../core/preference/PreferenceStore.kt | 41 ++++ .../tachiyomi/domain/manga/model/Manga.kt | 114 ++++++++++ .../tachiyomi/domain/track/model/Track.kt | 17 ++ 25 files changed, 1142 insertions(+), 5 deletions(-) create mode 100644 server/src/main/kotlin/eu/kanade/domain/track/service/TrackPreferences.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/Track.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackManager.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackService.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt create mode 100644 server/src/main/kotlin/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt create mode 100644 server/src/main/kotlin/tachiyomi/core/preference/Preference.kt.kt create mode 100644 server/src/main/kotlin/tachiyomi/core/preference/PreferenceStore.kt create mode 100644 server/src/main/kotlin/tachiyomi/domain/manga/model/Manga.kt create mode 100644 server/src/main/kotlin/tachiyomi/domain/track/model/Track.kt diff --git a/server/src/main/kotlin/eu/kanade/domain/track/service/TrackPreferences.kt b/server/src/main/kotlin/eu/kanade/domain/track/service/TrackPreferences.kt new file mode 100644 index 000000000..11e024106 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/domain/track/service/TrackPreferences.kt @@ -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" + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/Track.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/Track.kt new file mode 100644 index 000000000..38df114e2 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/Track.kt @@ -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() + } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt new file mode 100644 index 000000000..a83a5f7a7 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/database/models/TrackImpl.kt @@ -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 = "" +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt new file mode 100644 index 000000000..75245cf80 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/EnhancedTrackService.kt @@ -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 + + 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? +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackManager.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackManager.kt new file mode 100644 index 000000000..90a8cdaef --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -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 } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackService.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackService.kt new file mode 100644 index 000000000..5f722fd7b --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/TrackService.kt @@ -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 + +// @StringRes + abstract fun getStatus(status: Int): String? + + abstract fun getReadingStatus(): Int + + abstract fun getRereadingStatus(): Int + + abstract fun getCompletionStatus(): Int + + abstract fun getScoreList(): List + + // 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 + + 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().await(mangaId) +// val hasReadChapters = allChapters.any { it.read } +// bind(item, hasReadChapters) +// +// val track = item.toDomainTrack(idRequired = false) ?: return@withIOContext +// +// Injekt.get().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().await(allChapters, track, this@TrackService) +// } +// } +// } catch (e: Throwable) { +// withUIContext { +// // Injekt.get().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().await(it) +// } +// } catch (e: Exception) { +// logcat(LogPriority.ERROR, e) { "Failed to update remote track data id=$id" } +// withUIContext { Injekt.get().toast(e.message) } +// } +// } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt new file mode 100644 index 000000000..1a0df0be2 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdates.kt @@ -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 { + 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 = _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 { + 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() + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt new file mode 100644 index 000000000..c84a388f0 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesApi.kt @@ -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 { + val listItem = with(json) { + authClient.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}")) + .awaitSuccess() + .parseAs() + } + + 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() + } + } 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 { + 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() + .let { obj -> + obj["results"]?.jsonArray?.map { element -> + json.decodeFromJsonElement(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() + .let { obj -> + try { + json.decodeFromJsonElement(obj["context"]!!) + } catch (e: Exception) { +// logcat(LogPriority.ERROR, e) + null + } + } + } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt new file mode 100644 index 000000000..5a0edbff4 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/MangaUpdatesInterceptor.kt @@ -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 + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt new file mode 100644 index 000000000..ff5418fc6 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Context.kt @@ -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 +) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt new file mode 100644 index 000000000..5c082d7ba --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Image.kt @@ -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 +) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt new file mode 100644 index 000000000..211208e7f --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/ListItem.kt @@ -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 + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt new file mode 100644 index 000000000..f47cad7d5 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Rating.kt @@ -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 + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt new file mode 100644 index 000000000..c1342b164 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Record.kt @@ -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() + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt new file mode 100644 index 000000000..c21a78b99 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Series.kt @@ -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 +) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt new file mode 100644 index 000000000..c87795fdf --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Status.kt @@ -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 +) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt new file mode 100644 index 000000000..9a79b0800 --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/mangaupdates/dto/Url.kt @@ -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 +) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt new file mode 100644 index 000000000..2f930b50c --- /dev/null +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/data/track/model/TrackSearch.kt @@ -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() + } + } +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt index 0e6eb7751..a68b3ca16 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/OkHttpExtensions.kt @@ -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 { return asObservable() .doOnNext { response -> diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt index b8bfa73e5..7eb1fe415 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/Requests.kt @@ -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() +} diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt index ab8c1c933..cdf66be7e 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/util/lang/StringExtensions.kt @@ -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() +} diff --git a/server/src/main/kotlin/tachiyomi/core/preference/Preference.kt.kt b/server/src/main/kotlin/tachiyomi/core/preference/Preference.kt.kt new file mode 100644 index 000000000..c276141e9 --- /dev/null +++ b/server/src/main/kotlin/tachiyomi/core/preference/Preference.kt.kt @@ -0,0 +1,26 @@ +package tachiyomi.core.preference + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +interface Preference { + + fun key(): String + + fun get(): T + + fun set(value: T) + + fun isSet(): Boolean + + fun delete() + + fun defaultValue(): T + + fun changes(): Flow + + fun stateIn(scope: CoroutineScope): StateFlow +} + +inline fun Preference.getAndSet(crossinline block: (T) -> R) = set(block(get())) diff --git a/server/src/main/kotlin/tachiyomi/core/preference/PreferenceStore.kt b/server/src/main/kotlin/tachiyomi/core/preference/PreferenceStore.kt new file mode 100644 index 000000000..9f7a68833 --- /dev/null +++ b/server/src/main/kotlin/tachiyomi/core/preference/PreferenceStore.kt @@ -0,0 +1,41 @@ +package tachiyomi.core.preference + +interface PreferenceStore { + + fun getString(key: String, defaultValue: String = ""): Preference + + fun getLong(key: String, defaultValue: Long = 0): Preference + + fun getInt(key: String, defaultValue: Int = 0): Preference + + fun getFloat(key: String, defaultValue: Float = 0f): Preference + + fun getBoolean(key: String, defaultValue: Boolean = false): Preference + + fun getStringSet(key: String, defaultValue: Set = emptySet()): Preference> + + fun getObject( + key: String, + defaultValue: T, + serializer: (T) -> String, + deserializer: (String) -> T + ): Preference +} + +inline fun > PreferenceStore.getEnum( + key: String, + defaultValue: T +): Preference { + return getObject( + key = key, + defaultValue = defaultValue, + serializer = { it.name }, + deserializer = { + try { + enumValueOf(it) + } catch (e: IllegalArgumentException) { + defaultValue + } + } + ) +} diff --git a/server/src/main/kotlin/tachiyomi/domain/manga/model/Manga.kt b/server/src/main/kotlin/tachiyomi/domain/manga/model/Manga.kt new file mode 100644 index 000000000..0448e5769 --- /dev/null +++ b/server/src/main/kotlin/tachiyomi/domain/manga/model/Manga.kt @@ -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?, + 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 + ) + } +} diff --git a/server/src/main/kotlin/tachiyomi/domain/track/model/Track.kt b/server/src/main/kotlin/tachiyomi/domain/track/model/Track.kt new file mode 100644 index 000000000..ae8241775 --- /dev/null +++ b/server/src/main/kotlin/tachiyomi/domain/track/model/Track.kt @@ -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 +)