Use kotlin serialization for sync

This commit is contained in:
Koitharu
2025-06-09 12:43:33 +03:00
parent b854ca8807
commit 4ef6908e82
12 changed files with 370 additions and 154 deletions

View File

@@ -7,6 +7,7 @@ plugins {
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
id 'androidx.room'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
@@ -152,6 +153,7 @@ dependencies {
implementation libs.okhttp.tls
implementation libs.okhttp.dnsoverhttps
implementation libs.okio
implementation libs.kotlinx.serialization.json
implementation libs.adapterdelegates
implementation libs.adapterdelegates.viewbinding

View File

@@ -2,40 +2,23 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.ContentValues
import android.database.Cursor
import org.json.JSONObject
fun Cursor.toJson(): JSONObject {
val jo = JSONObject()
for (i in 0 until columnCount) {
val name = getColumnName(i)
when (getType(i)) {
Cursor.FIELD_TYPE_STRING -> jo.put(name, getString(i))
Cursor.FIELD_TYPE_FLOAT -> jo.put(name, getDouble(i))
Cursor.FIELD_TYPE_INTEGER -> jo.put(name, getLong(i))
Cursor.FIELD_TYPE_NULL -> jo.put(name, null)
Cursor.FIELD_TYPE_BLOB -> jo.put(name, getBlob(i))
}
}
return jo
}
fun JSONObject.toContentValues(): ContentValues {
val cv = ContentValues(length())
for (key in keys()) {
val name = key.escapeName()
when (val value = get(key)) {
JSONObject.NULL, "null", null -> cv.putNull(name)
is String -> cv.put(name, value)
is Float -> cv.put(name, value)
is Double -> cv.put(name, value)
is Int -> cv.put(name, value)
is Long -> cv.put(name, value)
else -> throw IllegalArgumentException("Value $value cannot be putted in ContentValues")
}
}
return cv
}
private fun String.escapeName() = "`$this`"
import androidx.collection.ArraySet
fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0
inline fun <T> Cursor.map(mapper: (Cursor) -> T): List<T> = mapTo(ArrayList(count), mapper)
inline fun <T> Cursor.mapToSet(mapper: (Cursor) -> T): Set<T> = mapTo(ArraySet(count), mapper)
inline fun <T, C: MutableCollection<in T>> Cursor.mapTo(destination: C, mapper: (Cursor) -> T): C = use { c ->
if (c.moveToFirst()) {
do {
destination.add(mapper(c))
} while (c.moveToNext())
}
destination
}
inline fun buildContentValues(capacity: Int, block: ContentValues.() -> Unit): ContentValues {
return ContentValues(capacity).apply(block)
}

View File

@@ -0,0 +1,42 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
import org.koitharu.kotatsu.core.util.ext.getBoolean
@Serializable
data class FavouriteCategorySyncDto(
@SerialName("category_id") val categoryId: Int,
@SerialName("created_at") val createdAt: Long,
@SerialName("sort_key") val sortKey: Int,
@SerialName("title") val title: String,
@SerialName("order") val order: String,
@SerialName("track") val track: Boolean,
@SerialName("show_in_lib") val isVisibleInLibrary: Boolean,
@SerialName("deleted_at") val deletedAt: Long,
) {
constructor(cursor: Cursor) : this(
categoryId = cursor.getInt(cursor.getColumnIndexOrThrow("category_id")),
createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("created_at")),
sortKey = cursor.getInt(cursor.getColumnIndexOrThrow("sort_key")),
title = cursor.getString(cursor.getColumnIndexOrThrow("title")),
order = cursor.getString(cursor.getColumnIndexOrThrow("order")),
track = cursor.getBoolean(cursor.getColumnIndexOrThrow("track")),
isVisibleInLibrary = cursor.getBoolean(cursor.getColumnIndexOrThrow("show_in_lib")),
deletedAt = cursor.getLong(cursor.getColumnIndexOrThrow("deleted_at")),
)
fun toContentValues() = buildContentValues(8) {
put("category_id", categoryId)
put("created_at", createdAt)
put("sort_key", sortKey)
put("title", title)
put("order", order)
put("track", track)
put("show_in_lib", isVisibleInLibrary)
put("deleted_at", deletedAt)
}
}

View File

@@ -0,0 +1,38 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
import org.koitharu.kotatsu.core.util.ext.getBoolean
@Serializable
data class FavouriteSyncDto(
@SerialName("manga_id") val mangaId: Long,
@SerialName("manga") val manga: MangaSyncDto,
@SerialName("category_id") val categoryId: Int,
@SerialName("sort_key") val sortKey: Int,
@SerialName("pinned") val pinned: Boolean,
@SerialName("created_at") val createdAt: Long,
@SerialName("deleted_at") var deletedAt: Long,
) {
constructor(cursor: Cursor, manga: MangaSyncDto) : this(
mangaId = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")),
manga = manga,
categoryId = cursor.getInt(cursor.getColumnIndexOrThrow("category_id")),
sortKey = cursor.getInt(cursor.getColumnIndexOrThrow("sort_key")),
pinned = cursor.getBoolean(cursor.getColumnIndexOrThrow("pinned")),
createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("created_at")),
deletedAt = cursor.getLong(cursor.getColumnIndexOrThrow("deleted_at")),
)
fun toContentValues() = buildContentValues(6) {
put("manga_id", mangaId)
put("category_id", categoryId)
put("sort_key", sortKey)
put("pinned", pinned)
put("created_at", createdAt)
put("deleted_at", deletedAt)
}
}

View File

@@ -0,0 +1,46 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
@Serializable
data class HistorySyncDto(
@SerialName("manga_id") val mangaId: Long,
@SerialName("created_at") val createdAt: Long,
@SerialName("updated_at") val updatedAt: Long,
@SerialName("chapter_id") val chapterId: Long,
@SerialName("page") val page: Int,
@SerialName("scroll") val scroll: Float,
@SerialName("percent") val percent: Float,
@SerialName("deleted_at") val deletedAt: Long,
@SerialName("chapters") val chaptersCount: Int,
@SerialName("manga") val manga: MangaSyncDto,
) {
constructor(cursor: Cursor, manga: MangaSyncDto) : this(
mangaId = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")),
createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("created_at")),
updatedAt = cursor.getLong(cursor.getColumnIndexOrThrow("updated_at")),
chapterId = cursor.getLong(cursor.getColumnIndexOrThrow("chapter_id")),
page = cursor.getInt(cursor.getColumnIndexOrThrow("page")),
scroll = cursor.getFloat(cursor.getColumnIndexOrThrow("scroll")),
percent = cursor.getFloat(cursor.getColumnIndexOrThrow("percent")),
deletedAt = cursor.getLong(cursor.getColumnIndexOrThrow("deleted_at")),
chaptersCount = cursor.getInt(cursor.getColumnIndexOrThrow("chapters")),
manga = manga,
)
fun toContentValues() = buildContentValues(9) {
put("manga_id", mangaId)
put("created_at", createdAt)
put("updated_at", updatedAt)
put("chapter_id", chapterId)
put("page", page)
put("scroll", scroll)
put("percent", percent)
put("deleted_at", deletedAt)
put("chapters", chaptersCount)
}
}

View File

@@ -0,0 +1,56 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import androidx.core.database.getStringOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
@Serializable
data class MangaSyncDto(
@SerialName("manga_id") val id: Long,
@SerialName("title") val title: String,
@SerialName("alt_title") val altTitle: String?,
@SerialName("url") val url: String,
@SerialName("public_url") val publicUrl: String,
@SerialName("rating") val rating: Float,
@SerialName("content_rating") val contentRating: String?,
@SerialName("cover_url") val coverUrl: String,
@SerialName("large_cover_url") val largeCoverUrl: String?,
@SerialName("tags") val tags: Set<MangaTagSyncDto>,
@SerialName("state") val state: String?,
@SerialName("author") val author: String?,
@SerialName("source") val source: String,
) {
constructor(cursor: Cursor, tags: Set<MangaTagSyncDto>) : this(
id = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")),
title = cursor.getString(cursor.getColumnIndexOrThrow("title")),
altTitle = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("alt_title")),
url = cursor.getString(cursor.getColumnIndexOrThrow("url")),
publicUrl = cursor.getString(cursor.getColumnIndexOrThrow("public_url")),
rating = cursor.getFloat(cursor.getColumnIndexOrThrow("rating")),
contentRating = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("content_rating")),
coverUrl = cursor.getString(cursor.getColumnIndexOrThrow("cover_url")),
largeCoverUrl = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("large_cover_url")),
tags = tags,
state = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("state")),
author = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("author")),
source = cursor.getString(cursor.getColumnIndexOrThrow("source")),
)
fun toContentValues() = buildContentValues(12) {
put("manga_id", id)
put("title", title)
put("alt_title", altTitle)
put("url", url)
put("public_url", publicUrl)
put("rating", rating)
put("content_rating", contentRating)
put("cover_url", coverUrl)
put("large_cover_url", largeCoverUrl)
put("state", state)
put("author", author)
put("source", source)
}
}

View File

@@ -0,0 +1,29 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
@Serializable
data class MangaTagSyncDto(
@SerialName("tag_id") val id: Long,
@SerialName("title") val title: String,
@SerialName("key") val key: String,
@SerialName("source") val source: String,
) {
constructor(cursor: Cursor) : this(
id = cursor.getLong(cursor.getColumnIndexOrThrow("tag_id")),
title = cursor.getString(cursor.getColumnIndexOrThrow("title")),
key = cursor.getString(cursor.getColumnIndexOrThrow("key")),
source = cursor.getString(cursor.getColumnIndexOrThrow("source")),
)
fun toContentValues() = buildContentValues(4) {
put("tag_id", id)
put("title", title)
put("key", key)
put("source", source)
}
}

View File

@@ -0,0 +1,12 @@
package org.koitharu.kotatsu.sync.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SyncDto(
@SerialName("history") val history: List<HistorySyncDto>?,
@SerialName("categories") val categories: List<FavouriteCategorySyncDto>?,
@SerialName("favourites") val favourites: List<FavouriteSyncDto>?,
@SerialName("timestamp") val timestamp: Long,
)

View File

@@ -11,16 +11,20 @@ import android.content.SyncStats
import android.database.Cursor
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf
import androidx.core.net.toUri
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.IOException
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
@@ -30,20 +34,23 @@ import org.koitharu.kotatsu.core.db.TABLE_MANGA
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
import org.koitharu.kotatsu.core.db.TABLE_TAGS
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.util.ext.parseJsonOrNull
import org.koitharu.kotatsu.core.util.ext.buildContentValues
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.mapToSet
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toContentValues
import org.koitharu.kotatsu.core.util.ext.toJson
import org.koitharu.kotatsu.core.util.ext.toRequestBody
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
import org.koitharu.kotatsu.sync.data.SyncInterceptor
import org.koitharu.kotatsu.sync.data.SyncSettings
import org.koitharu.kotatsu.sync.data.model.FavouriteCategorySyncDto
import org.koitharu.kotatsu.sync.data.model.FavouriteSyncDto
import org.koitharu.kotatsu.sync.data.model.HistorySyncDto
import org.koitharu.kotatsu.sync.data.model.MangaSyncDto
import org.koitharu.kotatsu.sync.data.model.MangaTagSyncDto
import org.koitharu.kotatsu.sync.data.model.SyncDto
import java.net.HttpURLConnection
import java.util.concurrent.TimeUnit
private const val FIELD_TIMESTAMP = "timestamp"
class SyncHelper @AssistedInject constructor(
@ApplicationContext context: Context,
@BaseHttpClient baseHttpClient: OkHttpClient,
@@ -54,6 +61,7 @@ class SyncHelper @AssistedInject constructor(
private val authorityHistory = context.getString(R.string.sync_authority_history)
private val authorityFavourites = context.getString(R.string.sync_authority_favourites)
private val mediaTypeJson = "application/json".toMediaType()
private val httpClient = baseHttpClient.newBuilder()
.authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient())))
.addInterceptor(SyncInterceptor(context, account))
@@ -66,20 +74,26 @@ class SyncHelper @AssistedInject constructor(
@WorkerThread
fun syncFavourites(stats: SyncStats) {
val data = JSONObject()
data.put(TABLE_FAVOURITE_CATEGORIES, getFavouriteCategories())
data.put(TABLE_FAVOURITES, getFavourites())
data.put(FIELD_TIMESTAMP, System.currentTimeMillis())
val payload = Json.encodeToString(
SyncDto(
history = null,
favourites = getFavourites(),
categories = getFavouriteCategories(),
timestamp = System.currentTimeMillis(),
),
)
val request = Request.Builder()
.url("$baseUrl/resource/$TABLE_FAVOURITES")
.post(data.toRequestBody())
.post(payload.toRequestBody(mediaTypeJson))
.build()
val response = httpClient.newCall(request).execute().parseJsonOrNull()
if (response != null) {
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES))
val response = httpClient.newCall(request).execute().parseDtoOrNull()
response?.categories?.let { categories ->
val categoriesResult = upsertFavouriteCategories(categories)
stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
stats.numInserts += categoriesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
val favouritesResult = upsertFavourites(response.getJSONArray(TABLE_FAVOURITES))
}
response?.favourites?.let { favourites ->
val favouritesResult = upsertFavourites(favourites)
stats.numDeletes += favouritesResult.first().count?.toLong() ?: 0L
stats.numInserts += favouritesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes
@@ -87,20 +101,24 @@ class SyncHelper @AssistedInject constructor(
gcFavourites()
}
@Blocking
@WorkerThread
fun syncHistory(stats: SyncStats) {
val data = JSONObject()
data.put(TABLE_HISTORY, getHistory())
data.put(FIELD_TIMESTAMP, System.currentTimeMillis())
val payload = Json.encodeToString(
SyncDto(
history = getHistory(),
favourites = null,
categories = null,
timestamp = System.currentTimeMillis(),
),
)
val request = Request.Builder()
.url("$baseUrl/resource/$TABLE_HISTORY")
.post(data.toRequestBody())
.post(payload.toRequestBody(mediaTypeJson))
.build()
val response = httpClient.newCall(request).execute().parseJsonOrNull()
if (response != null) {
val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY),
)
val response = httpClient.newCall(request).execute().parseDtoOrNull()
response?.history?.let { history ->
val result = upsertHistory(history)
stats.numDeletes += result.first().count?.toLong() ?: 0L
stats.numInserts += result.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes
@@ -118,140 +136,119 @@ class SyncHelper @AssistedInject constructor(
}
}
private fun upsertHistory(json: JSONArray): Array<ContentProviderResult> {
private fun upsertHistory(history: List<HistorySyncDto>): Array<ContentProviderResult> {
val uri = uri(authorityHistory, TABLE_HISTORY)
val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo ->
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityHistory))
history.mapTo(operations) {
operations.addAll(upsertManga(it.manga, authorityHistory))
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.withValues(it.toContentValues())
.build()
}
return provider.applyBatch(operations)
}
private fun upsertFavouriteCategories(json: JSONArray): Array<ContentProviderResult> {
private fun upsertFavouriteCategories(categories: List<FavouriteCategorySyncDto>): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo ->
categories.mapTo(operations) {
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.withValues(it.toContentValues())
.build()
}
return provider.applyBatch(operations)
}
private fun upsertFavourites(json: JSONArray): Array<ContentProviderResult> {
private fun upsertFavourites(favourites: List<FavouriteSyncDto>): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITES)
val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo ->
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityFavourites))
favourites.mapTo(operations) {
operations.addAll(upsertManga(it.manga, authorityFavourites))
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.withValues(it.toContentValues())
.build()
}
return provider.applyBatch(operations)
}
private fun upsertManga(json: JSONObject, authority: String): List<ContentProviderOperation> {
val tags = json.removeJSONArray(TABLE_TAGS)
val result = ArrayList<ContentProviderOperation>(tags.length() * 2 + 1)
for (i in 0 until tags.length()) {
val tag = tags.getJSONObject(i)
private fun upsertManga(manga: MangaSyncDto, authority: String): List<ContentProviderOperation> {
val tags = manga.tags
val result = ArrayList<ContentProviderOperation>(tags.size * 2 + 1)
for (tag in tags) {
result += ContentProviderOperation.newInsert(uri(authority, TABLE_TAGS))
.withValues(tag.toContentValues())
.build()
result += ContentProviderOperation.newInsert(uri(authority, TABLE_MANGA_TAGS))
.withValues(
contentValuesOf(
"manga_id" to json.getLong("manga_id"),
"tag_id" to tag.getLong("tag_id"),
),
buildContentValues(2) {
put("manga_id", manga.id)
put("tag_id", tag.id)
},
).build()
}
result.add(
0,
ContentProviderOperation.newInsert(uri(authority, TABLE_MANGA))
.withValues(json.toContentValues())
.withValues(manga.toContentValues())
.build(),
)
return result
}
private fun getHistory(): JSONArray {
private fun getHistory(): List<HistorySyncDto> {
return provider.query(authorityHistory, TABLE_HISTORY).use { cursor ->
val json = JSONArray()
val result = ArrayList<HistorySyncDto>(cursor.count)
if (cursor.moveToFirst()) {
do {
val jo = cursor.toJson()
jo.put("manga", getManga(authorityHistory, jo.getLong("manga_id")))
json.put(jo)
val mangaId = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id"))
result.add(HistorySyncDto(cursor, getManga(authorityHistory, mangaId)))
} while (cursor.moveToNext())
}
json
result
}
}
private fun getFavourites(): JSONArray {
return provider.query(authorityFavourites, TABLE_FAVOURITES).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
val jo = cursor.toJson()
jo.put("manga", getManga(authorityFavourites, jo.getLong("manga_id")))
json.put(jo)
} while (cursor.moveToNext())
}
json
private fun getFavourites(): List<FavouriteSyncDto> {
return provider.query(authorityFavourites, TABLE_FAVOURITES).map { cursor ->
val manga = getManga(authorityFavourites, cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")))
FavouriteSyncDto(cursor, manga)
}
}
private fun getFavouriteCategories(): JSONArray {
return provider.query(authorityFavourites, TABLE_FAVOURITE_CATEGORIES).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
json.put(cursor.toJson())
} while (cursor.moveToNext())
}
json
private fun getFavouriteCategories(): List<FavouriteCategorySyncDto> =
provider.query(authorityFavourites, TABLE_FAVOURITE_CATEGORIES).map { cursor ->
FavouriteCategorySyncDto(cursor)
}
private fun getManga(authority: String, id: Long): MangaSyncDto {
val tags = requireNotNull(
provider.query(
uri(authority, TABLE_MANGA_TAGS),
arrayOf("tag_id"),
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.mapToSet {
val tagId = it.getLong(it.getColumnIndexOrThrow("tag_id"))
getTag(authority, tagId)
},
)
return requireNotNull(
provider.query(
uri(authority, TABLE_MANGA),
null,
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.use { cursor ->
cursor.moveToFirst()
MangaSyncDto(cursor, tags)
},
)
}
private fun getManga(authority: String, id: Long): JSONObject {
val manga = provider.query(
uri(authority, TABLE_MANGA),
null,
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.use { cursor ->
cursor.moveToFirst()
cursor.toJson()
}
requireNotNull(manga)
val tags = provider.query(
uri(authority, TABLE_MANGA_TAGS),
arrayOf("tag_id"),
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
val tagId = cursor.getLong(0)
json.put(getTag(authority, tagId))
} while (cursor.moveToNext())
}
json
}
manga.put("tags", requireNotNull(tags))
return manga
}
private fun getTag(authority: String, tagId: Long): JSONObject {
val tag = provider.query(
private fun getTag(authority: String, tagId: Long): MangaTagSyncDto = requireNotNull(
provider.query(
uri(authority, TABLE_TAGS),
null,
"tag_id = ?",
@@ -259,13 +256,12 @@ class SyncHelper @AssistedInject constructor(
null,
)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.toJson()
MangaTagSyncDto(cursor)
} else {
null
}
}
return requireNotNull(tag)
}
},
)
private fun gcFavourites() {
val deletedAt = System.currentTimeMillis() - defaultGcPeriod
@@ -290,9 +286,17 @@ class SyncHelper @AssistedInject constructor(
private fun uri(authority: String, table: String) = "content://$authority/$table".toUri()
private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject
private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray
private fun Response.parseDtoOrNull(): SyncDto? {
return try {
when {
!isSuccessful -> throw IOException(body?.string())
code == HttpURLConnection.HTTP_NO_CONTENT -> null
else -> Json.decodeFromString<SyncDto>(body?.string() ?: return null)
}
} finally {
closeQuietly()
}
}
@AssistedFactory
interface Factory {

View File

@@ -44,8 +44,8 @@
<item>1</item>
</string-array>
<string-array name="sync_url_list" translatable="false">
<item>https://moe.shirizu.org</item>
<item>https://sync.kotatsu.app</item>
<item>https://moe.shirizu.org</item>
<item>http://54.254.71.100</item>
<item>http://86.57.183.214:8081</item>
</string-array>

View File

@@ -4,4 +4,5 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.kotlinx.serizliation) apply false
}

View File

@@ -36,6 +36,7 @@ parsers = "bcde8ef2a2"
preference = "1.2.1"
recyclerview = "1.4.0"
room = "2.7.1"
serialization = "1.8.1"
ssiv = "9a67b6a7c9"
swiperefreshlayout = "1.1.0"
testRules = "1.6.1"
@@ -96,6 +97,7 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "serialization" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" }
@@ -112,7 +114,8 @@ workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "w
[plugins]
android-application = { id = "com.android.application", version.ref = "gradle" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinx-serizliation = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "room" }