mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Update Tracking Backend (#1457)
* Update Tracking Library * Update Bangumi * Update Anilist * Update MangaUpdates * Update MAL * Add private to bind track * Use null * Remove old nullable * Remove custom implementation of supportsTrackDeletion * Add private to updateTrack * Some descriptions * Another description
This commit is contained in:
@@ -22,7 +22,9 @@ import suwayomi.tachidesk.graphql.types.TrackStatusType
|
|||||||
import suwayomi.tachidesk.graphql.types.TrackerType
|
import suwayomi.tachidesk.graphql.types.TrackerType
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrackSearch
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||||
|
import suwayomi.tachidesk.manga.model.table.TrackSearchTable
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
|
||||||
class TrackerDataLoader : KotlinDataLoader<Int, TrackerType> {
|
class TrackerDataLoader : KotlinDataLoader<Int, TrackerType> {
|
||||||
@@ -116,7 +118,30 @@ class DisplayScoreForTrackRecordDataLoader : KotlinDataLoader<Int, String> {
|
|||||||
.toList()
|
.toList()
|
||||||
.map { it.toTrack() }
|
.map { it.toTrack() }
|
||||||
.associateBy { it.id!! }
|
.associateBy { it.id!! }
|
||||||
.mapValues { TrackerManager.getTracker(it.value.sync_id)?.displayScore(it.value) }
|
.mapValues { TrackerManager.getTracker(it.value.tracker_id)?.displayScore(it.value) }
|
||||||
|
|
||||||
|
ids.map { trackRecords[it] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DisplayScoreForTrackSearchDataLoader : KotlinDataLoader<Int, String> {
|
||||||
|
override val dataLoaderName = "DisplayScoreForTrackRecordDataLoader"
|
||||||
|
|
||||||
|
override fun getDataLoader(graphQLContext: GraphQLContext): DataLoader<Int, String> =
|
||||||
|
DataLoaderFactory.newDataLoader<Int, String> { ids ->
|
||||||
|
future {
|
||||||
|
transaction {
|
||||||
|
addLogger(Slf4jSqlDebugLogger)
|
||||||
|
val trackRecords =
|
||||||
|
TrackSearchTable
|
||||||
|
.selectAll()
|
||||||
|
.where { TrackSearchTable.id inList ids }
|
||||||
|
.toList()
|
||||||
|
.map { it.toTrackSearch() }
|
||||||
|
.associateBy { it.id!! }
|
||||||
|
.mapValues { TrackerManager.getTracker(it.value.tracker_id)?.displayScore(it.value) }
|
||||||
|
|
||||||
ids.map { trackRecords[it] }
|
ids.map { trackRecords[it] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ class TrackMutation {
|
|||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
val trackerId: Int,
|
val trackerId: Int,
|
||||||
val remoteId: Long,
|
val remoteId: Long,
|
||||||
|
@GraphQLDescription("This will only work if the tracker of the track record supports private tracking")
|
||||||
|
val private: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BindTrackPayload(
|
data class BindTrackPayload(
|
||||||
@@ -116,13 +118,14 @@ class TrackMutation {
|
|||||||
)
|
)
|
||||||
|
|
||||||
fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
|
fun bindTrack(input: BindTrackInput): CompletableFuture<BindTrackPayload> {
|
||||||
val (clientMutationId, mangaId, trackerId, remoteId) = input
|
val (clientMutationId, mangaId, trackerId, remoteId, private) = input
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
Track.bind(
|
Track.bind(
|
||||||
mangaId,
|
mangaId,
|
||||||
trackerId,
|
trackerId,
|
||||||
remoteId,
|
remoteId,
|
||||||
|
private ?: false,
|
||||||
)
|
)
|
||||||
val trackRecord =
|
val trackRecord =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -238,8 +241,12 @@ class TrackMutation {
|
|||||||
val status: Int? = null,
|
val status: Int? = null,
|
||||||
val lastChapterRead: Double? = null,
|
val lastChapterRead: Double? = null,
|
||||||
val scoreString: String? = null,
|
val scoreString: String? = null,
|
||||||
|
@GraphQLDescription("This will only work if the tracker of the track record supports reading dates")
|
||||||
val startDate: Long? = null,
|
val startDate: Long? = null,
|
||||||
|
@GraphQLDescription("This will only work if the tracker of the track record supports reading dates")
|
||||||
val finishDate: Long? = null,
|
val finishDate: Long? = null,
|
||||||
|
@GraphQLDescription("This will only work if the tracker of the track record supports private tracking")
|
||||||
|
val private: Boolean? = null,
|
||||||
@GraphQLDeprecated("Replaced with \"unbindTrack\" mutation", replaceWith = ReplaceWith("unbindTrack"))
|
@GraphQLDeprecated("Replaced with \"unbindTrack\" mutation", replaceWith = ReplaceWith("unbindTrack"))
|
||||||
val unbind: Boolean? = null,
|
val unbind: Boolean? = null,
|
||||||
)
|
)
|
||||||
@@ -260,6 +267,7 @@ class TrackMutation {
|
|||||||
input.startDate,
|
input.startDate,
|
||||||
input.finishDate,
|
input.finishDate,
|
||||||
input.unbind,
|
input.unbind,
|
||||||
|
input.private,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.ChapterDataLoader
|
|||||||
import suwayomi.tachidesk.graphql.dataLoaders.ChapterMetaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ChapterMetaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ChaptersForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ChaptersForMangaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackRecordDataLoader
|
||||||
|
import suwayomi.tachidesk.graphql.dataLoaders.DisplayScoreForTrackSearchDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.DownloadedChapterCountForMangaDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionDataLoader
|
||||||
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader
|
||||||
@@ -83,6 +84,7 @@ class TachideskDataLoaderRegistryFactory {
|
|||||||
TrackerTokenExpiredDataLoader(),
|
TrackerTokenExpiredDataLoader(),
|
||||||
TrackRecordsForMangaIdDataLoader(),
|
TrackRecordsForMangaIdDataLoader(),
|
||||||
DisplayScoreForTrackRecordDataLoader(),
|
DisplayScoreForTrackRecordDataLoader(),
|
||||||
|
DisplayScoreForTrackSearchDataLoader(),
|
||||||
TrackRecordsForTrackerIdDataLoader(),
|
TrackRecordsForTrackerIdDataLoader(),
|
||||||
TrackRecordDataLoader(),
|
TrackRecordDataLoader(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import suwayomi.tachidesk.graphql.server.primitives.Node
|
|||||||
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
import suwayomi.tachidesk.graphql.server.primitives.NodeList
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
import suwayomi.tachidesk.graphql.server.primitives.PageInfo
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackSearchTable
|
import suwayomi.tachidesk.manga.model.table.TrackSearchTable
|
||||||
@@ -20,7 +21,9 @@ class TrackerType(
|
|||||||
val icon: String,
|
val icon: String,
|
||||||
val isLoggedIn: Boolean,
|
val isLoggedIn: Boolean,
|
||||||
val authUrl: String?,
|
val authUrl: String?,
|
||||||
val supportsTrackDeletion: Boolean?,
|
val supportsTrackDeletion: Boolean,
|
||||||
|
val supportsReadingDates: Boolean,
|
||||||
|
val supportsPrivateTracking: Boolean,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(tracker: Tracker) : this(
|
constructor(tracker: Tracker) : this(
|
||||||
tracker.isLoggedIn,
|
tracker.isLoggedIn,
|
||||||
@@ -37,7 +40,9 @@ class TrackerType(
|
|||||||
} else {
|
} else {
|
||||||
tracker.authUrl()
|
tracker.authUrl()
|
||||||
},
|
},
|
||||||
tracker.supportsTrackDeletion,
|
tracker is DeletableTracker,
|
||||||
|
tracker.supportsReadingDates,
|
||||||
|
tracker.supportsPrivateTracking,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun statuses(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<TrackStatusType>> =
|
fun statuses(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<List<TrackStatusType>> =
|
||||||
@@ -72,6 +77,7 @@ class TrackRecordType(
|
|||||||
val remoteUrl: String,
|
val remoteUrl: String,
|
||||||
val startDate: Long,
|
val startDate: Long,
|
||||||
val finishDate: Long,
|
val finishDate: Long,
|
||||||
|
val private: Boolean,
|
||||||
) : Node {
|
) : Node {
|
||||||
constructor(row: ResultRow) : this(
|
constructor(row: ResultRow) : this(
|
||||||
row[TrackRecordTable.id].value,
|
row[TrackRecordTable.id].value,
|
||||||
@@ -87,6 +93,7 @@ class TrackRecordType(
|
|||||||
row[TrackRecordTable.remoteUrl],
|
row[TrackRecordTable.remoteUrl],
|
||||||
row[TrackRecordTable.startDate],
|
row[TrackRecordTable.startDate],
|
||||||
row[TrackRecordTable.finishDate],
|
row[TrackRecordTable.finishDate],
|
||||||
|
row[TrackRecordTable.private],
|
||||||
)
|
)
|
||||||
|
|
||||||
fun displayScore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<String> =
|
fun displayScore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<String> =
|
||||||
@@ -103,7 +110,9 @@ class TrackSearchType(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val trackerId: Int,
|
val trackerId: Int,
|
||||||
val remoteId: Long,
|
val remoteId: Long,
|
||||||
|
val libraryId: Long?,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
val lastChapterRead: Double,
|
||||||
val totalChapters: Int,
|
val totalChapters: Int,
|
||||||
val trackingUrl: String,
|
val trackingUrl: String,
|
||||||
val coverUrl: String,
|
val coverUrl: String,
|
||||||
@@ -111,12 +120,19 @@ class TrackSearchType(
|
|||||||
val publishingStatus: String,
|
val publishingStatus: String,
|
||||||
val publishingType: String,
|
val publishingType: String,
|
||||||
val startDate: String,
|
val startDate: String,
|
||||||
|
val status: Int,
|
||||||
|
val score: Double,
|
||||||
|
val startedReadingDate: Long,
|
||||||
|
val finishedReadingDate: Long,
|
||||||
|
val private: Boolean,
|
||||||
) {
|
) {
|
||||||
constructor(row: ResultRow) : this(
|
constructor(row: ResultRow) : this(
|
||||||
row[TrackSearchTable.id].value,
|
row[TrackSearchTable.id].value,
|
||||||
row[TrackSearchTable.trackerId],
|
row[TrackSearchTable.trackerId],
|
||||||
row[TrackSearchTable.remoteId],
|
row[TrackSearchTable.remoteId],
|
||||||
|
row[TrackSearchTable.libraryId],
|
||||||
row[TrackSearchTable.title],
|
row[TrackSearchTable.title],
|
||||||
|
row[TrackSearchTable.lastChapterRead],
|
||||||
row[TrackSearchTable.totalChapters],
|
row[TrackSearchTable.totalChapters],
|
||||||
row[TrackSearchTable.trackingUrl],
|
row[TrackSearchTable.trackingUrl],
|
||||||
row[TrackSearchTable.coverUrl],
|
row[TrackSearchTable.coverUrl],
|
||||||
@@ -124,10 +140,18 @@ class TrackSearchType(
|
|||||||
row[TrackSearchTable.publishingStatus],
|
row[TrackSearchTable.publishingStatus],
|
||||||
row[TrackSearchTable.publishingType],
|
row[TrackSearchTable.publishingType],
|
||||||
row[TrackSearchTable.startDate],
|
row[TrackSearchTable.startDate],
|
||||||
|
row[TrackSearchTable.status],
|
||||||
|
row[TrackSearchTable.score],
|
||||||
|
row[TrackSearchTable.startedReadingDate],
|
||||||
|
row[TrackSearchTable.finishedReadingDate],
|
||||||
|
row[TrackSearchTable.private],
|
||||||
)
|
)
|
||||||
|
|
||||||
fun tracker(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackerType> =
|
fun tracker(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<TrackerType> =
|
||||||
dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", trackerId)
|
dataFetchingEnvironment.getValueFromDataLoader<Int, TrackerType>("TrackerDataLoader", trackerId)
|
||||||
|
|
||||||
|
fun displayScore(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<String> =
|
||||||
|
dataFetchingEnvironment.getValueFromDataLoader<Int, String>("DisplayScoreForTrackSearchDataLoader", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TrackRecordNodeList(
|
data class TrackRecordNodeList(
|
||||||
|
|||||||
@@ -114,15 +114,16 @@ object TrackController {
|
|||||||
queryParam<Int>("mangaId"),
|
queryParam<Int>("mangaId"),
|
||||||
queryParam<Int>("trackerId"),
|
queryParam<Int>("trackerId"),
|
||||||
queryParam<String>("remoteId"),
|
queryParam<String>("remoteId"),
|
||||||
|
queryParam<Boolean>("private"),
|
||||||
documentWith = {
|
documentWith = {
|
||||||
withOperation {
|
withOperation {
|
||||||
summary("Track Record Bind")
|
summary("Track Record Bind")
|
||||||
description("Bind a Track Record to a Manga")
|
description("Bind a Track Record to a Manga")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
behaviorOf = { ctx, mangaId, trackerId, remoteId ->
|
behaviorOf = { ctx, mangaId, trackerId, remoteId, private ->
|
||||||
ctx.future {
|
ctx.future {
|
||||||
future { Track.bind(mangaId, trackerId, remoteId.toLong()) }
|
future { Track.bind(mangaId, trackerId, remoteId.toLong(), private) }
|
||||||
.thenApply { ctx.status(HttpStatus.OK) }
|
.thenApply { ctx.status(HttpStatus.OK) }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -473,14 +473,14 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
Tracker
|
Tracker
|
||||||
.getTrackRecordsByMangaId(mangaId)
|
.getTrackRecordsByMangaId(mangaId)
|
||||||
.mapNotNull { it.record?.toTrack() }
|
.mapNotNull { it.record?.toTrack() }
|
||||||
.associateBy { it.sync_id }
|
.associateBy { it.tracker_id }
|
||||||
|
|
||||||
val (existingTracks, newTracks) =
|
val (existingTracks, newTracks) =
|
||||||
tracks
|
tracks
|
||||||
.mapNotNull { backupTrack ->
|
.mapNotNull { backupTrack ->
|
||||||
val track = backupTrack.toTrack(mangaId)
|
val track = backupTrack.toTrack(mangaId)
|
||||||
|
|
||||||
val isUnsupportedTracker = TrackerManager.getTracker(track.sync_id) == null
|
val isUnsupportedTracker = TrackerManager.getTracker(track.tracker_id) == null
|
||||||
if (isUnsupportedTracker) {
|
if (isUnsupportedTracker) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
@@ -495,7 +495,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbTrack.also {
|
dbTrack.also {
|
||||||
it.media_id = track.media_id
|
it.remote_id = track.remote_id
|
||||||
it.library_id = track.library_id
|
it.library_id = track.library_id
|
||||||
it.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
it.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ data class BackupTracking(
|
|||||||
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
@ProtoNumber(10) var startedReadingDate: Long = 0,
|
||||||
// finishedReadingDate is called endReadTime in 1.x
|
// finishedReadingDate is called endReadTime in 1.x
|
||||||
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
@ProtoNumber(11) var finishedReadingDate: Long = 0,
|
||||||
|
@ProtoNumber(12) var private: Boolean = false,
|
||||||
@ProtoNumber(100) var mediaId: Long = 0,
|
@ProtoNumber(100) var mediaId: Long = 0,
|
||||||
) {
|
) {
|
||||||
fun getTrackingImpl(): TrackImpl =
|
fun getTrackingImpl(): TrackImpl =
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import org.jetbrains.exposed.sql.deleteWhere
|
|||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jsoup.Jsoup
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTrackService
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
|
||||||
@@ -31,6 +31,7 @@ import suwayomi.tachidesk.manga.model.table.TrackRecordTable.finishDate
|
|||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.lastChapterRead
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.lastChapterRead
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.libraryId
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.libraryId
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.mangaId
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.mangaId
|
||||||
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.private
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteId
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteId
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteUrl
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteUrl
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.score
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.score
|
||||||
@@ -100,7 +101,7 @@ object Track {
|
|||||||
if (record != null) {
|
if (record != null) {
|
||||||
val track =
|
val track =
|
||||||
Track.create(it.id).also { t ->
|
Track.create(it.id).also { t ->
|
||||||
t.score = record.score.toFloat()
|
t.score = record.score
|
||||||
}
|
}
|
||||||
record.scoreString = it.displayScore(track)
|
record.scoreString = it.displayScore(track)
|
||||||
}
|
}
|
||||||
@@ -124,7 +125,9 @@ object Track {
|
|||||||
id = it[TrackSearchTable.id].value,
|
id = it[TrackSearchTable.id].value,
|
||||||
trackerId = it[TrackSearchTable.trackerId],
|
trackerId = it[TrackSearchTable.trackerId],
|
||||||
remoteId = it[TrackSearchTable.remoteId],
|
remoteId = it[TrackSearchTable.remoteId],
|
||||||
|
libraryId = it[TrackSearchTable.libraryId],
|
||||||
title = it[TrackSearchTable.title],
|
title = it[TrackSearchTable.title],
|
||||||
|
lastChapterRead = it[TrackSearchTable.lastChapterRead],
|
||||||
totalChapters = it[TrackSearchTable.totalChapters],
|
totalChapters = it[TrackSearchTable.totalChapters],
|
||||||
trackingUrl = it[TrackSearchTable.trackingUrl],
|
trackingUrl = it[TrackSearchTable.trackingUrl],
|
||||||
coverUrl = it[TrackSearchTable.coverUrl],
|
coverUrl = it[TrackSearchTable.coverUrl],
|
||||||
@@ -132,6 +135,12 @@ object Track {
|
|||||||
publishingStatus = it[TrackSearchTable.publishingStatus],
|
publishingStatus = it[TrackSearchTable.publishingStatus],
|
||||||
publishingType = it[TrackSearchTable.publishingType],
|
publishingType = it[TrackSearchTable.publishingType],
|
||||||
startDate = it[TrackSearchTable.startDate],
|
startDate = it[TrackSearchTable.startDate],
|
||||||
|
status = it[TrackSearchTable.status],
|
||||||
|
score = it[TrackSearchTable.score],
|
||||||
|
scoreString = null,
|
||||||
|
startedReadingDate = it[TrackSearchTable.startedReadingDate],
|
||||||
|
finishedReadingDate = it[TrackSearchTable.finishedReadingDate],
|
||||||
|
private = it[TrackSearchTable.private],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,7 +148,7 @@ object Track {
|
|||||||
private fun ResultRow.toTrackFromSearch(mangaId: Int): Track =
|
private fun ResultRow.toTrackFromSearch(mangaId: Int): Track =
|
||||||
Track.create(this[TrackSearchTable.trackerId]).also {
|
Track.create(this[TrackSearchTable.trackerId]).also {
|
||||||
it.manga_id = mangaId
|
it.manga_id = mangaId
|
||||||
it.media_id = this[TrackSearchTable.remoteId]
|
it.remote_id = this[TrackSearchTable.remoteId]
|
||||||
it.title = this[TrackSearchTable.title]
|
it.title = this[TrackSearchTable.title]
|
||||||
it.total_chapters = this[TrackSearchTable.totalChapters]
|
it.total_chapters = this[TrackSearchTable.totalChapters]
|
||||||
it.tracking_url = this[TrackSearchTable.trackingUrl]
|
it.tracking_url = this[TrackSearchTable.trackingUrl]
|
||||||
@@ -149,6 +158,7 @@ object Track {
|
|||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
trackerId: Int,
|
trackerId: Int,
|
||||||
remoteId: Long,
|
remoteId: Long,
|
||||||
|
private: Boolean,
|
||||||
) {
|
) {
|
||||||
val track =
|
val track =
|
||||||
transaction {
|
transaction {
|
||||||
@@ -167,7 +177,8 @@ object Track {
|
|||||||
}.first()
|
}.first()
|
||||||
.toTrack()
|
.toTrack()
|
||||||
.apply {
|
.apply {
|
||||||
manga_id = mangaId
|
this.manga_id = mangaId
|
||||||
|
this.private = private
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val tracker = TrackerManager.getTracker(trackerId)!!
|
val tracker = TrackerManager.getTracker(trackerId)!!
|
||||||
@@ -234,7 +245,7 @@ object Track {
|
|||||||
|
|
||||||
val tracker = TrackerManager.getTracker(recordDb[TrackRecordTable.trackerId])
|
val tracker = TrackerManager.getTracker(recordDb[TrackRecordTable.trackerId])
|
||||||
|
|
||||||
if (deleteRemoteTrack == true && tracker is DeletableTrackService) {
|
if (deleteRemoteTrack == true && tracker is DeletableTracker) {
|
||||||
tracker.delete(recordDb.toTrack())
|
tracker.delete(recordDb.toTrack())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,8 +289,7 @@ object Track {
|
|||||||
}
|
}
|
||||||
if (input.scoreString != null) {
|
if (input.scoreString != null) {
|
||||||
val score = tracker.indexToScore(tracker.getScoreList().indexOf(input.scoreString))
|
val score = tracker.indexToScore(tracker.getScoreList().indexOf(input.scoreString))
|
||||||
// conversion issues between Float <-> Double so convert to string before double
|
recordDb[TrackRecordTable.score] = score
|
||||||
recordDb[TrackRecordTable.score] = score.toString().toDouble()
|
|
||||||
}
|
}
|
||||||
if (input.startDate != null) {
|
if (input.startDate != null) {
|
||||||
recordDb[TrackRecordTable.startDate] = input.startDate
|
recordDb[TrackRecordTable.startDate] = input.startDate
|
||||||
@@ -287,6 +297,9 @@ object Track {
|
|||||||
if (input.finishDate != null) {
|
if (input.finishDate != null) {
|
||||||
recordDb[TrackRecordTable.finishDate] = input.finishDate
|
recordDb[TrackRecordTable.finishDate] = input.finishDate
|
||||||
}
|
}
|
||||||
|
if (input.private != null) {
|
||||||
|
recordDb[TrackRecordTable.private] = input.private
|
||||||
|
}
|
||||||
|
|
||||||
val track = recordDb.toTrack()
|
val track = recordDb.toTrack()
|
||||||
tracker.update(track)
|
tracker.update(track)
|
||||||
@@ -384,7 +397,7 @@ object Track {
|
|||||||
log.debug { "remoteLastReadChapter= $lastChapterRead" }
|
log.debug { "remoteLastReadChapter= $lastChapterRead" }
|
||||||
|
|
||||||
if (chapterNumber > lastChapterRead) {
|
if (chapterNumber > lastChapterRead) {
|
||||||
track.last_chapter_read = chapterNumber.toFloat()
|
track.last_chapter_read = chapterNumber
|
||||||
tracker.update(track, true)
|
tracker.update(track, true)
|
||||||
upsertTrackRecord(track)
|
upsertTrackRecord(track)
|
||||||
}
|
}
|
||||||
@@ -397,7 +410,7 @@ object Track {
|
|||||||
.selectAll()
|
.selectAll()
|
||||||
.where {
|
.where {
|
||||||
(TrackRecordTable.mangaId eq track.manga_id) and
|
(TrackRecordTable.mangaId eq track.manga_id) and
|
||||||
(TrackRecordTable.trackerId eq track.sync_id)
|
(TrackRecordTable.trackerId eq track.tracker_id)
|
||||||
}.singleOrNull()
|
}.singleOrNull()
|
||||||
|
|
||||||
if (existingRecord != null) {
|
if (existingRecord != null) {
|
||||||
@@ -416,16 +429,17 @@ object Track {
|
|||||||
BatchUpdateStatement(TrackRecordTable).apply {
|
BatchUpdateStatement(TrackRecordTable).apply {
|
||||||
tracks.forEach {
|
tracks.forEach {
|
||||||
addBatch(EntityID(it.id!!, TrackRecordTable))
|
addBatch(EntityID(it.id!!, TrackRecordTable))
|
||||||
this[remoteId] = it.media_id
|
this[remoteId] = it.remote_id
|
||||||
this[libraryId] = it.library_id
|
this[libraryId] = it.library_id
|
||||||
this[title] = it.title
|
this[title] = it.title
|
||||||
this[lastChapterRead] = it.last_chapter_read.toDouble()
|
this[lastChapterRead] = it.last_chapter_read
|
||||||
this[totalChapters] = it.total_chapters
|
this[totalChapters] = it.total_chapters
|
||||||
this[status] = it.status
|
this[status] = it.status
|
||||||
this[score] = it.score.toDouble()
|
this[score] = it.score
|
||||||
this[remoteUrl] = it.tracking_url
|
this[remoteUrl] = it.tracking_url
|
||||||
this[startDate] = it.started_reading_date
|
this[startDate] = it.started_reading_date
|
||||||
this[finishDate] = it.finished_reading_date
|
this[finishDate] = it.finished_reading_date
|
||||||
|
this[private] = it.private
|
||||||
}
|
}
|
||||||
execute(this@transaction)
|
execute(this@transaction)
|
||||||
}
|
}
|
||||||
@@ -439,17 +453,18 @@ object Track {
|
|||||||
TrackRecordTable
|
TrackRecordTable
|
||||||
.batchInsert(tracks) {
|
.batchInsert(tracks) {
|
||||||
this[mangaId] = it.manga_id
|
this[mangaId] = it.manga_id
|
||||||
this[trackerId] = it.sync_id
|
this[trackerId] = it.tracker_id
|
||||||
this[remoteId] = it.media_id
|
this[remoteId] = it.remote_id
|
||||||
this[libraryId] = it.library_id
|
this[libraryId] = it.library_id
|
||||||
this[title] = it.title
|
this[title] = it.title
|
||||||
this[lastChapterRead] = it.last_chapter_read.toDouble()
|
this[lastChapterRead] = it.last_chapter_read
|
||||||
this[totalChapters] = it.total_chapters
|
this[totalChapters] = it.total_chapters
|
||||||
this[status] = it.status
|
this[status] = it.status
|
||||||
this[score] = it.score.toDouble()
|
this[score] = it.score
|
||||||
this[remoteUrl] = it.tracking_url
|
this[remoteUrl] = it.tracking_url
|
||||||
this[startDate] = it.started_reading_date
|
this[startDate] = it.started_reading_date
|
||||||
this[finishDate] = it.finished_reading_date
|
this[finishDate] = it.finished_reading_date
|
||||||
|
this[private] = it.private
|
||||||
}.map { it[TrackRecordTable.id].value }
|
}.map { it[TrackRecordTable.id].value }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,5 +496,8 @@ object Track {
|
|||||||
val startDate: Long? = null,
|
val startDate: Long? = null,
|
||||||
val finishDate: Long? = null,
|
val finishDate: Long? = null,
|
||||||
val unbind: Boolean? = null,
|
val unbind: Boolean? = null,
|
||||||
|
val private: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun String.htmlDecode(): String = Jsoup.parse(this).wholeText()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|||||||
/**
|
/**
|
||||||
* For track services api that support deleting a manga entry for a user's list
|
* For track services api that support deleting a manga entry for a user's list
|
||||||
*/
|
*/
|
||||||
interface DeletableTrackService {
|
interface DeletableTracker {
|
||||||
suspend fun delete(track: Track)
|
suspend fun delete(track: Track)
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ abstract class Tracker(
|
|||||||
// Application and remote support for reading dates
|
// Application and remote support for reading dates
|
||||||
open val supportsReadingDates: Boolean = false
|
open val supportsReadingDates: Boolean = false
|
||||||
|
|
||||||
abstract val supportsTrackDeletion: Boolean
|
open val supportsPrivateTracking: Boolean = false
|
||||||
|
|
||||||
override fun toString() = "$name ($id) (isLoggedIn= $isLoggedIn, isAuthExpired= ${getIfAuthExpired()})"
|
override fun toString() = "$name ($id) (isLoggedIn= $isLoggedIn, isAuthExpired= ${getIfAuthExpired()})"
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ abstract class Tracker(
|
|||||||
|
|
||||||
abstract fun getScoreList(): List<String>
|
abstract fun getScoreList(): List<String>
|
||||||
|
|
||||||
open fun indexToScore(index: Int): Float = index.toFloat()
|
open fun indexToScore(index: Int): Double = index.toDouble()
|
||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ object TrackerPreferences {
|
|||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
) {
|
) {
|
||||||
logger.debug { "setTrackCredentials: id=${sync.id} username=$username" }
|
|
||||||
preferenceStore
|
preferenceStore
|
||||||
.edit()
|
.edit()
|
||||||
.putString(trackUsername(sync.id), username)
|
.putString(trackUsername(sync.id), username)
|
||||||
@@ -42,7 +41,6 @@ object TrackerPreferences {
|
|||||||
sync: Tracker,
|
sync: Tracker,
|
||||||
token: String?,
|
token: String?,
|
||||||
) {
|
) {
|
||||||
logger.debug { "setTrackToken: id=${sync.id} token=$token" }
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
preferenceStore
|
preferenceStore
|
||||||
.edit()
|
.edit()
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package suwayomi.tachidesk.manga.impl.track.tracker.anilist
|
|||||||
|
|
||||||
import android.annotation.StringRes
|
import android.annotation.StringRes
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTrackService
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALOAuth
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
@@ -15,7 +15,7 @@ import java.io.IOException
|
|||||||
class Anilist(
|
class Anilist(
|
||||||
id: Int,
|
id: Int,
|
||||||
) : Tracker(id, "AniList"),
|
) : Tracker(id, "AniList"),
|
||||||
DeletableTrackService {
|
DeletableTracker {
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
@@ -31,8 +31,6 @@ class Anilist(
|
|||||||
const val POINT_3 = "POINT_3"
|
const val POINT_3 = "POINT_3"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsTrackDeletion: Boolean = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { AnilistInterceptor(this) }
|
private val interceptor by lazy { AnilistInterceptor(this) }
|
||||||
@@ -41,6 +39,8 @@ class Anilist(
|
|||||||
|
|
||||||
override val supportsReadingDates: Boolean = true
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
|
override val supportsPrivateTracking: Boolean = true
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
override fun getLogo(): String = "/static/tracker/anilist.png"
|
override fun getLogo(): String = "/static/tracker/anilist.png"
|
||||||
@@ -80,26 +80,26 @@ class Anilist(
|
|||||||
else -> throw Exception("Unknown score type")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun indexToScore(index: Int): Float =
|
override fun indexToScore(index: Int): Double =
|
||||||
when (trackPreferences.getScoreType(this)) {
|
when (trackPreferences.getScoreType(this)) {
|
||||||
// 10 point
|
// 10 point
|
||||||
POINT_10 -> index * 10f
|
POINT_10 -> index * 10.0
|
||||||
// 100 point
|
// 100 point
|
||||||
POINT_100 -> index.toFloat()
|
POINT_100 -> index.toDouble()
|
||||||
// 5 stars
|
// 5 stars
|
||||||
POINT_5 ->
|
POINT_5 ->
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> 0f
|
0 -> 0.0
|
||||||
else -> index * 20f - 10f
|
else -> index * 20.0 - 10.0
|
||||||
}
|
}
|
||||||
// Smiley
|
// Smiley
|
||||||
POINT_3 ->
|
POINT_3 ->
|
||||||
when (index) {
|
when (index) {
|
||||||
0 -> 0f
|
0 -> 0.0
|
||||||
else -> index * 25f + 10f
|
else -> index * 25.0 + 10.0
|
||||||
}
|
}
|
||||||
// 10 point decimal
|
// 10 point decimal
|
||||||
POINT_10_DECIMAL -> index.toFloat()
|
POINT_10_DECIMAL -> index.toDouble()
|
||||||
else -> throw Exception("Unknown score type")
|
else -> throw Exception("Unknown score type")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,17 +108,17 @@ class Anilist(
|
|||||||
return when (val type = trackPreferences.getScoreType(this)) {
|
return when (val type = trackPreferences.getScoreType(this)) {
|
||||||
POINT_5 ->
|
POINT_5 ->
|
||||||
when (score) {
|
when (score) {
|
||||||
0f -> "0 ★"
|
0.0 -> "0 ★"
|
||||||
else -> "${((score + 10) / 20).toInt()} ★"
|
else -> "${((score + 10) / 20).toInt()} ★"
|
||||||
}
|
}
|
||||||
POINT_3 ->
|
POINT_3 ->
|
||||||
when {
|
when {
|
||||||
score == 0f -> "0"
|
score == 0.0 -> "0"
|
||||||
score <= 35 -> "😦"
|
score <= 35 -> "😦"
|
||||||
score <= 60 -> "😐"
|
score <= 60 -> "😐"
|
||||||
else -> "😊"
|
else -> "😊"
|
||||||
}
|
}
|
||||||
else -> track.toAnilistScore(type)
|
else -> track.toApiScore(type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ class Anilist(
|
|||||||
track.finished_reading_date = System.currentTimeMillis()
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
} else if (track.status != REREADING) {
|
} else if (track.status != REREADING) {
|
||||||
track.status = READING
|
track.status = READING
|
||||||
if (track.last_chapter_read == 1F) {
|
if (track.last_chapter_read == 1.0) {
|
||||||
track.started_reading_date = System.currentTimeMillis()
|
track.started_reading_date = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,19 +168,19 @@ class Anilist(
|
|||||||
): Track {
|
): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
val isRereading = track.status == REREADING
|
val isRereading = track.status == REREADING
|
||||||
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
track.status = if (!isRereading && hasReadChapters) READING else track.status
|
||||||
}
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0.0
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,12 +209,11 @@ class Anilist(
|
|||||||
|
|
||||||
private suspend fun login(token: String) {
|
private suspend fun login(token: String) {
|
||||||
try {
|
try {
|
||||||
logger.debug { "login $token" }
|
|
||||||
val oauth = api.createOAuth(token)
|
val oauth = api.createOAuth(token)
|
||||||
interceptor.setAuth(oauth)
|
interceptor.setAuth(oauth)
|
||||||
val (username, scoreType) = api.getCurrentUser()
|
val (username, scoreType) = api.getCurrentUser()
|
||||||
trackPreferences.setScoreType(this, scoreType)
|
trackPreferences.setScoreType(this, scoreType)
|
||||||
saveCredentials(username.toString(), oauth.access_token)
|
saveCredentials(username.toString(), oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logger.error(e) { "oauth err" }
|
logger.error(e) { "oauth err" }
|
||||||
logout()
|
logout()
|
||||||
@@ -228,13 +227,13 @@ class Anilist(
|
|||||||
interceptor.setAuth(null)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveOAuth(oAuth: OAuth?) {
|
fun saveOAuth(oAuth: ALOAuth?) {
|
||||||
trackPreferences.setTrackToken(this, json.encodeToString(oAuth))
|
trackPreferences.setTrackToken(this, json.encodeToString(oAuth))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadOAuth(): OAuth? =
|
fun loadOAuth(): ALOAuth? =
|
||||||
try {
|
try {
|
||||||
json.decodeFromString<OAuth>(trackPreferences.getTrackToken(this)!!)
|
json.decodeFromString<ALOAuth>(trackPreferences.getTrackToken(this)!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "loadOAuth err" }
|
logger.error(e) { "loadOAuth err" }
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -12,22 +12,21 @@ import kotlinx.serialization.json.Json
|
|||||||
import kotlinx.serialization.json.JsonNull
|
import kotlinx.serialization.json.JsonNull
|
||||||
import kotlinx.serialization.json.JsonObject
|
import kotlinx.serialization.json.JsonObject
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALAddMangaResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALCurrentUserResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALSearchResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALUserListMangaQueryResult
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.util.Calendar
|
import java.time.Instant
|
||||||
import kotlin.time.Duration.Companion.days
|
import java.time.ZoneId
|
||||||
|
import java.time.ZonedDateTime
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
class AnilistApi(
|
class AnilistApi(
|
||||||
@@ -47,8 +46,8 @@ class AnilistApi(
|
|||||||
withIOContext {
|
withIOContext {
|
||||||
val query =
|
val query =
|
||||||
"""
|
"""
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
|
||||||
| id
|
| id
|
||||||
| status
|
| status
|
||||||
|}
|
|}
|
||||||
@@ -59,9 +58,10 @@ class AnilistApi(
|
|||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("query", query)
|
put("query", query)
|
||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("mangaId", track.media_id)
|
put("mangaId", track.remote_id)
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
put("status", track.toAnilistStatus())
|
put("status", track.toApiStatus())
|
||||||
|
put("private", track.private)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with(json) {
|
with(json) {
|
||||||
@@ -72,13 +72,9 @@ class AnilistApi(
|
|||||||
body = payload.toString().toRequestBody(jsonMime),
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.library_id =
|
track.library_id = it.data.entry.id
|
||||||
it["data"]!!
|
|
||||||
.jsonObject["SaveMediaListEntry"]!!
|
|
||||||
.jsonObject["id"]!!
|
|
||||||
.jsonPrimitive.long
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,11 +85,11 @@ class AnilistApi(
|
|||||||
val query =
|
val query =
|
||||||
"""
|
"""
|
||||||
|mutation UpdateManga(
|
|mutation UpdateManga(
|
||||||
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|
||||||
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|
||||||
|) {
|
|) {
|
||||||
|SaveMediaListEntry(
|
|SaveMediaListEntry(
|
||||||
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|
||||||
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|
||||||
|) {
|
|) {
|
||||||
|id
|
|id
|
||||||
@@ -109,10 +105,11 @@ class AnilistApi(
|
|||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("listId", track.library_id)
|
put("listId", track.library_id)
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
put("status", track.toAnilistStatus())
|
put("status", track.toApiStatus())
|
||||||
put("score", track.score.toInt())
|
put("score", track.score.toInt())
|
||||||
put("startedAt", createDate(track.started_reading_date))
|
put("startedAt", createDate(track.started_reading_date))
|
||||||
put("completedAt", createDate(track.finished_reading_date))
|
put("completedAt", createDate(track.finished_reading_date))
|
||||||
|
put("private", track.private)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authClient
|
authClient
|
||||||
@@ -121,7 +118,7 @@ class AnilistApi(
|
|||||||
track
|
track
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteLibManga(track: Track) =
|
suspend fun deleteLibManga(track: Track) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val query =
|
val query =
|
||||||
"""
|
"""
|
||||||
@@ -143,6 +140,7 @@ class AnilistApi(
|
|||||||
.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun search(search: String): List<TrackSearch> =
|
suspend fun search(search: String): List<TrackSearch> =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
@@ -152,6 +150,19 @@ class AnilistApi(
|
|||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
|
||||||
|id
|
|id
|
||||||
|
|staff {
|
||||||
|
|edges {
|
||||||
|
|role
|
||||||
|
|id
|
||||||
|
|node {
|
||||||
|
|name {
|
||||||
|
|full
|
||||||
|
|userPreferred
|
||||||
|
|native
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|title {
|
|title {
|
||||||
|userPreferred
|
|userPreferred
|
||||||
|}
|
|}
|
||||||
@@ -167,6 +178,7 @@ class AnilistApi(
|
|||||||
|month
|
|month
|
||||||
|day
|
|day
|
||||||
|}
|
|}
|
||||||
|
|averageScore
|
||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
@@ -187,14 +199,9 @@ class AnilistApi(
|
|||||||
body = payload.toString().toRequestBody(jsonMime),
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALSearchResult>()
|
||||||
.let { response ->
|
.data.page.media
|
||||||
val data = response["data"]!!.jsonObject
|
.map { it.toALManga().toTrack() }
|
||||||
val page = data["Page"]!!.jsonObject
|
|
||||||
val media = page["media"]!!.jsonArray
|
|
||||||
val entries = media.map { jsonToALManga(it.jsonObject) }
|
|
||||||
entries.map { it.toTrack() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +219,7 @@ class AnilistApi(
|
|||||||
|status
|
|status
|
||||||
|scoreRaw: score(format: POINT_100)
|
|scoreRaw: score(format: POINT_100)
|
||||||
|progress
|
|progress
|
||||||
|
|private
|
||||||
|startedAt {
|
|startedAt {
|
||||||
|year
|
|year
|
||||||
|month
|
|month
|
||||||
@@ -239,6 +247,19 @@ class AnilistApi(
|
|||||||
|month
|
|month
|
||||||
|day
|
|day
|
||||||
|}
|
|}
|
||||||
|
|staff {
|
||||||
|
|edges {
|
||||||
|
|role
|
||||||
|
|id
|
||||||
|
|node {
|
||||||
|
|name {
|
||||||
|
|full
|
||||||
|
|userPreferred
|
||||||
|
|native
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|
|}
|
||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
|}
|
|}
|
||||||
@@ -250,7 +271,7 @@ class AnilistApi(
|
|||||||
put("query", query)
|
put("query", query)
|
||||||
putJsonObject("variables") {
|
putJsonObject("variables") {
|
||||||
put("id", userid)
|
put("id", userid)
|
||||||
put("manga_id", track.media_id)
|
put("manga_id", track.remote_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
with(json) {
|
with(json) {
|
||||||
@@ -261,24 +282,20 @@ class AnilistApi(
|
|||||||
body = payload.toString().toRequestBody(jsonMime),
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALUserListMangaQueryResult>()
|
||||||
.let { response ->
|
.data.page.mediaList
|
||||||
val data = response["data"]!!.jsonObject
|
.map { it.toALUserManga() }
|
||||||
val page = data["Page"]!!.jsonObject
|
.firstOrNull()
|
||||||
val media = page["mediaList"]!!.jsonArray
|
?.toTrack()
|
||||||
val entries = media.map { jsonToALUserManga(it.jsonObject) }
|
|
||||||
entries.firstOrNull()?.toTrack()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getLibManga(
|
suspend fun getLibManga(
|
||||||
track: Track,
|
track: Track,
|
||||||
userid: Int,
|
userId: Int,
|
||||||
): Track = findLibManga(track, userid) ?: throw Exception("Could not find manga")
|
): Track = findLibManga(track, userId) ?: throw Exception("Could not find manga")
|
||||||
|
|
||||||
fun createOAuth(token: String): OAuth =
|
fun createOAuth(token: String): ALOAuth = ALOAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||||
OAuth(token, "Bearer", System.currentTimeMillis() + 365.days.inWholeMilliseconds, 365.days.inWholeMilliseconds)
|
|
||||||
|
|
||||||
suspend fun getCurrentUser(): Pair<Int, String> =
|
suspend fun getCurrentUser(): Pair<Int, String> =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
@@ -306,57 +323,14 @@ class AnilistApi(
|
|||||||
body = payload.toString().toRequestBody(jsonMime),
|
body = payload.toString().toRequestBody(jsonMime),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<ALCurrentUserResult>()
|
||||||
.let {
|
.let {
|
||||||
val data = it["data"]!!.jsonObject
|
val viewer = it.data.viewer
|
||||||
val viewer = data["Viewer"]!!.jsonObject
|
Pair(viewer.id, viewer.mediaListOptions.scoreFormat)
|
||||||
Pair(
|
|
||||||
viewer["id"]!!.jsonPrimitive.int,
|
|
||||||
viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToALManga(struct: JsonObject): ALManga =
|
|
||||||
ALManga(
|
|
||||||
struct["id"]!!.jsonPrimitive.long,
|
|
||||||
struct["title"]!!.jsonObject["userPreferred"]!!.jsonPrimitive.content,
|
|
||||||
struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
|
|
||||||
struct["description"]!!.jsonPrimitive.contentOrNull,
|
|
||||||
struct["format"]!!.jsonPrimitive.content.replace("_", "-"),
|
|
||||||
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
|
|
||||||
parseDate(struct, "startDate"),
|
|
||||||
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun jsonToALUserManga(struct: JsonObject): ALUserManga =
|
|
||||||
ALUserManga(
|
|
||||||
struct["id"]!!.jsonPrimitive.long,
|
|
||||||
struct["status"]!!.jsonPrimitive.content,
|
|
||||||
struct["scoreRaw"]!!.jsonPrimitive.int,
|
|
||||||
struct["progress"]!!.jsonPrimitive.int,
|
|
||||||
parseDate(struct, "startedAt"),
|
|
||||||
parseDate(struct, "completedAt"),
|
|
||||||
jsonToALManga(struct["media"]!!.jsonObject),
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun parseDate(
|
|
||||||
struct: JsonObject,
|
|
||||||
dateKey: String,
|
|
||||||
): Long =
|
|
||||||
try {
|
|
||||||
val date = Calendar.getInstance()
|
|
||||||
date.set(
|
|
||||||
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
|
|
||||||
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
|
|
||||||
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int,
|
|
||||||
)
|
|
||||||
date.timeInMillis
|
|
||||||
} catch (_: Exception) {
|
|
||||||
0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDate(dateValue: Long): JsonObject {
|
private fun createDate(dateValue: Long): JsonObject {
|
||||||
if (dateValue == 0L) {
|
if (dateValue == 0L) {
|
||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
@@ -366,12 +340,11 @@ class AnilistApi(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val calendar = Calendar.getInstance()
|
val dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(dateValue), ZoneId.systemDefault())
|
||||||
calendar.timeInMillis = dateValue
|
|
||||||
return buildJsonObject {
|
return buildJsonObject {
|
||||||
put("year", calendar.get(Calendar.YEAR))
|
put("year", dateTime.year)
|
||||||
put("month", calendar.get(Calendar.MONTH) + 1)
|
put("month", dateTime.monthValue)
|
||||||
put("day", calendar.get(Calendar.DAY_OF_MONTH))
|
put("day", dateTime.dayOfMonth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ package suwayomi.tachidesk.manga.impl.track.tracker.anilist
|
|||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
|
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.ALOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto.isExpired
|
||||||
|
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class AnilistInterceptor(
|
class AnilistInterceptor(
|
||||||
private val anilist: Anilist,
|
val anilist: Anilist,
|
||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
@@ -14,7 +17,7 @@ class AnilistInterceptor(
|
|||||||
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
|
||||||
* before its original expiration date.
|
* before its original expiration date.
|
||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = null
|
private var oauth: ALOAuth? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
|
||||||
}
|
}
|
||||||
@@ -44,7 +47,8 @@ class AnilistInterceptor(
|
|||||||
val authRequest =
|
val authRequest =
|
||||||
originalRequest
|
originalRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
||||||
|
.header("User-Agent", "Suwayomi ${BuildConfig.VERSION} (${BuildConfig.REVISION})")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return chain.proceed(authRequest)
|
return chain.proceed(authRequest)
|
||||||
@@ -54,7 +58,7 @@ class AnilistInterceptor(
|
|||||||
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
|
||||||
* and the oauth object.
|
* and the oauth object.
|
||||||
*/
|
*/
|
||||||
fun setAuth(oauth: OAuth?) {
|
fun setAuth(oauth: ALOAuth?) {
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
anilist.saveOAuth(oauth)
|
anilist.saveOAuth(oauth)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.anilist
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
data class ALManga(
|
|
||||||
val media_id: Long,
|
|
||||||
val title_user_pref: String,
|
|
||||||
val image_url_lge: String,
|
|
||||||
val description: String?,
|
|
||||||
val format: String,
|
|
||||||
val publishing_status: String,
|
|
||||||
val start_date_fuzzy: Long,
|
|
||||||
val total_chapters: Int,
|
|
||||||
) {
|
|
||||||
fun toTrack() =
|
|
||||||
TrackSearch.create(TrackerManager.ANILIST).apply {
|
|
||||||
media_id = this@ALManga.media_id
|
|
||||||
title = title_user_pref
|
|
||||||
total_chapters = this@ALManga.total_chapters
|
|
||||||
cover_url = image_url_lge
|
|
||||||
summary = description ?: ""
|
|
||||||
tracking_url = AnilistApi.mangaUrl(media_id)
|
|
||||||
publishing_status = this@ALManga.publishing_status
|
|
||||||
publishing_type = format
|
|
||||||
if (start_date_fuzzy != 0L) {
|
|
||||||
start_date =
|
|
||||||
try {
|
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
outputDf.format(start_date_fuzzy)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ALUserManga(
|
|
||||||
val library_id: Long,
|
|
||||||
val list_status: String,
|
|
||||||
val score_raw: Int,
|
|
||||||
val chapters_read: Int,
|
|
||||||
val start_date_fuzzy: Long,
|
|
||||||
val completed_date_fuzzy: Long,
|
|
||||||
val manga: ALManga,
|
|
||||||
) {
|
|
||||||
fun toTrack() =
|
|
||||||
Track.create(TrackerManager.ANILIST).apply {
|
|
||||||
media_id = manga.media_id
|
|
||||||
title = manga.title_user_pref
|
|
||||||
status = toTrackStatus()
|
|
||||||
score = score_raw.toFloat()
|
|
||||||
started_reading_date = start_date_fuzzy
|
|
||||||
finished_reading_date = completed_date_fuzzy
|
|
||||||
last_chapter_read = chapters_read.toFloat()
|
|
||||||
library_id = this@ALUserManga.library_id
|
|
||||||
total_chapters = manga.total_chapters
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toTrackStatus() =
|
|
||||||
when (list_status) {
|
|
||||||
"CURRENT" -> Anilist.READING
|
|
||||||
"COMPLETED" -> Anilist.COMPLETED
|
|
||||||
"PAUSED" -> Anilist.ON_HOLD
|
|
||||||
"DROPPED" -> Anilist.DROPPED
|
|
||||||
"PLANNING" -> Anilist.PLAN_TO_READ
|
|
||||||
"REPEATING" -> Anilist.REREADING
|
|
||||||
else -> throw NotImplementedError("Unknown status: $list_status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val expires: Long,
|
|
||||||
val expires_in: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun OAuth.isExpired() = System.currentTimeMillis() > expires
|
|
||||||
|
|
||||||
fun Track.toAnilistStatus() =
|
|
||||||
when (status) {
|
|
||||||
Anilist.READING -> "CURRENT"
|
|
||||||
Anilist.COMPLETED -> "COMPLETED"
|
|
||||||
Anilist.ON_HOLD -> "PAUSED"
|
|
||||||
Anilist.DROPPED -> "DROPPED"
|
|
||||||
Anilist.PLAN_TO_READ -> "PLANNING"
|
|
||||||
Anilist.REREADING -> "REPEATING"
|
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Track.toAnilistScore(scoreType: String?): String =
|
|
||||||
when (scoreType) {
|
|
||||||
// 10 point
|
|
||||||
"POINT_10" -> (score.toInt() / 10).toString()
|
|
||||||
// 100 point
|
|
||||||
"POINT_100" -> score.toInt().toString()
|
|
||||||
// 5 stars
|
|
||||||
"POINT_5" ->
|
|
||||||
when {
|
|
||||||
score == 0f -> "0"
|
|
||||||
score < 30 -> "1"
|
|
||||||
score < 50 -> "2"
|
|
||||||
score < 70 -> "3"
|
|
||||||
score < 90 -> "4"
|
|
||||||
else -> "5"
|
|
||||||
}
|
|
||||||
// Smiley
|
|
||||||
"POINT_3" ->
|
|
||||||
when {
|
|
||||||
score == 0f -> "0"
|
|
||||||
score <= 35 -> ":("
|
|
||||||
score <= 60 -> ":|"
|
|
||||||
else -> ":)"
|
|
||||||
}
|
|
||||||
// 10 point decimal
|
|
||||||
"POINT_10_DECIMAL" -> (score / 10).toString()
|
|
||||||
else -> throw NotImplementedError("Unknown score type")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
|
fun Track.toApiStatus() =
|
||||||
|
when (status) {
|
||||||
|
Anilist.READING -> "CURRENT"
|
||||||
|
Anilist.COMPLETED -> "COMPLETED"
|
||||||
|
Anilist.ON_HOLD -> "PAUSED"
|
||||||
|
Anilist.DROPPED -> "DROPPED"
|
||||||
|
Anilist.PLAN_TO_READ -> "PLANNING"
|
||||||
|
Anilist.REREADING -> "REPEATING"
|
||||||
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.toApiScore(scoreType: String?): String =
|
||||||
|
when (scoreType) {
|
||||||
|
// 10 point
|
||||||
|
"POINT_10" -> (score.toInt() / 10).toString()
|
||||||
|
// 100 point
|
||||||
|
"POINT_100" -> score.toInt().toString()
|
||||||
|
// 5 stars
|
||||||
|
"POINT_5" ->
|
||||||
|
when {
|
||||||
|
score == 0.0 -> "0"
|
||||||
|
score < 30 -> "1"
|
||||||
|
score < 50 -> "2"
|
||||||
|
score < 70 -> "3"
|
||||||
|
score < 90 -> "4"
|
||||||
|
else -> "5"
|
||||||
|
}
|
||||||
|
// Smiley
|
||||||
|
"POINT_3" ->
|
||||||
|
when {
|
||||||
|
score == 0.0 -> "0"
|
||||||
|
score <= 35 -> ":("
|
||||||
|
score <= 60 -> ":|"
|
||||||
|
else -> ":)"
|
||||||
|
}
|
||||||
|
// 10 point decimal
|
||||||
|
"POINT_10_DECIMAL" -> (score / 10).toString()
|
||||||
|
else -> throw NotImplementedError("Unknown score type")
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALAddMangaResult(
|
||||||
|
val data: ALAddMangaData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALAddMangaData(
|
||||||
|
@SerialName("SaveMediaListEntry")
|
||||||
|
val entry: ALAddMangaEntry,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALAddMangaEntry(
|
||||||
|
val id: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.ZoneId
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALFuzzyDate(
|
||||||
|
val year: Int?,
|
||||||
|
val month: Int?,
|
||||||
|
val day: Int?,
|
||||||
|
) {
|
||||||
|
fun toEpochMilli(): Long =
|
||||||
|
try {
|
||||||
|
LocalDate
|
||||||
|
.of(year!!, month!!, day!!)
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.toEpochMilli()
|
||||||
|
} catch (_: Exception) {
|
||||||
|
0L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.Track.htmlDecode
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.Anilist
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.anilist.AnilistApi
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
data class ALManga(
|
||||||
|
val remoteId: Long,
|
||||||
|
val title: String,
|
||||||
|
val imageUrl: String,
|
||||||
|
val description: String?,
|
||||||
|
val format: String,
|
||||||
|
val publishingStatus: String,
|
||||||
|
val startDateFuzzy: Long,
|
||||||
|
val totalChapters: Int,
|
||||||
|
val averageScore: Int,
|
||||||
|
val staff: ALStaff,
|
||||||
|
) {
|
||||||
|
fun toTrack() =
|
||||||
|
TrackSearch.create(TrackerManager.ANILIST).apply {
|
||||||
|
remote_id = remoteId
|
||||||
|
title = this@ALManga.title
|
||||||
|
total_chapters = totalChapters
|
||||||
|
cover_url = imageUrl
|
||||||
|
summary = description?.htmlDecode() ?: ""
|
||||||
|
score = averageScore.toDouble()
|
||||||
|
tracking_url = AnilistApi.mangaUrl(remote_id)
|
||||||
|
publishing_status = publishingStatus
|
||||||
|
publishing_type = format
|
||||||
|
if (startDateFuzzy != 0L) {
|
||||||
|
start_date =
|
||||||
|
try {
|
||||||
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
outputDf.format(startDateFuzzy)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
staff.edges.forEach {
|
||||||
|
val name = it.node.name() ?: return@forEach
|
||||||
|
if ("Story" in it.role) authors += name
|
||||||
|
if ("Art" in it.role) artists += name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ALUserManga(
|
||||||
|
val libraryId: Long,
|
||||||
|
val listStatus: String,
|
||||||
|
val scoreRaw: Int,
|
||||||
|
val chaptersRead: Int,
|
||||||
|
val startDateFuzzy: Long,
|
||||||
|
val completedDateFuzzy: Long,
|
||||||
|
val manga: ALManga,
|
||||||
|
val private: Boolean,
|
||||||
|
) {
|
||||||
|
fun toTrack() =
|
||||||
|
Track.create(TrackerManager.ANILIST).apply {
|
||||||
|
remote_id = manga.remoteId
|
||||||
|
title = manga.title
|
||||||
|
status = toTrackStatus()
|
||||||
|
score = scoreRaw.toDouble()
|
||||||
|
started_reading_date = startDateFuzzy
|
||||||
|
finished_reading_date = completedDateFuzzy
|
||||||
|
last_chapter_read = chaptersRead.toDouble()
|
||||||
|
library_id = libraryId
|
||||||
|
total_chapters = manga.totalChapters
|
||||||
|
private = this@ALUserManga.private
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toTrackStatus() =
|
||||||
|
when (listStatus) {
|
||||||
|
"CURRENT" -> Anilist.READING
|
||||||
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
|
"PAUSED" -> Anilist.ON_HOLD
|
||||||
|
"DROPPED" -> Anilist.DROPPED
|
||||||
|
"PLANNING" -> Anilist.PLAN_TO_READ
|
||||||
|
"REPEATING" -> Anilist.REREADING
|
||||||
|
else -> throw NotImplementedError("Unknown status: $listStatus")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALOAuth(
|
||||||
|
@SerialName("access_token")
|
||||||
|
val accessToken: String,
|
||||||
|
@SerialName("token_type")
|
||||||
|
val tokenType: String,
|
||||||
|
val expires: Long,
|
||||||
|
@SerialName("expires_in")
|
||||||
|
val expiresIn: Long,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ALOAuth.isExpired() = System.currentTimeMillis() > expires
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALSearchResult(
|
||||||
|
val data: ALSearchPage,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALSearchPage(
|
||||||
|
@SerialName("Page")
|
||||||
|
val page: ALSearchMedia,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALSearchMedia(
|
||||||
|
val media: List<ALSearchItem>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALSearchItem(
|
||||||
|
val id: Long,
|
||||||
|
val title: ALItemTitle,
|
||||||
|
val coverImage: ItemCover,
|
||||||
|
val description: String?,
|
||||||
|
val format: String,
|
||||||
|
val status: String?,
|
||||||
|
val startDate: ALFuzzyDate,
|
||||||
|
val chapters: Int?,
|
||||||
|
val averageScore: Int?,
|
||||||
|
val staff: ALStaff,
|
||||||
|
) {
|
||||||
|
fun toALManga(): ALManga =
|
||||||
|
ALManga(
|
||||||
|
remoteId = id,
|
||||||
|
title = title.userPreferred,
|
||||||
|
imageUrl = coverImage.large,
|
||||||
|
description = description,
|
||||||
|
format = format.replace("_", "-"),
|
||||||
|
publishingStatus = status ?: "",
|
||||||
|
startDateFuzzy = startDate.toEpochMilli(),
|
||||||
|
totalChapters = chapters ?: 0,
|
||||||
|
averageScore = averageScore ?: -1,
|
||||||
|
staff = staff,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALItemTitle(
|
||||||
|
val userPreferred: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ItemCover(
|
||||||
|
val large: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALStaff(
|
||||||
|
val edges: List<ALEdge>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALEdge(
|
||||||
|
val role: String,
|
||||||
|
val id: Int,
|
||||||
|
val node: ALStaffNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALStaffNode(
|
||||||
|
val name: ALStaffName,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALStaffName(
|
||||||
|
val userPreferred: String?,
|
||||||
|
val native: String?,
|
||||||
|
val full: String?,
|
||||||
|
) {
|
||||||
|
operator fun invoke(): String? = userPreferred ?: full ?: native
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALCurrentUserResult(
|
||||||
|
val data: ALUserViewer,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserViewer(
|
||||||
|
@SerialName("Viewer")
|
||||||
|
val viewer: ALUserViewerData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserViewerData(
|
||||||
|
val id: Int,
|
||||||
|
val mediaListOptions: ALUserListOptions,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserListOptions(
|
||||||
|
val scoreFormat: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.anilist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserListMangaQueryResult(
|
||||||
|
val data: ALUserListMangaPage,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserListMangaPage(
|
||||||
|
@SerialName("Page")
|
||||||
|
val page: ALUserListMediaList,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserListMediaList(
|
||||||
|
val mediaList: List<ALUserListItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ALUserListItem(
|
||||||
|
val id: Long,
|
||||||
|
val status: String,
|
||||||
|
val scoreRaw: Int,
|
||||||
|
val progress: Int,
|
||||||
|
val startedAt: ALFuzzyDate,
|
||||||
|
val completedAt: ALFuzzyDate,
|
||||||
|
val media: ALSearchItem,
|
||||||
|
val private: Boolean,
|
||||||
|
) {
|
||||||
|
fun toALUserManga(): ALUserManga =
|
||||||
|
ALUserManga(
|
||||||
|
libraryId = this@ALUserListItem.id,
|
||||||
|
listStatus = status,
|
||||||
|
scoreRaw = scoreRaw,
|
||||||
|
chaptersRead = progress,
|
||||||
|
startDateFuzzy = startedAt.toEpochMilli(),
|
||||||
|
completedDateFuzzy = completedAt.toEpochMilli(),
|
||||||
|
manga = media.toALManga(),
|
||||||
|
private = private,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.bangumi
|
|||||||
import android.annotation.StringRes
|
import android.annotation.StringRes
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.BGMOAuth
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
@@ -24,14 +25,14 @@ class Bangumi(
|
|||||||
.map(Int::toString)
|
.map(Int::toString)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsTrackDeletion: Boolean = false
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { BangumiInterceptor(this) }
|
private val interceptor by lazy { BangumiInterceptor(this) }
|
||||||
|
|
||||||
private val api by lazy { BangumiApi(id, client, interceptor) }
|
private val api by lazy { BangumiApi(id, client, interceptor) }
|
||||||
|
|
||||||
|
override val supportsPrivateTracking: Boolean = true
|
||||||
|
|
||||||
override fun getScoreList(): List<String> = SCORE_LIST
|
override fun getScoreList(): List<String> = SCORE_LIST
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = track.score.toInt().toString()
|
override fun displayScore(track: Track): String = track.score.toInt().toString()
|
||||||
@@ -61,7 +62,7 @@ class Bangumi(
|
|||||||
): Track {
|
): Track {
|
||||||
val statusTrack = api.statusLibManga(track, getUsername())
|
val statusTrack = api.statusLibManga(track, getUsername())
|
||||||
return if (statusTrack != null) {
|
return if (statusTrack != null) {
|
||||||
track.copyPersonalFrom(statusTrack)
|
track.copyPersonalFrom(statusTrack, copyRemotePrivate = false)
|
||||||
track.library_id = statusTrack.library_id
|
track.library_id = statusTrack.library_id
|
||||||
track.score = statusTrack.score
|
track.score = statusTrack.score
|
||||||
track.last_chapter_read = statusTrack.last_chapter_read
|
track.last_chapter_read = statusTrack.last_chapter_read
|
||||||
@@ -74,7 +75,7 @@ class Bangumi(
|
|||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0.0F
|
track.score = 0.0
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,13 +152,3 @@ class Bangumi(
|
|||||||
interceptor.newAuth(null)
|
interceptor.newAuth(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Track.toApiStatus() =
|
|
||||||
when (status) {
|
|
||||||
Bangumi.PLAN_TO_READ -> 1
|
|
||||||
Bangumi.COMPLETED -> 2
|
|
||||||
Bangumi.READING -> 3
|
|
||||||
Bangumi.ON_HOLD -> 4
|
|
||||||
Bangumi.DROPPED -> 5
|
|
||||||
else -> throw NotImplementedError("Unknown status: $status")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import okhttp3.Headers.Companion.headersOf
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.BGMCollectionResponse
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.BGMOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.BGMSearchResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.BGMUser
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -35,27 +39,33 @@ class BangumiApi(
|
|||||||
|
|
||||||
suspend fun addLibManga(track: Track): Track =
|
suspend fun addLibManga(track: Track): Track =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val url = "$API_URL/v0/users/-/collections/${track.media_id}"
|
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
|
||||||
val body =
|
val body =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("type", track.toApiStatus())
|
put("type", track.toApiStatus())
|
||||||
put("rate", track.score.toInt().coerceIn(0, 10))
|
put("rate", track.score.toInt().coerceIn(0, 10))
|
||||||
put("ep_status", track.last_chapter_read.toInt())
|
put("ep_status", track.last_chapter_read.toInt())
|
||||||
}.toString().toRequestBody()
|
put("private", track.private)
|
||||||
|
}.toString()
|
||||||
|
.toRequestBody()
|
||||||
// Returns with 202 Accepted on success with no body
|
// Returns with 202 Accepted on success with no body
|
||||||
authClient.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON))).awaitSuccess()
|
authClient
|
||||||
|
.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
|
||||||
|
.awaitSuccess()
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateLibManga(track: Track): Track =
|
suspend fun updateLibManga(track: Track): Track =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val url = "$API_URL/v0/users/-/collections/${track.media_id}"
|
val url = "$API_URL/v0/users/-/collections/${track.remote_id}"
|
||||||
val body =
|
val body =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("type", track.toApiStatus())
|
put("type", track.toApiStatus())
|
||||||
put("rate", track.score.toInt().coerceIn(0, 10))
|
put("rate", track.score.toInt().coerceIn(0, 10))
|
||||||
put("ep_status", track.last_chapter_read.toInt())
|
put("ep_status", track.last_chapter_read.toInt())
|
||||||
}.toString().toRequestBody()
|
put("private", track.private)
|
||||||
|
}.toString()
|
||||||
|
.toRequestBody()
|
||||||
|
|
||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
@@ -65,7 +75,9 @@ class BangumiApi(
|
|||||||
.headers(headersOf("Content-Type", APP_JSON))
|
.headers(headersOf("Content-Type", APP_JSON))
|
||||||
.build()
|
.build()
|
||||||
// Returns with 204 No Content
|
// Returns with 204 No Content
|
||||||
authClient.newCall(request).awaitSuccess()
|
authClient
|
||||||
|
.newCall(request)
|
||||||
|
.awaitSuccess()
|
||||||
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
@@ -86,7 +98,8 @@ class BangumiApi(
|
|||||||
add(1) // "Book" (书籍) type
|
add(1) // "Book" (书籍) type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.toString().toRequestBody()
|
}.toString()
|
||||||
|
.toRequestBody()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
|
.newCall(POST(url, body = body, headers = headersOf("Content-Type", APP_JSON)))
|
||||||
@@ -104,7 +117,7 @@ class BangumiApi(
|
|||||||
username: String,
|
username: String,
|
||||||
): Track? =
|
): Track? =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val url = "$API_URL/v0/users/$username/collections/${track.media_id}"
|
val url = "$API_URL/v0/users/$username/collections/${track.remote_id}"
|
||||||
with(json) {
|
with(json) {
|
||||||
try {
|
try {
|
||||||
authClient
|
authClient
|
||||||
@@ -113,8 +126,8 @@ class BangumiApi(
|
|||||||
.parseAs<BGMCollectionResponse>()
|
.parseAs<BGMCollectionResponse>()
|
||||||
.let {
|
.let {
|
||||||
track.status = it.getStatus()
|
track.status = it.getStatus()
|
||||||
track.last_chapter_read = it.epStatus?.toFloat() ?: 0.0F
|
track.last_chapter_read = it.epStatus?.toDouble() ?: 0.0
|
||||||
track.score = it.rate?.toFloat() ?: 0.0F
|
track.score = it.rate?.toDouble() ?: 0.0
|
||||||
track.total_chapters = it.subject?.eps ?: 0
|
track.total_chapters = it.subject?.eps ?: 0
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
@@ -140,7 +153,10 @@ class BangumiApi(
|
|||||||
.add("redirect_uri", REDIRECT_URL)
|
.add("redirect_uri", REDIRECT_URL)
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
client.newCall(POST(OAUTH_URL, body = body)).awaitSuccess().parseAs<BGMOAuth>()
|
client
|
||||||
|
.newCall(POST(OAUTH_URL, body = body))
|
||||||
|
.awaitSuccess()
|
||||||
|
.parseAs<BGMOAuth>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package suwayomi.tachidesk.manga.impl.track.tracker.bangumi
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.BGMOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto.isExpired
|
||||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ class BangumiInterceptor(
|
|||||||
.newBuilder()
|
.newBuilder()
|
||||||
.header(
|
.header(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
"Suwayomi/Suwayomi-Server/v${BuildConfig.VERSION} (${BuildConfig.GITHUB})",
|
"Suwayomi/Suwayomi-Server/${BuildConfig.VERSION} (${BuildConfig.GITHUB})",
|
||||||
).apply {
|
).apply {
|
||||||
addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
addHeader("Authorization", "Bearer ${currAuth.accessToken}")
|
||||||
}.build()
|
}.build()
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.bangumi
|
|
||||||
|
|
||||||
import kotlinx.serialization.EncodeDefault
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
// Incomplete DTO with only our needed attributes
|
|
||||||
data class BGMUser(
|
|
||||||
val username: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BGMSearchResult(
|
|
||||||
val total: Int,
|
|
||||||
val limit: Int,
|
|
||||||
val offset: Int,
|
|
||||||
val data: List<BGMSubject> = emptyList(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
// Incomplete DTO with only our needed attributes
|
|
||||||
data class BGMSubject(
|
|
||||||
val id: Long,
|
|
||||||
@SerialName("name_cn")
|
|
||||||
val nameCn: String,
|
|
||||||
val name: String,
|
|
||||||
val summary: String?,
|
|
||||||
val date: String?, // YYYY-MM-DD
|
|
||||||
val images: BGMSubjectImages?,
|
|
||||||
val volumes: Long = 0,
|
|
||||||
val eps: Long = 0,
|
|
||||||
val rating: BGMSubjectRating?,
|
|
||||||
val platform: String?,
|
|
||||||
) {
|
|
||||||
fun toTrackSearch(trackId: Int): TrackSearch =
|
|
||||||
TrackSearch.create(TrackerManager.BANGUMI).apply {
|
|
||||||
media_id = this@BGMSubject.id
|
|
||||||
title = nameCn.ifBlank { name }
|
|
||||||
cover_url = images?.common.orEmpty()
|
|
||||||
summary =
|
|
||||||
if (nameCn.isNotBlank()) {
|
|
||||||
"作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty()
|
|
||||||
} else {
|
|
||||||
this@BGMSubject.summary?.trim().orEmpty()
|
|
||||||
}
|
|
||||||
tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}"
|
|
||||||
total_chapters = eps.toInt()
|
|
||||||
start_date = date ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
// Incomplete DTO with only our needed attributes
|
|
||||||
data class BGMSubjectImages(
|
|
||||||
val common: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
// Incomplete DTO with only our needed attributes
|
|
||||||
data class BGMSubjectRating(
|
|
||||||
val score: Double?,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BGMOAuth(
|
|
||||||
@SerialName("access_token")
|
|
||||||
val accessToken: String,
|
|
||||||
@SerialName("token_type")
|
|
||||||
val tokenType: String,
|
|
||||||
@SerialName("created_at")
|
|
||||||
@EncodeDefault
|
|
||||||
val createdAt: Long = System.currentTimeMillis() / 1000,
|
|
||||||
@SerialName("expires_in")
|
|
||||||
val expiresIn: Long,
|
|
||||||
@SerialName("refresh_token")
|
|
||||||
val refreshToken: String?,
|
|
||||||
@SerialName("user_id")
|
|
||||||
val userId: Long?,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Access token refresh before expired
|
|
||||||
fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
// Incomplete DTO with only our needed attributes
|
|
||||||
data class BGMCollectionResponse(
|
|
||||||
val rate: Int?,
|
|
||||||
val type: Int?,
|
|
||||||
@SerialName("ep_status")
|
|
||||||
val epStatus: Int? = 0,
|
|
||||||
@SerialName("vol_status")
|
|
||||||
val volStatus: Int? = 0,
|
|
||||||
val private: Boolean = false,
|
|
||||||
val subject: BGMSlimSubject? = null,
|
|
||||||
) {
|
|
||||||
fun getStatus(): Int =
|
|
||||||
when (type) {
|
|
||||||
1 -> Bangumi.PLAN_TO_READ
|
|
||||||
2 -> Bangumi.COMPLETED
|
|
||||||
3 -> Bangumi.READING
|
|
||||||
4 -> Bangumi.ON_HOLD
|
|
||||||
5 -> Bangumi.DROPPED
|
|
||||||
else -> throw NotImplementedError("Unknown status: $type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
// Incomplete DTO with only our needed attributes
|
|
||||||
data class BGMSlimSubject(
|
|
||||||
val volumes: Int?,
|
|
||||||
val eps: Int?,
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.bangumi
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
|
fun Track.toApiStatus() =
|
||||||
|
when (status) {
|
||||||
|
Bangumi.PLAN_TO_READ -> 1
|
||||||
|
Bangumi.COMPLETED -> 2
|
||||||
|
Bangumi.READING -> 3
|
||||||
|
Bangumi.ON_HOLD -> 4
|
||||||
|
Bangumi.DROPPED -> 5
|
||||||
|
else -> throw NotImplementedError("Unknown status: $status")
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.bangumi.Bangumi
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMCollectionResponse(
|
||||||
|
val rate: Int?,
|
||||||
|
val type: Int?,
|
||||||
|
@SerialName("ep_status")
|
||||||
|
val epStatus: Int? = 0,
|
||||||
|
@SerialName("vol_status")
|
||||||
|
val volStatus: Int? = 0,
|
||||||
|
val private: Boolean = false,
|
||||||
|
val subject: BGMSlimSubject? = null,
|
||||||
|
) {
|
||||||
|
fun getStatus(): Int =
|
||||||
|
when (type) {
|
||||||
|
1 -> Bangumi.PLAN_TO_READ
|
||||||
|
2 -> Bangumi.COMPLETED
|
||||||
|
3 -> Bangumi.READING
|
||||||
|
4 -> Bangumi.ON_HOLD
|
||||||
|
5 -> Bangumi.DROPPED
|
||||||
|
else -> throw NotImplementedError("Unknown status: $type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSlimSubject(
|
||||||
|
val volumes: Int?,
|
||||||
|
val eps: Int?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BGMOAuth(
|
||||||
|
@SerialName("access_token")
|
||||||
|
val accessToken: String,
|
||||||
|
@SerialName("token_type")
|
||||||
|
val tokenType: String,
|
||||||
|
@SerialName("created_at")
|
||||||
|
@EncodeDefault
|
||||||
|
val createdAt: Long = System.currentTimeMillis() / 1000,
|
||||||
|
@SerialName("expires_in")
|
||||||
|
val expiresIn: Long,
|
||||||
|
@SerialName("refresh_token")
|
||||||
|
val refreshToken: String?,
|
||||||
|
@SerialName("user_id")
|
||||||
|
val userId: Long?,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Access token refresh before expired
|
||||||
|
fun BGMOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BGMSearchResult(
|
||||||
|
val total: Int,
|
||||||
|
val limit: Int,
|
||||||
|
val offset: Int,
|
||||||
|
val data: List<BGMSubject> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSubject(
|
||||||
|
val id: Long,
|
||||||
|
@SerialName("name_cn")
|
||||||
|
val nameCn: String,
|
||||||
|
val name: String,
|
||||||
|
val summary: String?,
|
||||||
|
val date: String?, // YYYY-MM-DD
|
||||||
|
val images: BGMSubjectImages?,
|
||||||
|
val volumes: Long = 0,
|
||||||
|
val eps: Int = 0,
|
||||||
|
val rating: BGMSubjectRating?,
|
||||||
|
val platform: String?,
|
||||||
|
) {
|
||||||
|
fun toTrackSearch(trackId: Int): TrackSearch =
|
||||||
|
TrackSearch.create(trackId).apply {
|
||||||
|
remote_id = this@BGMSubject.id
|
||||||
|
title = nameCn.ifBlank { name }
|
||||||
|
cover_url = images?.common.orEmpty()
|
||||||
|
summary =
|
||||||
|
if (nameCn.isNotBlank()) {
|
||||||
|
"作品原名:$name" + this@BGMSubject.summary?.let { "\n${it.trim()}" }.orEmpty()
|
||||||
|
} else {
|
||||||
|
this@BGMSubject.summary?.trim().orEmpty()
|
||||||
|
}
|
||||||
|
score = rating?.score ?: -1.0
|
||||||
|
tracking_url = "https://bangumi.tv/subject/${this@BGMSubject.id}"
|
||||||
|
total_chapters = eps
|
||||||
|
start_date = date ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSubjectImages(
|
||||||
|
val common: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMSubjectRating(
|
||||||
|
val score: Double?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.bangumi.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
// Incomplete DTO with only our needed attributes
|
||||||
|
data class BGMUser(
|
||||||
|
val username: String,
|
||||||
|
)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
||||||
|
|
||||||
import android.annotation.StringRes
|
import android.annotation.StringRes
|
||||||
import kotlinx.serialization.encodeToString
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTrackService
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
@@ -13,7 +13,7 @@ import java.text.DecimalFormat
|
|||||||
class Kitsu(
|
class Kitsu(
|
||||||
id: Int,
|
id: Int,
|
||||||
) : Tracker(id, "Kitsu"),
|
) : Tracker(id, "Kitsu"),
|
||||||
DeletableTrackService {
|
DeletableTracker {
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
@@ -22,10 +22,10 @@ class Kitsu(
|
|||||||
const val PLAN_TO_READ = 5
|
const val PLAN_TO_READ = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsTrackDeletion: Boolean = true
|
|
||||||
|
|
||||||
override val supportsReadingDates: Boolean = true
|
override val supportsReadingDates: Boolean = true
|
||||||
|
|
||||||
|
override val supportsPrivateTracking: Boolean = true
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { KitsuInterceptor(this) }
|
private val interceptor by lazy { KitsuInterceptor(this) }
|
||||||
@@ -58,7 +58,7 @@ class Kitsu(
|
|||||||
return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }
|
return listOf("0") + IntRange(2, 20).map { df.format(it / 2f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun indexToScore(index: Int): Float = if (index > 0) (index + 1) / 2.0f else 0.0f
|
override fun indexToScore(index: Int): Double = if (index > 0) (index + 1) / 2.0 else 0.0
|
||||||
|
|
||||||
override fun displayScore(track: Track): String {
|
override fun displayScore(track: Track): String {
|
||||||
val df = DecimalFormat("0.#")
|
val df = DecimalFormat("0.#")
|
||||||
@@ -78,7 +78,7 @@ class Kitsu(
|
|||||||
track.finished_reading_date = System.currentTimeMillis()
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
} else {
|
} else {
|
||||||
track.status = READING
|
track.status = READING
|
||||||
if (track.last_chapter_read == 1.0f) {
|
if (track.last_chapter_read == 1.0) {
|
||||||
track.started_reading_date = System.currentTimeMillis()
|
track.started_reading_date = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,8 +98,8 @@ class Kitsu(
|
|||||||
): Track {
|
): Track {
|
||||||
val remoteTrack = api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
|
||||||
track.media_id = remoteTrack.media_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
track.status = if (hasReadChapters) READING else track.status
|
track.status = if (hasReadChapters) READING else track.status
|
||||||
@@ -108,7 +108,7 @@ class Kitsu(
|
|||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0.0f
|
track.score = 0.0
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,14 +140,14 @@ class Kitsu(
|
|||||||
private fun getUserId(): String = getPassword()
|
private fun getUserId(): String = getPassword()
|
||||||
|
|
||||||
// TODO: this seems to be called saveOAuth in other trackers
|
// TODO: this seems to be called saveOAuth in other trackers
|
||||||
fun saveToken(oauth: OAuth?) {
|
fun saveToken(oauth: KitsuOAuth?) {
|
||||||
trackPreferences.setTrackToken(this, json.encodeToString(oauth))
|
trackPreferences.setTrackToken(this, json.encodeToString(oauth))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this seems to be called loadOAuth in other trackers
|
// TODO: this seems to be called loadOAuth in other trackers
|
||||||
fun restoreToken(): OAuth? =
|
fun restoreToken(): KitsuOAuth? =
|
||||||
try {
|
try {
|
||||||
json.decodeFromString<OAuth>(trackPreferences.getTrackToken(this)!!)
|
json.decodeFromString<KitsuOAuth>(trackPreferences.getTrackToken(this)!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
||||||
|
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
@@ -9,12 +10,7 @@ import eu.kanade.tachiyomi.network.jsonMime
|
|||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
import kotlinx.serialization.json.buildJsonObject
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import kotlinx.serialization.json.put
|
import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
@@ -24,6 +20,11 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto.KitsuAddMangaResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto.KitsuAlgoliaSearchResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto.KitsuCurrentUserResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto.KitsuListSearchResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto.KitsuSearchResult
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@@ -48,8 +49,9 @@ class KitsuApi(
|
|||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("type", "libraryEntries")
|
put("type", "libraryEntries")
|
||||||
putJsonObject("attributes") {
|
putJsonObject("attributes") {
|
||||||
put("status", track.toKitsuStatus())
|
put("status", track.toApiStatus())
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
|
put("private", track.private)
|
||||||
}
|
}
|
||||||
putJsonObject("relationships") {
|
putJsonObject("relationships") {
|
||||||
putJsonObject("user") {
|
putJsonObject("user") {
|
||||||
@@ -60,7 +62,7 @@ class KitsuApi(
|
|||||||
}
|
}
|
||||||
putJsonObject("media") {
|
putJsonObject("media") {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
put("type", "manga")
|
put("type", "manga")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,20 +75,13 @@ class KitsuApi(
|
|||||||
.newCall(
|
.newCall(
|
||||||
POST(
|
POST(
|
||||||
"${BASE_URL}library-entries",
|
"${BASE_URL}library-entries",
|
||||||
headers =
|
headers = headersOf("Content-Type", VND_API_JSON),
|
||||||
headersOf(
|
body = data.toString().toRequestBody(VND_JSON_MEDIA_TYPE),
|
||||||
"Content-Type",
|
|
||||||
"application/vnd.api+json",
|
|
||||||
),
|
|
||||||
body =
|
|
||||||
data
|
|
||||||
.toString()
|
|
||||||
.toRequestBody("application/vnd.api+json".toMediaType()),
|
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<KitsuAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.media_id = it["data"]!!.jsonObject["id"]!!.jsonPrimitive.long
|
track.remote_id = it.data.id
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,50 +93,39 @@ class KitsuApi(
|
|||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
putJsonObject("data") {
|
putJsonObject("data") {
|
||||||
put("type", "libraryEntries")
|
put("type", "libraryEntries")
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
putJsonObject("attributes") {
|
putJsonObject("attributes") {
|
||||||
put("status", track.toKitsuStatus())
|
put("status", track.toApiStatus())
|
||||||
put("progress", track.last_chapter_read.toInt())
|
put("progress", track.last_chapter_read.toInt())
|
||||||
put("ratingTwenty", track.toKitsuScore())
|
put("ratingTwenty", track.toApiScore())
|
||||||
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
put("startedAt", KitsuDateHelper.convert(track.started_reading_date))
|
||||||
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
put("finishedAt", KitsuDateHelper.convert(track.finished_reading_date))
|
||||||
|
put("private", track.private)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
with(json) {
|
|
||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
Request
|
Request
|
||||||
.Builder()
|
.Builder()
|
||||||
.url("${BASE_URL}library-entries/${track.media_id}")
|
.url("${BASE_URL}library-entries/${track.remote_id}")
|
||||||
.headers(
|
.headers(
|
||||||
headersOf(
|
headersOf("Content-Type", VND_API_JSON),
|
||||||
"Content-Type",
|
).patch(data.toString().toRequestBody(VND_JSON_MEDIA_TYPE))
|
||||||
"application/vnd.api+json",
|
.build(),
|
||||||
),
|
|
||||||
).patch(
|
|
||||||
data.toString().toRequestBody("application/vnd.api+json".toMediaType()),
|
|
||||||
).build(),
|
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
|
||||||
.let {
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun removeLibManga(track: Track) {
|
suspend fun removeLibManga(track: Track) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
DELETE(
|
DELETE(
|
||||||
"${BASE_URL}library-entries/${track.media_id}",
|
"${BASE_URL}library-entries/${track.remote_id}",
|
||||||
headers =
|
headers = headersOf("Content-Type", VND_API_JSON),
|
||||||
headersOf(
|
|
||||||
"Content-Type",
|
|
||||||
"application/vnd.api+json",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
}
|
}
|
||||||
@@ -153,10 +137,9 @@ class KitsuApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(GET(ALGOLIA_KEY_URL))
|
.newCall(GET(ALGOLIA_KEY_URL))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<KitsuSearchResult>()
|
||||||
.let {
|
.let {
|
||||||
val key = it["media"]!!.jsonObject["key"]!!.jsonPrimitive.content
|
algoliaSearch(it.media.key, query)
|
||||||
algoliaSearch(key, query)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,16 +169,12 @@ class KitsuApi(
|
|||||||
body = jsonObject.toString().toRequestBody(jsonMime),
|
body = jsonObject.toString().toRequestBody(jsonMime),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<KitsuAlgoliaSearchResult>()
|
||||||
.let {
|
.hits
|
||||||
it["hits"]!!
|
.filter { it.subtype != "novel" }
|
||||||
.jsonArray
|
|
||||||
.map { KitsuSearchManga(it.jsonObject) }
|
|
||||||
.filter { it.subType != "novel" }
|
|
||||||
.map { it.toTrack() }
|
.map { it.toTrack() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun findLibManga(
|
suspend fun findLibManga(
|
||||||
track: Track,
|
track: Track,
|
||||||
@@ -206,19 +185,17 @@ class KitsuApi(
|
|||||||
"${BASE_URL}library-entries"
|
"${BASE_URL}library-entries"
|
||||||
.toUri()
|
.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.encodedQuery("filter[manga_id]=${track.media_id}&filter[user_id]=$userId")
|
.encodedQuery("filter[manga_id]=${track.remote_id}&filter[user_id]=$userId")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(GET(url.toString()))
|
.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<KitsuListSearchResult>()
|
||||||
.let {
|
.let {
|
||||||
val data = it["data"]!!.jsonArray
|
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
|
||||||
if (data.size > 0) {
|
it.firstToTrack()
|
||||||
val manga = it["included"]!!.jsonArray[0].jsonObject
|
|
||||||
KitsuLibManga(data[0].jsonObject, manga).toTrack()
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -232,19 +209,17 @@ class KitsuApi(
|
|||||||
"${BASE_URL}library-entries"
|
"${BASE_URL}library-entries"
|
||||||
.toUri()
|
.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.encodedQuery("filter[id]=${track.media_id}")
|
.encodedQuery("filter[id]=${track.remote_id}")
|
||||||
.appendQueryParameter("include", "manga")
|
.appendQueryParameter("include", "manga")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(GET(url.toString()))
|
.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<KitsuListSearchResult>()
|
||||||
.let {
|
.let {
|
||||||
val data = it["data"]!!.jsonArray
|
if (it.data.isNotEmpty() && it.included.isNotEmpty()) {
|
||||||
if (data.size > 0) {
|
it.firstToTrack()
|
||||||
val manga = it["included"]!!.jsonArray[0].jsonObject
|
|
||||||
KitsuLibManga(data[0].jsonObject, manga).toTrack()
|
|
||||||
} else {
|
} else {
|
||||||
throw Exception("Could not find manga")
|
throw Exception("Could not find manga")
|
||||||
}
|
}
|
||||||
@@ -255,7 +230,7 @@ class KitsuApi(
|
|||||||
suspend fun login(
|
suspend fun login(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): OAuth =
|
): KitsuOAuth =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val formBody: RequestBody =
|
val formBody: RequestBody =
|
||||||
FormBody
|
FormBody
|
||||||
@@ -286,13 +261,9 @@ class KitsuApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(GET(url.toString()))
|
.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<KitsuCurrentUserResult>()
|
||||||
.let {
|
.data[0]
|
||||||
it["data"]!!
|
.id
|
||||||
.jsonArray[0]
|
|
||||||
.jsonObject["id"]!!
|
|
||||||
.jsonPrimitive.content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +283,9 @@ class KitsuApi(
|
|||||||
"%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
|
"%5B%22synopsis%22%2C%22averageRating%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22" +
|
||||||
"posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
"posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||||
|
|
||||||
|
private const val VND_API_JSON = "application/vnd.api+json"
|
||||||
|
private val VND_JSON_MEDIA_TYPE = VND_API_JSON.toMediaType()
|
||||||
|
|
||||||
fun mangaUrl(remoteId: Long): String = BASE_MANGA_URL + remoteId
|
fun mangaUrl(remoteId: Long): String = BASE_MANGA_URL + remoteId
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) =
|
fun refreshTokenRequest(token: String) =
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.KitsuOAuth
|
||||||
|
import eu.kanade.tachiyomi.data.track.kitsu.dto.isExpired
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -14,14 +16,14 @@ class KitsuInterceptor(
|
|||||||
/**
|
/**
|
||||||
* OAuth object used for authenticated requests.
|
* OAuth object used for authenticated requests.
|
||||||
*/
|
*/
|
||||||
private var oauth: OAuth? = kitsu.restoreToken()
|
private var oauth: KitsuOAuth? = kitsu.restoreToken()
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
val currAuth = oauth ?: throw Exception("Not authenticated with Kitsu")
|
||||||
|
|
||||||
val refreshToken = currAuth.refresh_token!!
|
val refreshToken = currAuth.refreshToken!!
|
||||||
|
|
||||||
// Refresh access token if expired.
|
// Refresh access token if expired.
|
||||||
if (currAuth.isExpired()) {
|
if (currAuth.isExpired()) {
|
||||||
@@ -37,7 +39,7 @@ class KitsuInterceptor(
|
|||||||
val authRequest =
|
val authRequest =
|
||||||
originalRequest
|
originalRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
||||||
.header("User-Agent", "Suwayomi ${BuildConfig.VERSION} (${BuildConfig.REVISION})")
|
.header("User-Agent", "Suwayomi ${BuildConfig.VERSION} (${BuildConfig.REVISION})")
|
||||||
.header("Accept", "application/vnd.api+json")
|
.header("Accept", "application/vnd.api+json")
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
@@ -46,7 +48,7 @@ class KitsuInterceptor(
|
|||||||
return chain.proceed(authRequest)
|
return chain.proceed(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newAuth(oauth: OAuth?) {
|
fun newAuth(oauth: KitsuOAuth?) {
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
kitsu.saveToken(oauth)
|
kitsu.saveToken(oauth)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.intOrNull
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
class KitsuSearchManga(
|
|
||||||
obj: JsonObject,
|
|
||||||
) {
|
|
||||||
val id = obj["id"]!!.jsonPrimitive.long
|
|
||||||
private val canonicalTitle = obj["canonicalTitle"]!!.jsonPrimitive.content
|
|
||||||
private val chapterCount = obj["chapterCount"]?.jsonPrimitive?.intOrNull
|
|
||||||
val subType = obj["subtype"]?.jsonPrimitive?.contentOrNull
|
|
||||||
val original =
|
|
||||||
try {
|
|
||||||
obj["posterImage"]
|
|
||||||
?.jsonObject
|
|
||||||
?.get("original")
|
|
||||||
?.jsonPrimitive
|
|
||||||
?.content
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
// posterImage is sometimes a jsonNull object instead
|
|
||||||
null
|
|
||||||
}
|
|
||||||
private val synopsis = obj["synopsis"]?.jsonPrimitive?.contentOrNull
|
|
||||||
private val rating = obj["averageRating"]?.jsonPrimitive?.contentOrNull?.toDoubleOrNull()
|
|
||||||
private var startDate =
|
|
||||||
obj["startDate"]?.jsonPrimitive?.contentOrNull?.let {
|
|
||||||
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
|
||||||
outputDf.format(Date(it.toLong() * 1000))
|
|
||||||
}
|
|
||||||
private val endDate = obj["endDate"]?.jsonPrimitive?.contentOrNull
|
|
||||||
|
|
||||||
fun toTrack() =
|
|
||||||
TrackSearch.create(TrackerManager.KITSU).apply {
|
|
||||||
media_id = this@KitsuSearchManga.id
|
|
||||||
title = canonicalTitle
|
|
||||||
total_chapters = chapterCount ?: 0
|
|
||||||
cover_url = original ?: ""
|
|
||||||
summary = synopsis ?: ""
|
|
||||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
|
||||||
// score = rating ?: -1.0
|
|
||||||
publishing_status =
|
|
||||||
if (endDate == null) {
|
|
||||||
"Publishing"
|
|
||||||
} else {
|
|
||||||
"Finished"
|
|
||||||
}
|
|
||||||
publishing_type = subType ?: ""
|
|
||||||
start_date = startDate ?: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class KitsuLibManga(
|
|
||||||
obj: JsonObject,
|
|
||||||
manga: JsonObject,
|
|
||||||
) {
|
|
||||||
val id = manga["id"]!!.jsonPrimitive.int
|
|
||||||
private val canonicalTitle = manga["attributes"]!!.jsonObject["canonicalTitle"]!!.jsonPrimitive.content
|
|
||||||
private val chapterCount = manga["attributes"]!!.jsonObject["chapterCount"]?.jsonPrimitive?.intOrNull
|
|
||||||
val type =
|
|
||||||
manga["attributes"]!!
|
|
||||||
.jsonObject["mangaType"]
|
|
||||||
?.jsonPrimitive
|
|
||||||
?.contentOrNull
|
|
||||||
.orEmpty()
|
|
||||||
val original =
|
|
||||||
manga["attributes"]!!
|
|
||||||
.jsonObject["posterImage"]!!
|
|
||||||
.jsonObject["original"]!!
|
|
||||||
.jsonPrimitive.content
|
|
||||||
private val synopsis = manga["attributes"]!!.jsonObject["synopsis"]!!.jsonPrimitive.content
|
|
||||||
private val startDate =
|
|
||||||
manga["attributes"]!!
|
|
||||||
.jsonObject["startDate"]
|
|
||||||
?.jsonPrimitive
|
|
||||||
?.contentOrNull
|
|
||||||
.orEmpty()
|
|
||||||
private val startedAt = obj["attributes"]!!.jsonObject["startedAt"]?.jsonPrimitive?.contentOrNull
|
|
||||||
private val finishedAt = obj["attributes"]!!.jsonObject["finishedAt"]?.jsonPrimitive?.contentOrNull
|
|
||||||
private val libraryId = obj["id"]!!.jsonPrimitive.long
|
|
||||||
val status = obj["attributes"]!!.jsonObject["status"]!!.jsonPrimitive.content
|
|
||||||
private val ratingTwenty = obj["attributes"]!!.jsonObject["ratingTwenty"]?.jsonPrimitive?.contentOrNull
|
|
||||||
val progress = obj["attributes"]!!.jsonObject["progress"]!!.jsonPrimitive.int
|
|
||||||
|
|
||||||
fun toTrack() =
|
|
||||||
Track.create(TrackerManager.KITSU).apply {
|
|
||||||
media_id = libraryId
|
|
||||||
title = canonicalTitle
|
|
||||||
total_chapters = chapterCount ?: 0
|
|
||||||
// cover_url = original
|
|
||||||
// summary = synopsis
|
|
||||||
tracking_url = KitsuApi.mangaUrl(media_id)
|
|
||||||
// publishing_status = this@KitsuLibManga.status
|
|
||||||
// publishing_type = type
|
|
||||||
// start_date = startDate
|
|
||||||
started_reading_date = KitsuDateHelper.parse(startedAt)
|
|
||||||
finished_reading_date = KitsuDateHelper.parse(finishedAt)
|
|
||||||
status = toTrackStatus()
|
|
||||||
score = ratingTwenty?.let { it.toInt() / 2.0f } ?: 0.0f
|
|
||||||
last_chapter_read = progress.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toTrackStatus() =
|
|
||||||
when (status) {
|
|
||||||
"current" -> Kitsu.READING
|
|
||||||
"completed" -> Kitsu.COMPLETED
|
|
||||||
"on_hold" -> Kitsu.ON_HOLD
|
|
||||||
"dropped" -> Kitsu.DROPPED
|
|
||||||
"planned" -> Kitsu.PLAN_TO_READ
|
|
||||||
else -> throw Exception("Unknown status")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val created_at: Long,
|
|
||||||
val expires_in: Long,
|
|
||||||
val refresh_token: String?,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun OAuth.isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
|
||||||
|
|
||||||
fun Track.toKitsuStatus() =
|
|
||||||
when (status) {
|
|
||||||
Kitsu.READING -> "current"
|
|
||||||
Kitsu.COMPLETED -> "completed"
|
|
||||||
Kitsu.ON_HOLD -> "on_hold"
|
|
||||||
Kitsu.DROPPED -> "dropped"
|
|
||||||
Kitsu.PLAN_TO_READ -> "planned"
|
|
||||||
else -> throw Exception("Unknown status")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Track.toKitsuScore(): String? = if (score > 0) (score * 2).toInt().toString() else null
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
|
fun Track.toApiStatus() =
|
||||||
|
when (status) {
|
||||||
|
Kitsu.READING -> "current"
|
||||||
|
Kitsu.COMPLETED -> "completed"
|
||||||
|
Kitsu.ON_HOLD -> "on_hold"
|
||||||
|
Kitsu.DROPPED -> "dropped"
|
||||||
|
Kitsu.PLAN_TO_READ -> "planned"
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Track.toApiScore(): String? = if (score > 0) (score * 2).toInt().toString() else null
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuAddMangaResult(
|
||||||
|
val data: KitsuAddMangaItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuAddMangaItem(
|
||||||
|
val id: Long,
|
||||||
|
)
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.Kitsu
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.KitsuApi
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.KitsuDateHelper
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuListSearchResult(
|
||||||
|
val data: List<KitsuListSearchItemData>,
|
||||||
|
val included: List<KitsuListSearchItemIncluded> = emptyList(),
|
||||||
|
) {
|
||||||
|
fun firstToTrack(): TrackSearch {
|
||||||
|
require(data.isNotEmpty()) { "Missing User data from Kitsu" }
|
||||||
|
require(included.isNotEmpty()) { "Missing Manga data from Kitsu" }
|
||||||
|
|
||||||
|
val userData = data[0]
|
||||||
|
val userDataAttrs = userData.attributes
|
||||||
|
val manga = included[0].attributes
|
||||||
|
|
||||||
|
return TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
|
remote_id = userData.id
|
||||||
|
title = manga.canonicalTitle
|
||||||
|
total_chapters = manga.chapterCount ?: 0
|
||||||
|
cover_url = manga.posterImage?.original ?: ""
|
||||||
|
summary = manga.synopsis ?: ""
|
||||||
|
tracking_url = KitsuApi.mangaUrl(remote_id)
|
||||||
|
publishing_status = manga.status
|
||||||
|
publishing_type = manga.mangaType ?: ""
|
||||||
|
start_date = userDataAttrs.startedAt ?: ""
|
||||||
|
started_reading_date = KitsuDateHelper.parse(userDataAttrs.startedAt)
|
||||||
|
finished_reading_date = KitsuDateHelper.parse(userDataAttrs.finishedAt)
|
||||||
|
status =
|
||||||
|
when (userDataAttrs.status) {
|
||||||
|
"current" -> Kitsu.READING
|
||||||
|
"completed" -> Kitsu.COMPLETED
|
||||||
|
"on_hold" -> Kitsu.ON_HOLD
|
||||||
|
"dropped" -> Kitsu.DROPPED
|
||||||
|
"planned" -> Kitsu.PLAN_TO_READ
|
||||||
|
else -> throw Exception("Unknown status")
|
||||||
|
}
|
||||||
|
score = userDataAttrs.ratingTwenty?.let { it / 2.0 } ?: 0.0
|
||||||
|
last_chapter_read = userDataAttrs.progress.toDouble()
|
||||||
|
private = userDataAttrs.private
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuListSearchItemData(
|
||||||
|
val id: Long,
|
||||||
|
val attributes: KitsuListSearchItemDataAttributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuListSearchItemDataAttributes(
|
||||||
|
val status: String,
|
||||||
|
val startedAt: String?,
|
||||||
|
val finishedAt: String?,
|
||||||
|
val ratingTwenty: Int?,
|
||||||
|
val progress: Int,
|
||||||
|
val private: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuListSearchItemIncluded(
|
||||||
|
val id: Long,
|
||||||
|
val attributes: KitsuListSearchItemIncludedAttributes,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuListSearchItemIncludedAttributes(
|
||||||
|
val canonicalTitle: String,
|
||||||
|
val chapterCount: Int?,
|
||||||
|
val mangaType: String?,
|
||||||
|
val posterImage: KitsuSearchItemCover?,
|
||||||
|
val synopsis: String?,
|
||||||
|
val startDate: String?,
|
||||||
|
val status: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.track.kitsu.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuOAuth(
|
||||||
|
@SerialName("access_token")
|
||||||
|
val accessToken: String,
|
||||||
|
@SerialName("token_type")
|
||||||
|
val tokenType: String,
|
||||||
|
@SerialName("created_at")
|
||||||
|
val createdAt: Long,
|
||||||
|
@SerialName("expires_in")
|
||||||
|
val expiresIn: Long,
|
||||||
|
@SerialName("refresh_token")
|
||||||
|
val refreshToken: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun KitsuOAuth.isExpired() = (System.currentTimeMillis() / 1000) > (createdAt + expiresIn - 3600)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.kitsu.KitsuApi
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuSearchResult(
|
||||||
|
val media: KitsuSearchResultData,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuSearchResultData(
|
||||||
|
val key: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuAlgoliaSearchResult(
|
||||||
|
val hits: List<KitsuAlgoliaSearchItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuAlgoliaSearchItem(
|
||||||
|
val id: Long,
|
||||||
|
val canonicalTitle: String,
|
||||||
|
val chapterCount: Int?,
|
||||||
|
val subtype: String?,
|
||||||
|
val posterImage: KitsuSearchItemCover?,
|
||||||
|
val synopsis: String?,
|
||||||
|
val averageRating: Double?,
|
||||||
|
val startDate: Long?,
|
||||||
|
val endDate: Long?,
|
||||||
|
) {
|
||||||
|
fun toTrack(): TrackSearch =
|
||||||
|
TrackSearch.create(TrackerManager.KITSU).apply {
|
||||||
|
remote_id = this@KitsuAlgoliaSearchItem.id
|
||||||
|
title = canonicalTitle
|
||||||
|
total_chapters = chapterCount ?: 0
|
||||||
|
cover_url = posterImage?.original ?: ""
|
||||||
|
summary = synopsis ?: ""
|
||||||
|
tracking_url = KitsuApi.mangaUrl(remote_id)
|
||||||
|
score = averageRating ?: -1.0
|
||||||
|
publishing_status = if (endDate == null) "Publishing" else "Finished"
|
||||||
|
publishing_type = subtype ?: ""
|
||||||
|
start_date = startDate?.let {
|
||||||
|
val outputDf = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||||
|
outputDf.format(Date(it * 1000))
|
||||||
|
} ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuSearchItemCover(
|
||||||
|
val original: String?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.kitsu.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuCurrentUserResult(
|
||||||
|
val data: List<KitsuUser>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class KitsuUser(
|
||||||
|
val id: String,
|
||||||
|
)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates
|
package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates
|
||||||
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTrackService
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.ListItem
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MUListItem
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.Rating
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MURating
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.copyTo
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.copyTo
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.toTrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.toTrackSearch
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
@@ -12,7 +12,7 @@ import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
|||||||
class MangaUpdates(
|
class MangaUpdates(
|
||||||
id: Int,
|
id: Int,
|
||||||
) : Tracker(id, "MangaUpdates"),
|
) : Tracker(id, "MangaUpdates"),
|
||||||
DeletableTrackService {
|
DeletableTracker {
|
||||||
companion object {
|
companion object {
|
||||||
const val READING_LIST = 0
|
const val READING_LIST = 0
|
||||||
const val WISH_LIST = 1
|
const val WISH_LIST = 1
|
||||||
@@ -34,8 +34,6 @@ class MangaUpdates(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsTrackDeletion: Boolean = true
|
|
||||||
|
|
||||||
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
|
private val interceptor by lazy { MangaUpdatesInterceptor(this) }
|
||||||
|
|
||||||
private val api by lazy { MangaUpdatesApi(interceptor, client) }
|
private val api by lazy { MangaUpdatesApi(interceptor, client) }
|
||||||
@@ -62,7 +60,7 @@ class MangaUpdates(
|
|||||||
|
|
||||||
override fun getScoreList(): List<String> = SCORE_LIST
|
override fun getScoreList(): List<String> = SCORE_LIST
|
||||||
|
|
||||||
override fun indexToScore(index: Int): Float = if (index == 0) 0f else SCORE_LIST[index].toFloat()
|
override fun indexToScore(index: Int): Double = if (index == 0) 0.0 else SCORE_LIST[index].toDouble()
|
||||||
|
|
||||||
override fun displayScore(track: Track): String = track.score.toString()
|
override fun displayScore(track: Track): String = track.score.toString()
|
||||||
|
|
||||||
@@ -88,8 +86,8 @@ class MangaUpdates(
|
|||||||
try {
|
try {
|
||||||
val (series, rating) = api.getSeriesListItem(track)
|
val (series, rating) = api.getSeriesListItem(track)
|
||||||
track.copyFrom(series, rating)
|
track.copyFrom(series, rating)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
track.score = 0f
|
track.score = 0.0
|
||||||
api.addSeriesToList(track, hasReadChapters)
|
api.addSeriesToList(track, hasReadChapters)
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
@@ -107,12 +105,12 @@ class MangaUpdates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Track.copyFrom(
|
private fun Track.copyFrom(
|
||||||
item: ListItem,
|
item: MUListItem,
|
||||||
rating: Rating?,
|
rating: MURating?,
|
||||||
): Track =
|
): Track =
|
||||||
apply {
|
apply {
|
||||||
item.copyTo(this)
|
item.copyTo(this)
|
||||||
score = rating?.rating ?: 0f
|
score = rating?.rating ?: 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun login(
|
override suspend fun login(
|
||||||
@@ -124,5 +122,5 @@ class MangaUpdates(
|
|||||||
interceptor.newAuth(authenticated.sessionToken)
|
interceptor.newAuth(authenticated.sessionToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restoreSession(): String? = trackPreferences.getTrackPassword(this)
|
fun restoreSession(): String? = trackPreferences.getTrackPassword(this)?.ifBlank { null }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,10 @@ import eu.kanade.tachiyomi.network.PUT
|
|||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.add
|
import kotlinx.serialization.json.add
|
||||||
import kotlinx.serialization.json.addJsonObject
|
import kotlinx.serialization.json.addJsonObject
|
||||||
import kotlinx.serialization.json.buildJsonArray
|
import kotlinx.serialization.json.buildJsonArray
|
||||||
import kotlinx.serialization.json.buildJsonObject
|
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.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -22,10 +18,12 @@ import okhttp3.OkHttpClient
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates.Companion.READING_LIST
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates.Companion.READING_LIST
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates.Companion.WISH_LIST
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates.Companion.WISH_LIST
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.Context
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MUContext
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.ListItem
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MUListItem
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.Rating
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MULoginResponse
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.Record
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MURating
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MURecord
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto.MUSearchResult
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@@ -35,9 +33,6 @@ class MangaUpdatesApi(
|
|||||||
) {
|
) {
|
||||||
private val json: Json by injectLazy()
|
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 {
|
private val authClient by lazy {
|
||||||
client
|
client
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
@@ -45,13 +40,13 @@ class MangaUpdatesApi(
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getSeriesListItem(track: Track): Pair<ListItem, Rating?> {
|
suspend fun getSeriesListItem(track: Track): Pair<MUListItem, MURating?> {
|
||||||
val listItem =
|
val listItem =
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(GET("$baseUrl/v1/lists/series/${track.media_id}"))
|
.newCall(GET("$BASE_URL/v1/lists/series/${track.remote_id}"))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<ListItem>()
|
.parseAs<MUListItem>()
|
||||||
}
|
}
|
||||||
|
|
||||||
val rating = getSeriesRating(track)
|
val rating = getSeriesRating(track)
|
||||||
@@ -68,7 +63,7 @@ class MangaUpdatesApi(
|
|||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
addJsonObject {
|
addJsonObject {
|
||||||
putJsonObject("series") {
|
putJsonObject("series") {
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
}
|
}
|
||||||
put("list_id", status)
|
put("list_id", status)
|
||||||
}
|
}
|
||||||
@@ -76,14 +71,14 @@ class MangaUpdatesApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
POST(
|
POST(
|
||||||
url = "$baseUrl/v1/lists/series",
|
url = "$BASE_URL/v1/lists/series",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.let {
|
.let {
|
||||||
if (it.code == 200) {
|
if (it.code == 200) {
|
||||||
track.status = status
|
track.status = status
|
||||||
track.last_chapter_read = 1f
|
track.last_chapter_read = 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +88,7 @@ class MangaUpdatesApi(
|
|||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
addJsonObject {
|
addJsonObject {
|
||||||
putJsonObject("series") {
|
putJsonObject("series") {
|
||||||
put("id", track.media_id)
|
put("id", track.remote_id)
|
||||||
}
|
}
|
||||||
put("list_id", track.status)
|
put("list_id", track.status)
|
||||||
putJsonObject("status") {
|
putJsonObject("status") {
|
||||||
@@ -104,8 +99,8 @@ class MangaUpdatesApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
POST(
|
POST(
|
||||||
url = "$baseUrl/v1/lists/series/update",
|
url = "$BASE_URL/v1/lists/series/update",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
|
|
||||||
@@ -115,31 +110,32 @@ class MangaUpdatesApi(
|
|||||||
suspend fun deleteSeriesFromList(track: Track) {
|
suspend fun deleteSeriesFromList(track: Track) {
|
||||||
val body =
|
val body =
|
||||||
buildJsonArray {
|
buildJsonArray {
|
||||||
add(track.media_id)
|
add(track.remote_id)
|
||||||
}
|
}
|
||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
POST(
|
POST(
|
||||||
url = "$baseUrl/v1/lists/series/delete",
|
url = "$BASE_URL/v1/lists/series/delete",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getSeriesRating(track: Track): Rating? =
|
private suspend fun getSeriesRating(track: Track): MURating? =
|
||||||
try {
|
try {
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(GET("$baseUrl/v1/series/${track.media_id}/rating"))
|
.newCall(GET("$BASE_URL/v1/series/${track.remote_id}/rating"))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<Rating>()
|
.parseAs<MURating>()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateSeriesRating(track: Track) {
|
private suspend fun updateSeriesRating(track: Track) {
|
||||||
if (track.score != 0f) {
|
if (track.score < 0.0) return
|
||||||
|
if (track.score != 0.0) {
|
||||||
val body =
|
val body =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("rating", track.score)
|
put("rating", track.score)
|
||||||
@@ -147,21 +143,19 @@ class MangaUpdatesApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
PUT(
|
PUT(
|
||||||
url = "$baseUrl/v1/series/${track.media_id}/rating",
|
url = "$BASE_URL/v1/series/${track.remote_id}/rating",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
} else {
|
} else {
|
||||||
authClient
|
authClient
|
||||||
.newCall(
|
.newCall(
|
||||||
DELETE(
|
DELETE(url = "$BASE_URL/v1/series/${track.remote_id}/rating"),
|
||||||
url = "$baseUrl/v1/series/${track.media_id}/rating",
|
|
||||||
),
|
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun search(query: String): List<Record> {
|
suspend fun search(query: String): List<MURecord> {
|
||||||
val body =
|
val body =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("search", query)
|
put("search", query)
|
||||||
@@ -173,27 +167,25 @@ class MangaUpdatesApi(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return with(json) {
|
return with(json) {
|
||||||
client
|
client
|
||||||
.newCall(
|
.newCall(
|
||||||
POST(
|
POST(
|
||||||
url = "$baseUrl/v1/series/search",
|
url = "$BASE_URL/v1/series/search",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MUSearchResult>()
|
||||||
.let { obj ->
|
.results
|
||||||
obj["results"]?.jsonArray?.map { element ->
|
.map { it.record }
|
||||||
json.decodeFromJsonElement<Record>(element.jsonObject["record"]!!)
|
|
||||||
}
|
|
||||||
}.orEmpty()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun authenticate(
|
suspend fun authenticate(
|
||||||
username: String,
|
username: String,
|
||||||
password: String,
|
password: String,
|
||||||
): Context? {
|
): MUContext? {
|
||||||
val body =
|
val body =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("username", username)
|
put("username", username)
|
||||||
@@ -203,19 +195,18 @@ class MangaUpdatesApi(
|
|||||||
client
|
client
|
||||||
.newCall(
|
.newCall(
|
||||||
PUT(
|
PUT(
|
||||||
url = "$baseUrl/v1/account/login",
|
url = "$BASE_URL/v1/account/login",
|
||||||
body = body.toString().toRequestBody(contentType),
|
body = body.toString().toRequestBody(CONTENT_TYPE),
|
||||||
),
|
),
|
||||||
).awaitSuccess()
|
).awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MULoginResponse>()
|
||||||
.let { obj ->
|
.context
|
||||||
try {
|
|
||||||
json.decodeFromJsonElement<Context>(obj["context"]!!)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// logcat(LogPriority.ERROR, e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val BASE_URL = "https://api.mangaupdates.com"
|
||||||
|
|
||||||
|
private val CONTENT_TYPE = "application/vnd.api+json".toMediaType()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class MangaUpdatesInterceptor(
|
|||||||
originalRequest
|
originalRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer $token")
|
.addHeader("Authorization", "Bearer $token")
|
||||||
.header("User-Agent", "Suwayomi ${BuildConfig.VERSION}")
|
.header("User-Agent", "Suwayomi ${BuildConfig.VERSION} (${BuildConfig.REVISION})")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return chain.proceed(authRequest)
|
return chain.proceed(authRequest)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Context(
|
data class MUContext(
|
||||||
@SerialName("session_token")
|
@SerialName("session_token")
|
||||||
val sessionToken: String,
|
val sessionToken: String,
|
||||||
val uid: Long,
|
val uid: Long,
|
||||||
@@ -3,8 +3,8 @@ package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Image(
|
data class MUImage(
|
||||||
val url: Url? = null,
|
val url: MUUrl? = null,
|
||||||
val height: Int? = null,
|
val height: Int? = null,
|
||||||
val width: Int? = null,
|
val width: Int? = null,
|
||||||
)
|
)
|
||||||
@@ -6,16 +6,16 @@ import suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.MangaUpdates.Com
|
|||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ListItem(
|
data class MUListItem(
|
||||||
val series: Series? = null,
|
val series: MUSeries? = null,
|
||||||
@SerialName("list_id")
|
@SerialName("list_id")
|
||||||
val listId: Int? = null,
|
val listId: Int? = null,
|
||||||
val status: Status? = null,
|
val status: MUStatus? = null,
|
||||||
val priority: Int? = null,
|
val priority: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ListItem.copyTo(track: Track): Track =
|
fun MUListItem.copyTo(track: Track): Track =
|
||||||
track.apply {
|
track.apply {
|
||||||
this.status = listId ?: READING_LIST
|
this.status = listId ?: READING_LIST
|
||||||
this.last_chapter_read = this@copyTo.status?.chapter?.toFloat() ?: 0f
|
this.last_chapter_read = this@copyTo.status?.chapter?.toDouble() ?: 0.0
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MULoginResponse(
|
||||||
|
val context: MUContext,
|
||||||
|
)
|
||||||
@@ -4,11 +4,11 @@ import kotlinx.serialization.Serializable
|
|||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Rating(
|
data class MURating(
|
||||||
val rating: Float? = null,
|
val rating: Double? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Rating.copyTo(track: Track): Track =
|
fun MURating.copyTo(track: Track): Track =
|
||||||
track.apply {
|
track.apply {
|
||||||
this.score = rating ?: 0f
|
this.score = rating ?: 0.0
|
||||||
}
|
}
|
||||||
@@ -2,17 +2,17 @@ package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
|||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import org.jsoup.Jsoup
|
import suwayomi.tachidesk.manga.impl.track.Track.htmlDecode
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Record(
|
data class MURecord(
|
||||||
@SerialName("series_id")
|
@SerialName("series_id")
|
||||||
val seriesId: Long? = null,
|
val seriesId: Long? = null,
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
val image: Image? = null,
|
val image: MUImage? = null,
|
||||||
val type: String? = null,
|
val type: String? = null,
|
||||||
val year: String? = null,
|
val year: String? = null,
|
||||||
@SerialName("bayesian_rating")
|
@SerialName("bayesian_rating")
|
||||||
@@ -23,11 +23,9 @@ data class Record(
|
|||||||
val latestChapter: Int? = null,
|
val latestChapter: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun String.htmlDecode(): String = Jsoup.parse(this).wholeText()
|
fun MURecord.toTrackSearch(id: Int): TrackSearch =
|
||||||
|
|
||||||
fun Record.toTrackSearch(id: Int): TrackSearch =
|
|
||||||
TrackSearch.create(id).apply {
|
TrackSearch.create(id).apply {
|
||||||
media_id = this@toTrackSearch.seriesId ?: 0L
|
remote_id = this@toTrackSearch.seriesId ?: 0L
|
||||||
title = this@toTrackSearch.title?.htmlDecode() ?: ""
|
title = this@toTrackSearch.title?.htmlDecode() ?: ""
|
||||||
total_chapters = 0
|
total_chapters = 0
|
||||||
cover_url = this@toTrackSearch.image?.url?.original ?: ""
|
cover_url = this@toTrackSearch.image?.url?.original ?: ""
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MUSearchResult(
|
||||||
|
val results: List<MUSearchResultItem>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MUSearchResultItem(
|
||||||
|
val record: MURecord,
|
||||||
|
)
|
||||||
@@ -3,7 +3,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Series(
|
data class MUSeries(
|
||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
val title: String? = null,
|
val title: String? = null,
|
||||||
)
|
)
|
||||||
@@ -3,7 +3,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Status(
|
data class MUStatus(
|
||||||
val volume: Int? = null,
|
val volume: Int? = null,
|
||||||
val chapter: Int? = null,
|
val chapter: Int? = null,
|
||||||
)
|
)
|
||||||
@@ -3,7 +3,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.mangaupdates.dto
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Url(
|
data class MUUrl(
|
||||||
val original: String? = null,
|
val original: String? = null,
|
||||||
val thumb: String? = null,
|
val thumb: String? = null,
|
||||||
)
|
)
|
||||||
@@ -9,19 +9,19 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var manga_id: Int
|
var manga_id: Int
|
||||||
|
|
||||||
var sync_id: Int
|
var tracker_id: Int
|
||||||
|
|
||||||
var media_id: Long
|
var remote_id: Long
|
||||||
|
|
||||||
var library_id: Long?
|
var library_id: Long?
|
||||||
|
|
||||||
var title: String
|
var title: String
|
||||||
|
|
||||||
var last_chapter_read: Float
|
var last_chapter_read: Double
|
||||||
|
|
||||||
var total_chapters: Int
|
var total_chapters: Int
|
||||||
|
|
||||||
var score: Float
|
var score: Double
|
||||||
|
|
||||||
var status: Int
|
var status: Int
|
||||||
|
|
||||||
@@ -31,18 +31,24 @@ interface Track : Serializable {
|
|||||||
|
|
||||||
var tracking_url: String
|
var tracking_url: String
|
||||||
|
|
||||||
fun copyPersonalFrom(other: Track) {
|
var private: Boolean
|
||||||
|
|
||||||
|
fun copyPersonalFrom(
|
||||||
|
other: Track,
|
||||||
|
copyRemotePrivate: Boolean = true,
|
||||||
|
) {
|
||||||
last_chapter_read = other.last_chapter_read
|
last_chapter_read = other.last_chapter_read
|
||||||
score = other.score
|
score = other.score
|
||||||
status = other.status
|
status = other.status
|
||||||
started_reading_date = other.started_reading_date
|
started_reading_date = other.started_reading_date
|
||||||
finished_reading_date = other.finished_reading_date
|
finished_reading_date = other.finished_reading_date
|
||||||
|
if (copyRemotePrivate) private = other.private
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(serviceId: Int): Track =
|
fun create(serviceId: Int): Track =
|
||||||
TrackImpl().apply {
|
TrackImpl().apply {
|
||||||
sync_id = serviceId
|
tracker_id = serviceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
|
|||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.lastChapterRead
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.lastChapterRead
|
||||||
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteUrl
|
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteUrl
|
||||||
|
import suwayomi.tachidesk.manga.model.table.TrackSearchTable
|
||||||
|
|
||||||
fun ResultRow.toTrackRecordDataClass(): TrackRecordDataClass =
|
fun ResultRow.toTrackRecordDataClass(): TrackRecordDataClass =
|
||||||
TrackRecordDataClass(
|
TrackRecordDataClass(
|
||||||
@@ -22,62 +23,89 @@ fun ResultRow.toTrackRecordDataClass(): TrackRecordDataClass =
|
|||||||
remoteUrl = this[TrackRecordTable.remoteUrl],
|
remoteUrl = this[TrackRecordTable.remoteUrl],
|
||||||
startDate = this[TrackRecordTable.startDate],
|
startDate = this[TrackRecordTable.startDate],
|
||||||
finishDate = this[TrackRecordTable.finishDate],
|
finishDate = this[TrackRecordTable.finishDate],
|
||||||
|
private = this[TrackRecordTable.private],
|
||||||
)
|
)
|
||||||
|
|
||||||
fun ResultRow.toTrack(): Track =
|
fun ResultRow.toTrack(): Track =
|
||||||
Track.create(this[TrackRecordTable.trackerId]).also {
|
Track.create(this[TrackRecordTable.trackerId]).also {
|
||||||
it.id = this[TrackRecordTable.id].value
|
it.id = this[TrackRecordTable.id].value
|
||||||
it.manga_id = this[TrackRecordTable.mangaId].value
|
it.manga_id = this[TrackRecordTable.mangaId].value
|
||||||
it.media_id = this[TrackRecordTable.remoteId]
|
it.remote_id = this[TrackRecordTable.remoteId]
|
||||||
it.library_id = this[TrackRecordTable.libraryId]
|
it.library_id = this[TrackRecordTable.libraryId]
|
||||||
it.title = this[TrackRecordTable.title]
|
it.title = this[TrackRecordTable.title]
|
||||||
it.last_chapter_read = this[TrackRecordTable.lastChapterRead].toFloat()
|
it.last_chapter_read = this[TrackRecordTable.lastChapterRead]
|
||||||
it.total_chapters = this[TrackRecordTable.totalChapters]
|
it.total_chapters = this[TrackRecordTable.totalChapters]
|
||||||
it.status = this[TrackRecordTable.status]
|
it.status = this[TrackRecordTable.status]
|
||||||
it.score = this[TrackRecordTable.score].toFloat()
|
it.score = this[TrackRecordTable.score]
|
||||||
it.tracking_url = this[TrackRecordTable.remoteUrl]
|
it.tracking_url = this[TrackRecordTable.remoteUrl]
|
||||||
it.started_reading_date = this[TrackRecordTable.startDate]
|
it.started_reading_date = this[TrackRecordTable.startDate]
|
||||||
it.finished_reading_date = this[TrackRecordTable.finishDate]
|
it.finished_reading_date = this[TrackRecordTable.finishDate]
|
||||||
|
it.private = this[TrackRecordTable.private]
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ResultRow.toTrackSearch(): TrackSearch =
|
||||||
|
TrackSearch.create(this[TrackSearchTable.trackerId]).also {
|
||||||
|
it.id = this[TrackSearchTable.id].value
|
||||||
|
it.remote_id = this[TrackSearchTable.remoteId]
|
||||||
|
it.library_id = this[TrackSearchTable.libraryId]
|
||||||
|
it.title = this[TrackSearchTable.title]
|
||||||
|
it.last_chapter_read = this[TrackSearchTable.lastChapterRead]
|
||||||
|
it.total_chapters = this[TrackSearchTable.totalChapters]
|
||||||
|
it.status = this[TrackSearchTable.status]
|
||||||
|
it.score = this[TrackSearchTable.score]
|
||||||
|
it.tracking_url = this[TrackSearchTable.trackingUrl]
|
||||||
|
it.started_reading_date = this[TrackSearchTable.startedReadingDate]
|
||||||
|
it.finished_reading_date = this[TrackSearchTable.finishedReadingDate]
|
||||||
|
it.private = this[TrackSearchTable.private]
|
||||||
|
it.authors = this[TrackSearchTable.authors]?.split(",").orEmpty()
|
||||||
|
it.artists = this[TrackSearchTable.artists]?.split(",").orEmpty()
|
||||||
|
it.cover_url = this[TrackSearchTable.coverUrl]
|
||||||
|
it.summary = this[TrackSearchTable.summary]
|
||||||
|
it.publishing_status = this[TrackSearchTable.publishingStatus]
|
||||||
|
it.publishing_type = this[TrackSearchTable.publishingType]
|
||||||
|
it.start_date = this[TrackSearchTable.startDate]
|
||||||
}
|
}
|
||||||
|
|
||||||
fun BackupTracking.toTrack(mangaId: Int): Track =
|
fun BackupTracking.toTrack(mangaId: Int): Track =
|
||||||
Track.create(syncId).also {
|
Track.create(syncId).also {
|
||||||
it.id = -1
|
it.id = -1
|
||||||
it.manga_id = mangaId
|
it.manga_id = mangaId
|
||||||
it.media_id = mediaId
|
it.remote_id = mediaId
|
||||||
it.library_id = libraryId
|
it.library_id = libraryId
|
||||||
it.title = title
|
it.title = title
|
||||||
it.last_chapter_read = lastChapterRead
|
it.last_chapter_read = lastChapterRead.toDouble()
|
||||||
it.total_chapters = totalChapters
|
it.total_chapters = totalChapters
|
||||||
it.status = status
|
it.status = status
|
||||||
it.score = score
|
it.score = score.toDouble()
|
||||||
it.tracking_url = trackingUrl
|
it.tracking_url = trackingUrl
|
||||||
it.started_reading_date = startedReadingDate
|
it.started_reading_date = startedReadingDate
|
||||||
it.finished_reading_date = finishedReadingDate
|
it.finished_reading_date = finishedReadingDate
|
||||||
|
it.private = private
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TrackRecordDataClass.toTrack(): Track =
|
fun TrackRecordDataClass.toTrack(): Track =
|
||||||
Track.create(trackerId).also {
|
Track.create(trackerId).also {
|
||||||
it.id = id
|
it.id = id
|
||||||
it.manga_id = mangaId
|
it.manga_id = mangaId
|
||||||
it.media_id = remoteId
|
it.remote_id = remoteId
|
||||||
it.library_id = libraryId
|
it.library_id = libraryId
|
||||||
it.title = title
|
it.title = title
|
||||||
it.last_chapter_read = lastChapterRead.toFloat()
|
it.last_chapter_read = lastChapterRead
|
||||||
it.total_chapters = totalChapters
|
it.total_chapters = totalChapters
|
||||||
it.status = status
|
it.status = status
|
||||||
it.score = score.toFloat()
|
it.score = score
|
||||||
it.tracking_url = remoteUrl
|
it.tracking_url = remoteUrl
|
||||||
it.started_reading_date = startDate
|
it.started_reading_date = startDate
|
||||||
it.finished_reading_date = finishDate
|
it.finished_reading_date = finishDate
|
||||||
|
it.private = private
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Track.toTrackRecordDataClass(): TrackRecordDataClass =
|
fun Track.toTrackRecordDataClass(): TrackRecordDataClass =
|
||||||
TrackRecordDataClass(
|
TrackRecordDataClass(
|
||||||
id = id ?: -1,
|
id = id ?: -1,
|
||||||
mangaId = manga_id,
|
mangaId = manga_id,
|
||||||
trackerId = sync_id,
|
trackerId = tracker_id,
|
||||||
remoteId = media_id,
|
remoteId = remote_id,
|
||||||
libraryId = library_id,
|
libraryId = library_id,
|
||||||
title = title,
|
title = title,
|
||||||
lastChapterRead = last_chapter_read.toDouble(),
|
lastChapterRead = last_chapter_read.toDouble(),
|
||||||
@@ -87,4 +115,5 @@ fun Track.toTrackRecordDataClass(): TrackRecordDataClass =
|
|||||||
remoteUrl = tracking_url,
|
remoteUrl = tracking_url,
|
||||||
startDate = started_reading_date,
|
startDate = started_reading_date,
|
||||||
finishDate = finished_reading_date,
|
finishDate = finished_reading_date,
|
||||||
|
private = private,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ class TrackImpl : Track {
|
|||||||
|
|
||||||
override var manga_id: Int = 0
|
override var manga_id: Int = 0
|
||||||
|
|
||||||
override var sync_id: Int = 0
|
override var tracker_id: Int = 0
|
||||||
|
|
||||||
override var media_id: Long = 0
|
override var remote_id: Long = 0
|
||||||
|
|
||||||
override var library_id: Long? = null
|
override var library_id: Long? = null
|
||||||
|
|
||||||
override lateinit var title: String
|
override lateinit var title: String
|
||||||
|
|
||||||
override var last_chapter_read: Float = 0F
|
override var last_chapter_read: Double = 0.0
|
||||||
|
|
||||||
override var total_chapters: Int = 0
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
override var score: Float = 0f
|
override var score: Double = 0.0
|
||||||
|
|
||||||
override var status: Int = 0
|
override var status: Int = 0
|
||||||
|
|
||||||
@@ -28,4 +28,6 @@ class TrackImpl : Track {
|
|||||||
override var finished_reading_date: Long = 0
|
override var finished_reading_date: Long = 0
|
||||||
|
|
||||||
override var tracking_url: String = ""
|
override var tracking_url: String = ""
|
||||||
|
|
||||||
|
override var private: Boolean = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,38 @@
|
|||||||
|
|
||||||
package suwayomi.tachidesk.manga.impl.track.tracker.model
|
package suwayomi.tachidesk.manga.impl.track.tracker.model
|
||||||
|
|
||||||
class TrackSearch {
|
class TrackSearch : Track {
|
||||||
var sync_id: Int = 0
|
override var id: Int? = null
|
||||||
|
|
||||||
var media_id: Long = 0
|
override var manga_id: Int = 0
|
||||||
|
|
||||||
lateinit var title: String
|
override var tracker_id: Int = 0
|
||||||
|
|
||||||
var total_chapters: Int = 0
|
override var remote_id: Long = 0
|
||||||
|
|
||||||
lateinit var tracking_url: String
|
override var library_id: Long? = null
|
||||||
|
|
||||||
|
override lateinit var title: String
|
||||||
|
|
||||||
|
override var last_chapter_read: Double = 0.0
|
||||||
|
|
||||||
|
override var total_chapters: Int = 0
|
||||||
|
|
||||||
|
override var score: Double = -1.0
|
||||||
|
|
||||||
|
override var status: Int = 0
|
||||||
|
|
||||||
|
override var started_reading_date: Long = 0
|
||||||
|
|
||||||
|
override var finished_reading_date: Long = 0
|
||||||
|
|
||||||
|
override var private: Boolean = false
|
||||||
|
|
||||||
|
override lateinit var tracking_url: String
|
||||||
|
|
||||||
|
var authors: List<String> = emptyList()
|
||||||
|
|
||||||
|
var artists: List<String> = emptyList()
|
||||||
|
|
||||||
var cover_url: String = ""
|
var cover_url: String = ""
|
||||||
|
|
||||||
@@ -29,22 +51,24 @@ class TrackSearch {
|
|||||||
|
|
||||||
other as TrackSearch
|
other as TrackSearch
|
||||||
|
|
||||||
if (sync_id != other.sync_id) return false
|
if (manga_id != other.manga_id) return false
|
||||||
if (media_id != other.media_id) return false
|
if (tracker_id != other.tracker_id) return false
|
||||||
|
if (remote_id != other.remote_id) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = sync_id.hashCode()
|
var result = manga_id.hashCode()
|
||||||
result = 31 * result + media_id.hashCode()
|
result = 31 * result + tracker_id.hashCode()
|
||||||
|
result = 31 * result + remote_id.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun create(serviceId: Int): TrackSearch =
|
fun create(serviceId: Int): TrackSearch =
|
||||||
TrackSearch().apply {
|
TrackSearch().apply {
|
||||||
sync_id = serviceId
|
tracker_id = serviceId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist
|
|||||||
|
|
||||||
import android.annotation.StringRes
|
import android.annotation.StringRes
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.serialization.encodeToString
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTrackService
|
import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
import suwayomi.tachidesk.manga.impl.track.tracker.Tracker
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
import suwayomi.tachidesk.manga.impl.track.tracker.extractToken
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALOAuth
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class MyAnimeList(
|
class MyAnimeList(
|
||||||
id: Int,
|
id: Int,
|
||||||
) : Tracker(id, "MyAnimeList"),
|
) : Tracker(id, "MyAnimeList"),
|
||||||
DeletableTrackService {
|
DeletableTracker {
|
||||||
companion object {
|
companion object {
|
||||||
const val READING = 1
|
const val READING = 1
|
||||||
const val COMPLETED = 2
|
const val COMPLETED = 2
|
||||||
@@ -28,8 +28,6 @@ class MyAnimeList(
|
|||||||
private const val SEARCH_LIST_PREFIX = "my:"
|
private const val SEARCH_LIST_PREFIX = "my:"
|
||||||
}
|
}
|
||||||
|
|
||||||
override val supportsTrackDeletion: Boolean = true
|
|
||||||
|
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
private val interceptor by lazy { MyAnimeListInterceptor(this) }
|
||||||
@@ -78,7 +76,7 @@ class MyAnimeList(
|
|||||||
track.finished_reading_date = System.currentTimeMillis()
|
track.finished_reading_date = System.currentTimeMillis()
|
||||||
} else if (track.status != REREADING) {
|
} else if (track.status != REREADING) {
|
||||||
track.status = READING
|
track.status = READING
|
||||||
if (track.last_chapter_read == 1F) {
|
if (track.last_chapter_read == 1.0) {
|
||||||
track.started_reading_date = System.currentTimeMillis()
|
track.started_reading_date = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,18 +97,18 @@ class MyAnimeList(
|
|||||||
val remoteTrack = api.findListItem(track)
|
val remoteTrack = api.findListItem(track)
|
||||||
return if (remoteTrack != null) {
|
return if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.remote_id = remoteTrack.remote_id
|
||||||
|
|
||||||
if (track.status != COMPLETED) {
|
if (track.status != COMPLETED) {
|
||||||
val isRereading = track.status == REREADING
|
val isRereading = track.status == REREADING
|
||||||
track.status = if (isRereading.not() && hasReadChapters) READING else track.status
|
track.status = if (!isRereading && hasReadChapters) READING else track.status
|
||||||
}
|
}
|
||||||
|
|
||||||
update(track)
|
update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
track.status = if (hasReadChapters) READING else PLAN_TO_READ
|
||||||
track.score = 0F
|
track.score = 0.0
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,11 +145,10 @@ class MyAnimeList(
|
|||||||
|
|
||||||
suspend fun login(authCode: String) {
|
suspend fun login(authCode: String) {
|
||||||
try {
|
try {
|
||||||
logger.debug { "login $authCode" }
|
|
||||||
val oauth = api.getAccessToken(authCode)
|
val oauth = api.getAccessToken(authCode)
|
||||||
interceptor.setAuth(oauth)
|
interceptor.setAuth(oauth)
|
||||||
val username = api.getCurrentUser()
|
val username = api.getCurrentUser()
|
||||||
saveCredentials(username, oauth.access_token)
|
saveCredentials(username, oauth.accessToken)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logger.error(e) { "oauth err" }
|
logger.error(e) { "oauth err" }
|
||||||
logout()
|
logout()
|
||||||
@@ -165,13 +162,13 @@ class MyAnimeList(
|
|||||||
interceptor.setAuth(null)
|
interceptor.setAuth(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveOAuth(oAuth: OAuth?) {
|
fun saveOAuth(oAuth: MALOAuth?) {
|
||||||
trackPreferences.setTrackToken(this, json.encodeToString(oAuth))
|
trackPreferences.setTrackToken(this, json.encodeToString(oAuth))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadOAuth(): OAuth? =
|
fun loadOAuth(): MALOAuth? =
|
||||||
try {
|
try {
|
||||||
json.decodeFromString<OAuth>(trackPreferences.getTrackToken(this)!!)
|
json.decodeFromString<MALOAuth>(trackPreferences.getTrackToken(this)!!)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logger.error(e) { "loadOAuth err" }
|
logger.error(e) { "loadOAuth err" }
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
@@ -11,15 +12,6 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
|
|||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.boolean
|
|
||||||
import kotlinx.serialization.json.contentOrNull
|
|
||||||
import kotlinx.serialization.json.float
|
|
||||||
import kotlinx.serialization.json.int
|
|
||||||
import kotlinx.serialization.json.jsonArray
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import kotlinx.serialization.json.jsonPrimitive
|
|
||||||
import kotlinx.serialization.json.long
|
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -28,6 +20,13 @@ import okhttp3.RequestBody
|
|||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALListItem
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALListItemStatus
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALManga
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALOAuth
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALSearchResult
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALUser
|
||||||
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALUserSearchResult
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -40,7 +39,7 @@ class MyAnimeListApi(
|
|||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
suspend fun getAccessToken(authCode: String): OAuth =
|
suspend fun getAccessToken(authCode: String): MALOAuth =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val formBody: RequestBody =
|
val formBody: RequestBody =
|
||||||
FormBody
|
FormBody
|
||||||
@@ -70,8 +69,8 @@ class MyAnimeListApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(request)
|
.newCall(request)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MALUser>()
|
||||||
.let { it["name"]!!.jsonPrimitive.content }
|
.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,17 +88,11 @@ class MyAnimeListApi(
|
|||||||
authClient
|
authClient
|
||||||
.newCall(GET(url.toString()))
|
.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MALSearchResult>()
|
||||||
.let {
|
.data
|
||||||
it["data"]!!
|
.map { async { getMangaDetails(it.node.id) } }
|
||||||
.jsonArray
|
.awaitAll()
|
||||||
.map { data -> data.jsonObject["node"]!!.jsonObject }
|
.filter { !it.publishing_type.contains("novel") }
|
||||||
.map { node ->
|
|
||||||
val id = node["id"]!!.jsonPrimitive.int
|
|
||||||
async { getMangaDetails(id) }
|
|
||||||
}.awaitAll()
|
|
||||||
.filter { trackSearch -> !trackSearch.publishing_type.contains("novel") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,33 +103,27 @@ class MyAnimeListApi(
|
|||||||
.toUri()
|
.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendPath(id.toString())
|
.appendPath(id.toString())
|
||||||
.appendQueryParameter("fields", "id,title,synopsis,num_chapters,main_picture,status,media_type,start_date")
|
.appendQueryParameter(
|
||||||
.build()
|
"fields",
|
||||||
|
"id,title,synopsis,num_chapters,mean,main_picture,status,media_type,start_date",
|
||||||
|
).build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(GET(url.toString()))
|
.newCall(GET(url.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MALManga>()
|
||||||
.let {
|
.let {
|
||||||
val obj = it.jsonObject
|
|
||||||
TrackSearch.create(TrackerManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackerManager.MYANIMELIST).apply {
|
||||||
media_id = obj["id"]!!.jsonPrimitive.long
|
remote_id = it.id
|
||||||
title = obj["title"]!!.jsonPrimitive.content
|
title = it.title
|
||||||
summary = obj["synopsis"]?.jsonPrimitive?.content ?: ""
|
summary = it.synopsis
|
||||||
total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
total_chapters = it.numChapters
|
||||||
cover_url =
|
score = it.mean
|
||||||
obj["main_picture"]
|
cover_url = it.covers?.large.orEmpty()
|
||||||
?.jsonObject
|
tracking_url = "https://myanimelist.net/manga/$remote_id"
|
||||||
?.get("large")
|
publishing_status = it.status.replace("_", " ")
|
||||||
?.jsonPrimitive
|
publishing_type = it.mediaType.replace("_", " ")
|
||||||
?.content
|
start_date = it.startDate ?: ""
|
||||||
?: ""
|
|
||||||
tracking_url = "https://myanimelist.net/manga/$media_id"
|
|
||||||
publishing_status =
|
|
||||||
obj["status"]!!.jsonPrimitive.content.replace("_", " ")
|
|
||||||
publishing_type =
|
|
||||||
obj["media_type"]!!.jsonPrimitive.content.replace("_", " ")
|
|
||||||
start_date = obj["start_date"]?.jsonPrimitive?.content ?: ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,30 +148,25 @@ class MyAnimeListApi(
|
|||||||
val request =
|
val request =
|
||||||
Request
|
Request
|
||||||
.Builder()
|
.Builder()
|
||||||
.url(mangaUrl(track.media_id).toString())
|
.url(mangaUrl(track.remote_id).toString())
|
||||||
.put(formBodyBuilder.build())
|
.put(formBodyBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(request)
|
.newCall(request)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MALListItemStatus>()
|
||||||
.let { parseMangaItem(it, track) }
|
.let { parseMangaItem(it, track) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteItem(track: Track) =
|
suspend fun deleteItem(track: Track) {
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val request =
|
|
||||||
Request
|
|
||||||
.Builder()
|
|
||||||
.url(mangaUrl(track.media_id).toString())
|
|
||||||
.delete()
|
|
||||||
.build()
|
|
||||||
authClient
|
authClient
|
||||||
.newCall(request)
|
.newCall(DELETE(mangaUrl(track.remote_id).toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun findListItem(track: Track): Track? =
|
suspend fun findListItem(track: Track): Track? =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
@@ -192,19 +174,17 @@ class MyAnimeListApi(
|
|||||||
"$BASE_API_URL/manga"
|
"$BASE_API_URL/manga"
|
||||||
.toUri()
|
.toUri()
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.appendPath(track.media_id.toString())
|
.appendPath(track.remote_id.toString())
|
||||||
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
.appendQueryParameter("fields", "num_chapters,my_list_status{start_date,finish_date}")
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
authClient
|
authClient
|
||||||
.newCall(GET(uri.toString()))
|
.newCall(GET(uri.toString()))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
.parseAs<JsonObject>()
|
.parseAs<MALListItem>()
|
||||||
.let { obj ->
|
.let { item ->
|
||||||
track.total_chapters = obj["num_chapters"]!!.jsonPrimitive.int
|
track.total_chapters = item.numChapters
|
||||||
obj.jsonObject["my_list_status"]?.jsonObject?.let {
|
item.myListStatus?.let { parseMangaItem(it, track) }
|
||||||
parseMangaItem(it, track)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,39 +194,23 @@ class MyAnimeListApi(
|
|||||||
offset: Int = 0,
|
offset: Int = 0,
|
||||||
): List<TrackSearch> =
|
): List<TrackSearch> =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val json = getListPage(offset)
|
val myListSearchResult = getListPage(offset)
|
||||||
val obj = json.jsonObject
|
|
||||||
|
|
||||||
val matches =
|
val matches =
|
||||||
obj["data"]!!
|
myListSearchResult.data
|
||||||
.jsonArray
|
.filter { it.node.title.contains(query, ignoreCase = true) }
|
||||||
.filter {
|
.map { async { getMangaDetails(it.node.id) } }
|
||||||
it.jsonObject["node"]!!.jsonObject["title"]!!.jsonPrimitive.content.contains(
|
.awaitAll()
|
||||||
query,
|
|
||||||
ignoreCase = true,
|
|
||||||
)
|
|
||||||
}.map {
|
|
||||||
val id =
|
|
||||||
it.jsonObject["node"]!!
|
|
||||||
.jsonObject["id"]!!
|
|
||||||
.jsonPrimitive.int
|
|
||||||
async { getMangaDetails(id) }
|
|
||||||
}.awaitAll()
|
|
||||||
|
|
||||||
// Check next page if there's more
|
// Check next page if there's more
|
||||||
if (!obj["paging"]!!
|
if (!myListSearchResult.paging.next.isNullOrBlank()) {
|
||||||
.jsonObject["next"]
|
|
||||||
?.jsonPrimitive
|
|
||||||
?.contentOrNull
|
|
||||||
.isNullOrBlank()
|
|
||||||
) {
|
|
||||||
matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT)
|
matches + findListItems(query, offset + LIST_PAGINATION_AMOUNT)
|
||||||
} else {
|
} else {
|
||||||
matches
|
matches
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getListPage(offset: Int): JsonObject =
|
private suspend fun getListPage(offset: Int): MALUserSearchResult =
|
||||||
withIOContext {
|
withIOContext {
|
||||||
val urlBuilder =
|
val urlBuilder =
|
||||||
"$BASE_API_URL/users/@me/mangalist"
|
"$BASE_API_URL/users/@me/mangalist"
|
||||||
@@ -273,22 +237,16 @@ class MyAnimeListApi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun parseMangaItem(
|
private fun parseMangaItem(
|
||||||
response: JsonObject,
|
listStatus: MALListItemStatus,
|
||||||
track: Track,
|
track: Track,
|
||||||
): Track {
|
): Track =
|
||||||
val obj = response.jsonObject
|
track.apply {
|
||||||
return track.apply {
|
val isRereading = listStatus.isRereading
|
||||||
val isRereading = obj["is_rereading"]!!.jsonPrimitive.boolean
|
status = if (isRereading) MyAnimeList.REREADING else getStatus(listStatus.status)
|
||||||
status = if (isRereading) MyAnimeList.REREADING else getStatus(obj["status"]?.jsonPrimitive?.content)
|
last_chapter_read = listStatus.numChaptersRead
|
||||||
last_chapter_read = obj["num_chapters_read"]!!.jsonPrimitive.float
|
score = listStatus.score.toDouble()
|
||||||
score = obj["score"]!!.jsonPrimitive.int.toFloat()
|
listStatus.startDate?.let { started_reading_date = parseDate(it) }
|
||||||
obj["start_date"]?.let {
|
listStatus.finishDate?.let { finished_reading_date = parseDate(it) }
|
||||||
started_reading_date = parseDate(it.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
obj["finish_date"]?.let {
|
|
||||||
finished_reading_date = parseDate(it.jsonPrimitive.content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseDate(isoDate: String): Long = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
private fun parseDate(isoDate: String): Long = SimpleDateFormat("yyyy-MM-dd", Locale.US).parse(isoDate)?.time ?: 0L
|
||||||
@@ -332,12 +290,12 @@ class MyAnimeListApi(
|
|||||||
.appendPath("my_list_status")
|
.appendPath("my_list_status")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun refreshTokenRequest(oauth: OAuth): Request {
|
fun refreshTokenRequest(oauth: MALOAuth): Request {
|
||||||
val formBody: RequestBody =
|
val formBody: RequestBody =
|
||||||
FormBody
|
FormBody
|
||||||
.Builder()
|
.Builder()
|
||||||
.add("client_id", CLIENT_ID)
|
.add("client_id", CLIENT_ID)
|
||||||
.add("refresh_token", oauth.refresh_token)
|
.add("refresh_token", oauth.refreshToken)
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -347,7 +305,7 @@ class MyAnimeListApi(
|
|||||||
val headers =
|
val headers =
|
||||||
Headers
|
Headers
|
||||||
.Builder()
|
.Builder()
|
||||||
.add("Authorization", "Bearer ${oauth.access_token}")
|
.add("Authorization", "Bearer ${oauth.accessToken}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers)
|
return POST("$BASE_OAUTH_URL/token", body = formBody, headers = headers)
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import eu.kanade.tachiyomi.network.parseAs
|
|||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TokenExpired
|
import suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto.MALOAuth
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TokenRefreshFailed
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
@@ -15,11 +14,12 @@ class MyAnimeListInterceptor(
|
|||||||
) : Interceptor {
|
) : Interceptor {
|
||||||
private val json: Json by injectLazy()
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private var oauth: OAuth? = myanimelist.loadOAuth()
|
private var oauth: MALOAuth? = myanimelist.loadOAuth()
|
||||||
|
private val tokenExpired get() = myanimelist.getIfAuthExpired()
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
if (myanimelist.getIfAuthExpired()) {
|
if (tokenExpired) {
|
||||||
throw TokenExpired()
|
throw MALTokenExpired()
|
||||||
}
|
}
|
||||||
val originalRequest = chain.request()
|
val originalRequest = chain.request()
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class MyAnimeListInterceptor(
|
|||||||
val authRequest =
|
val authRequest =
|
||||||
originalRequest
|
originalRequest
|
||||||
.newBuilder()
|
.newBuilder()
|
||||||
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
|
.addHeader("Authorization", "Bearer ${oauth!!.accessToken}")
|
||||||
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()}")
|
.header("User-Agent", "Suwayomi v${AppInfo.getVersionName()}")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
@@ -46,37 +46,44 @@ class MyAnimeListInterceptor(
|
|||||||
* Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token
|
* Called when the user authenticates with MyAnimeList for the first time. Sets the refresh token
|
||||||
* and the oauth object.
|
* and the oauth object.
|
||||||
*/
|
*/
|
||||||
fun setAuth(oauth: OAuth?) {
|
fun setAuth(oauth: MALOAuth?) {
|
||||||
this.oauth = oauth
|
this.oauth = oauth
|
||||||
myanimelist.saveOAuth(oauth)
|
myanimelist.saveOAuth(oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshToken(chain: Interceptor.Chain): OAuth =
|
private fun refreshToken(chain: Interceptor.Chain): MALOAuth =
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
if (myanimelist.getIfAuthExpired()) throw TokenExpired()
|
if (tokenExpired) throw MALTokenExpired()
|
||||||
oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it }
|
oauth?.takeUnless { it.isExpired() }?.let { return@synchronized it }
|
||||||
|
|
||||||
val response =
|
val response =
|
||||||
try {
|
try {
|
||||||
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
throw TokenRefreshFailed()
|
throw MALTokenRefreshFailed()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.code == 401) {
|
if (response.code == 401) {
|
||||||
myanimelist.setAuthExpired()
|
myanimelist.setAuthExpired()
|
||||||
throw TokenExpired()
|
throw MALTokenExpired()
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCatching {
|
return runCatching {
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
with(json) { response.parseAs<OAuth>() }
|
with(json) { response.parseAs<MALOAuth>() }
|
||||||
} else {
|
} else {
|
||||||
response.close()
|
response.close()
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
?.also(::setAuth)
|
?.also {
|
||||||
?: throw TokenRefreshFailed()
|
this.oauth = it
|
||||||
|
myanimelist.saveOAuth(it)
|
||||||
|
}
|
||||||
|
?: throw MALTokenRefreshFailed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MALTokenRefreshFailed : IOException("MAL: Failed to refresh account token")
|
||||||
|
|
||||||
|
class MALTokenExpired : IOException("MAL: Login has expired")
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class OAuth(
|
|
||||||
val refresh_token: String,
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val created_at: Long = System.currentTimeMillis(),
|
|
||||||
val expires_in: Long,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun OAuth.isExpired() = System.currentTimeMillis() > created_at + (expires_in * 1000)
|
|
||||||
|
|
||||||
fun Track.toMyAnimeListStatus() =
|
fun Track.toMyAnimeListStatus() =
|
||||||
when (status) {
|
when (status) {
|
||||||
MyAnimeList.READING -> "reading"
|
MyAnimeList.READING -> "reading"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALListItem(
|
||||||
|
@SerialName("num_chapters")
|
||||||
|
val numChapters: Int,
|
||||||
|
@SerialName("my_list_status")
|
||||||
|
val myListStatus: MALListItemStatus?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALListItemStatus(
|
||||||
|
@SerialName("is_rereading")
|
||||||
|
val isRereading: Boolean,
|
||||||
|
val status: String,
|
||||||
|
@SerialName("num_chapters_read")
|
||||||
|
val numChaptersRead: Double,
|
||||||
|
val score: Int,
|
||||||
|
@SerialName("start_date")
|
||||||
|
val startDate: String?,
|
||||||
|
@SerialName("finish_date")
|
||||||
|
val finishDate: String?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALManga(
|
||||||
|
val id: Long,
|
||||||
|
val title: String,
|
||||||
|
val synopsis: String = "",
|
||||||
|
@SerialName("num_chapters")
|
||||||
|
val numChapters: Int,
|
||||||
|
val mean: Double = -1.0,
|
||||||
|
@SerialName("main_picture")
|
||||||
|
val covers: MALMangaCovers?,
|
||||||
|
val status: String,
|
||||||
|
@SerialName("media_type")
|
||||||
|
val mediaType: String,
|
||||||
|
@SerialName("start_date")
|
||||||
|
val startDate: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALMangaCovers(
|
||||||
|
val large: String = "",
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.EncodeDefault
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALOAuth(
|
||||||
|
@SerialName("token_type")
|
||||||
|
val tokenType: String,
|
||||||
|
@SerialName("refresh_token")
|
||||||
|
val refreshToken: String,
|
||||||
|
@SerialName("access_token")
|
||||||
|
val accessToken: String,
|
||||||
|
@SerialName("expires_in")
|
||||||
|
val expiresIn: Long,
|
||||||
|
@SerialName("created_at")
|
||||||
|
@EncodeDefault
|
||||||
|
val createdAt: Long = System.currentTimeMillis() / 1000,
|
||||||
|
) {
|
||||||
|
// Assumes expired a minute earlier
|
||||||
|
private val adjustedExpiresIn: Long = (expiresIn - 60)
|
||||||
|
|
||||||
|
fun isExpired() = createdAt + adjustedExpiresIn < System.currentTimeMillis() / 1000
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALSearchResult(
|
||||||
|
val data: List<MALSearchResultNode>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALSearchResultNode(
|
||||||
|
val node: MALSearchResultItem,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALSearchResultItem(
|
||||||
|
val id: Int,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALUser(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.track.tracker.myanimelist.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALUserSearchResult(
|
||||||
|
val data: List<MALUserSearchItem>,
|
||||||
|
val paging: MALUserSearchPaging,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALUserSearchItem(
|
||||||
|
val node: MALUserSearchItemNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALUserSearchPaging(
|
||||||
|
val next: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MALUserSearchItemNode(
|
||||||
|
val id: Int,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
@@ -22,4 +22,5 @@ data class TrackRecordDataClass(
|
|||||||
val remoteUrl: String,
|
val remoteUrl: String,
|
||||||
val startDate: Long,
|
val startDate: Long,
|
||||||
val finishDate: Long,
|
val finishDate: Long,
|
||||||
|
val private: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ data class TrackSearchDataClass(
|
|||||||
val id: Int,
|
val id: Int,
|
||||||
val trackerId: Int,
|
val trackerId: Int,
|
||||||
val remoteId: Long,
|
val remoteId: Long,
|
||||||
|
val libraryId: Long?,
|
||||||
val title: String,
|
val title: String,
|
||||||
|
val lastChapterRead: Double,
|
||||||
val totalChapters: Int,
|
val totalChapters: Int,
|
||||||
val trackingUrl: String,
|
val trackingUrl: String,
|
||||||
val coverUrl: String,
|
val coverUrl: String,
|
||||||
@@ -22,4 +24,10 @@ data class TrackSearchDataClass(
|
|||||||
val publishingStatus: String,
|
val publishingStatus: String,
|
||||||
val publishingType: String,
|
val publishingType: String,
|
||||||
val startDate: String,
|
val startDate: String,
|
||||||
|
val status: Int,
|
||||||
|
val score: Double,
|
||||||
|
var scoreString: String?,
|
||||||
|
val startedReadingDate: Long,
|
||||||
|
val finishedReadingDate: Long,
|
||||||
|
val private: Boolean,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,4 +24,5 @@ object TrackRecordTable : IntIdTable() {
|
|||||||
val remoteUrl = varchar("remote_url", 512)
|
val remoteUrl = varchar("remote_url", 512)
|
||||||
val startDate = long("start_date")
|
val startDate = long("start_date")
|
||||||
val finishDate = long("finish_date")
|
val finishDate = long("finish_date")
|
||||||
|
val private = bool("private").default(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,13 +29,22 @@ object TrackSearchTable : IntIdTable() {
|
|||||||
val publishingStatus = truncatingVarchar("publishing_status", 512)
|
val publishingStatus = truncatingVarchar("publishing_status", 512)
|
||||||
val publishingType = truncatingVarchar("publishing_type", 512)
|
val publishingType = truncatingVarchar("publishing_type", 512)
|
||||||
val startDate = truncatingVarchar("start_date", 128)
|
val startDate = truncatingVarchar("start_date", 128)
|
||||||
|
val libraryId = long("library_id").nullable().default(null)
|
||||||
|
val lastChapterRead = double("last_chapter_read").default(0.0)
|
||||||
|
val status = integer("status").default(0)
|
||||||
|
val score = double("score").default(0.0)
|
||||||
|
val startedReadingDate = long("started_reading_date").default(0)
|
||||||
|
val finishedReadingDate = long("finished_reading_date").default(0)
|
||||||
|
val private = bool("private").default(false)
|
||||||
|
val authors = truncatingVarchar("authors", 256).nullable().default(null)
|
||||||
|
val artists = truncatingVarchar("artists", 256).nullable().default(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
||||||
if (isEmpty()) return emptyList()
|
if (isEmpty()) return emptyList()
|
||||||
return transaction {
|
return transaction {
|
||||||
val trackerIds = map { it.sync_id }.toSet()
|
val trackerIds = map { it.tracker_id }.toSet()
|
||||||
val remoteIds = map { it.media_id }.toSet()
|
val remoteIds = map { it.remote_id }.toSet()
|
||||||
val existing =
|
val existing =
|
||||||
transaction {
|
transaction {
|
||||||
TrackSearchTable
|
TrackSearchTable
|
||||||
@@ -50,8 +59,8 @@ fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
|||||||
forEach { trackSearch ->
|
forEach { trackSearch ->
|
||||||
val existingRow =
|
val existingRow =
|
||||||
existing.find {
|
existing.find {
|
||||||
it[TrackSearchTable.trackerId] == trackSearch.sync_id &&
|
it[TrackSearchTable.trackerId] == trackSearch.tracker_id &&
|
||||||
it[TrackSearchTable.remoteId] == trackSearch.media_id
|
it[TrackSearchTable.remoteId] == trackSearch.remote_id
|
||||||
}
|
}
|
||||||
grouped
|
grouped
|
||||||
.getOrPut(existingRow != null) { mutableListOf() }
|
.getOrPut(existingRow != null) { mutableListOf() }
|
||||||
@@ -72,6 +81,15 @@ fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
|||||||
this[TrackSearchTable.publishingStatus] = trackSearch.publishing_status
|
this[TrackSearchTable.publishingStatus] = trackSearch.publishing_status
|
||||||
this[TrackSearchTable.publishingType] = trackSearch.publishing_type
|
this[TrackSearchTable.publishingType] = trackSearch.publishing_type
|
||||||
this[TrackSearchTable.startDate] = trackSearch.start_date
|
this[TrackSearchTable.startDate] = trackSearch.start_date
|
||||||
|
this[TrackSearchTable.libraryId] = trackSearch.library_id
|
||||||
|
this[TrackSearchTable.lastChapterRead] = trackSearch.last_chapter_read
|
||||||
|
this[TrackSearchTable.status] = trackSearch.status
|
||||||
|
this[TrackSearchTable.score] = trackSearch.score
|
||||||
|
this[TrackSearchTable.startedReadingDate] = trackSearch.started_reading_date
|
||||||
|
this[TrackSearchTable.finishedReadingDate] = trackSearch.finished_reading_date
|
||||||
|
this[TrackSearchTable.private] = trackSearch.private
|
||||||
|
this[TrackSearchTable.authors] = trackSearch.authors.ifEmpty { null }?.joinToString(",")
|
||||||
|
this[TrackSearchTable.artists] = trackSearch.artists.ifEmpty { null }?.joinToString(",")
|
||||||
}
|
}
|
||||||
execute(this@transaction)
|
execute(this@transaction)
|
||||||
}
|
}
|
||||||
@@ -79,8 +97,8 @@ fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
|||||||
val insertedRows =
|
val insertedRows =
|
||||||
if (!toInsert.isNullOrEmpty()) {
|
if (!toInsert.isNullOrEmpty()) {
|
||||||
TrackSearchTable.batchInsert(toInsert) {
|
TrackSearchTable.batchInsert(toInsert) {
|
||||||
this[TrackSearchTable.trackerId] = it.sync_id
|
this[TrackSearchTable.trackerId] = it.tracker_id
|
||||||
this[TrackSearchTable.remoteId] = it.media_id
|
this[TrackSearchTable.remoteId] = it.remote_id
|
||||||
this[TrackSearchTable.title] = it.title
|
this[TrackSearchTable.title] = it.title
|
||||||
this[TrackSearchTable.totalChapters] = it.total_chapters
|
this[TrackSearchTable.totalChapters] = it.total_chapters
|
||||||
this[TrackSearchTable.trackingUrl] = it.tracking_url
|
this[TrackSearchTable.trackingUrl] = it.tracking_url
|
||||||
@@ -89,6 +107,15 @@ fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
|||||||
this[TrackSearchTable.publishingStatus] = it.publishing_status
|
this[TrackSearchTable.publishingStatus] = it.publishing_status
|
||||||
this[TrackSearchTable.publishingType] = it.publishing_type
|
this[TrackSearchTable.publishingType] = it.publishing_type
|
||||||
this[TrackSearchTable.startDate] = it.start_date
|
this[TrackSearchTable.startDate] = it.start_date
|
||||||
|
this[TrackSearchTable.libraryId] = it.library_id
|
||||||
|
this[TrackSearchTable.lastChapterRead] = it.last_chapter_read
|
||||||
|
this[TrackSearchTable.status] = it.status
|
||||||
|
this[TrackSearchTable.score] = it.score
|
||||||
|
this[TrackSearchTable.startedReadingDate] = it.started_reading_date
|
||||||
|
this[TrackSearchTable.finishedReadingDate] = it.finished_reading_date
|
||||||
|
this[TrackSearchTable.private] = it.private
|
||||||
|
this[TrackSearchTable.authors] = it.authors.ifEmpty { null }?.joinToString(",")
|
||||||
|
this[TrackSearchTable.artists] = it.artists.ifEmpty { null }?.joinToString(",")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyList()
|
emptyList()
|
||||||
@@ -104,8 +131,8 @@ fun List<TrackSearch>.insertAll(): List<ResultRow> {
|
|||||||
(insertedRows + updatedRows)
|
(insertedRows + updatedRows)
|
||||||
.sortedBy { row ->
|
.sortedBy { row ->
|
||||||
indexOfFirst {
|
indexOfFirst {
|
||||||
it.sync_id == row[TrackSearchTable.trackerId] &&
|
it.tracker_id == row[TrackSearchTable.trackerId] &&
|
||||||
it.media_id == row[TrackSearchTable.remoteId]
|
it.remote_id == row[TrackSearchTable.remoteId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package suwayomi.tachidesk.server.database.migration
|
||||||
|
|
||||||
|
import de.neonew.exposed.migrations.helpers.SQLMigration
|
||||||
|
|
||||||
|
@Suppress("ClassName", "unused")
|
||||||
|
class M0048_AddTrackingColumns : SQLMigration() {
|
||||||
|
fun createNewColumn(
|
||||||
|
tableName: String,
|
||||||
|
columnName: String,
|
||||||
|
columnType: String,
|
||||||
|
default: String,
|
||||||
|
notNull: Boolean = false,
|
||||||
|
) = "ALTER TABLE $tableName" +
|
||||||
|
" ADD COLUMN $columnName $columnType DEFAULT $default${if (notNull) " NOT NULL" else ""};"
|
||||||
|
|
||||||
|
override val sql: String =
|
||||||
|
"""
|
||||||
|
${createNewColumn("TRACKRECORD", "PRIVATE", "BOOLEAN", "FALSE", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "LIBRARY_ID", "BIGINT", "NULL")}
|
||||||
|
${createNewColumn("TRACKSEARCH", "LAST_CHAPTER_READ", "DOUBLE PRECISION", "0", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "STATUS", "INT", "0", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "SCORE", "DOUBLE PRECISION", "0", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "STARTED_READING_DATE", "BIGINT", "0", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "FINISHED_READING_DATE", "BIGINT", "0", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "PRIVATE", "BOOLEAN", "FALSE", notNull = true)}
|
||||||
|
${createNewColumn("TRACKSEARCH", "AUTHORS", "VARCHAR(256)", "NULL")}
|
||||||
|
${createNewColumn("TRACKSEARCH", "ARTISTS", "VARCHAR(256)", "NULL")}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user